SoapHandler.java
/*
** Module : SoapHandler.java
** Abstract : Handler for SOAP services.
**
** Copyright (c) 2020-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------------Description----------------------------------------
** 001 CA 20190518 First version.
** CA 20200528 Fixes for extent parameters, BEFORE-TABLE and error management.
** 002 CA 20211012 Fixed generated WSDL and reworked SOAP support to rely on namespace, when resolving an
** operation.
** CA 20211112 Refactored to allow transfer of the DATASET or TABLE via XML. Added support for
** DATASET-HANDLE and TABLE-HANDLE.
** CA 20211214 For State-free appservers, the agents can be bound to remote persistent procedures; this
** binding must be made using the agent's ID, as a client can invoke multiple remote
** persistent procedures, and using the procedure's ID for this binding can lead to
** collisions, as the pair (connection ID, procedure ID) is not guaranteed to be unique for
** a connection.
** CA 20220120 Fixed charset encoding when reading the SOAP body.
** CA 20220206 removeNamespaces must keep any default namespace (its goal is to remove only namespaces
** used at the SOAP envelope - request or response).
** CA 20220329 The agentId and resourceId must be resolved on the worker thread.
** CA 20220405 Added authentication and authorization for web requests. When this is enabled, the target
** API call will be executed under the authenticated FWD context, and not the agent's context.
** 003 SBI 20230324 Changed registerService to return the registered RestService.
** 004 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
** 005 GBB 20240524 Adding configurations for wsdl soap:address location url (host and port).
** 006 GBB 20240610 Namespace for request body to be resolved from schema instead of wsdl definition.
** Handle paths without trailing slash without redirect. Helper to be disconnected on quit.
** Rename vars for clarity.
** 007 ICP 20240705 Fixed method of obtaining the class loader.
** 008 SB 20241108 Prevent namespace removal for elements using the default namespace. Refs #9309.
** 009 RAA 20241210 Updated the log message in registerService() for when the service was not found.
** 010 AL2 20250325 Let the SOAP response arguments hook onto a reset event in order to drop its
** serializer and avoid memory leaks.
** 011 GBB 20250403 Replace service type with enum constant.
** 012 RB2 20250530 The handle method now calls the handle method with an extra parameter, the request. This
** was done in order to handle multiple URLs in the Access-Control-Allow-Origin field
*/
/*
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU Affero General Public License as
** published by the Free Software Foundation, either version 3 of the
** License, or (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
** GNU Affero General Public License for more details.
**
** You may find a copy of the GNU Affero GPL version 3 at the following
** location: https://www.gnu.org/licenses/agpl-3.0.en.html
**
** Additional terms under GNU Affero GPL version 3 section 7:
**
** Under Section 7 of the GNU Affero GPL version 3, the following additional
** terms apply to the works covered under the License. These additional terms
** are non-permissive additional terms allowed under Section 7 of the GNU
** Affero GPL version 3 and may not be removed by you.
**
** 0. Attribution Requirement.
**
** You must preserve all legal notices or author attributions in the covered
** work or Appropriate Legal Notices displayed by works containing the covered
** work. You may not remove from the covered work any author or developer
** credit already included within the covered work.
**
** 1. No License To Use Trademarks.
**
** This license does not grant any license or rights to use the trademarks
** Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
** of Golden Code Development Corporation. You are not authorized to use the
** name Golden Code, FWD, or the names of any author or contributor, for
** publicity purposes without written authorization.
**
** 2. No Misrepresentation of Affiliation.
**
** You may not represent yourself as Golden Code Development Corporation or FWD.
**
** You may not represent yourself for publicity purposes as associated with
** Golden Code Development Corporation, FWD, or any author or contributor to
** the covered work, without written authorization.
**
** 3. No Misrepresentation of Source or Origin.
**
** You may not represent the covered work as solely your work. All modified
** versions of the covered work must be marked in a reasonable way to make it
** clear that the modified work is not originating from Golden Code Development
** Corporation or FWD. All modified versions must contain the notices of
** attribution required in this license.
*/
package com.goldencode.p2j.soap;
import java.io.*;
import java.lang.reflect.*;
import java.util.*;
import java.util.function.*;
import java.util.logging.*;
import javax.servlet.http.*;
import javax.wsdl.*;
import javax.wsdl.Part;
import javax.wsdl.extensions.*;
import javax.wsdl.factory.*;
import javax.wsdl.xml.*;
import javax.xml.namespace.*;
import javax.xml.parsers.ParserConfigurationException;
import com.goldencode.p2j.classloader.MultiClassLoader;
import com.goldencode.p2j.util.logging.*;
import org.apache.axiom.om.*;
import org.apache.axiom.soap.*;
import org.apache.axiom.soap.SOAPHeader;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.server.*;
import org.eclipse.jetty.server.handler.*;
import org.w3c.dom.*;
import org.xml.sax.SAXException;
import com.goldencode.p2j.main.*;
import com.goldencode.p2j.oo.lang.*;
import com.goldencode.p2j.rest.*;
import com.goldencode.p2j.soap.WsdlConfig.WSDLSchema;
import com.goldencode.p2j.util.*;
import com.goldencode.util.XmlHelper;
/**
* Process SOAP service requests and delegate the calls to the mapped service.
* <p>
* Service registration is being performed via {@link SourceNameMapper}, which loads the programs marked with
* <code>with-services</code>, via {@link #addService} API.
* <p>
* Any converted Java code mapped to a service must have a class-level {@link LegacyService} annotation. If
* this has operations defined via internal entries (procedure, function, method), then each Java method must
* be annotated via {@link LegacyService}.
* <p>
* The implicit basepath is <code>/ws</code>, and the implicit timeout is 10 seconds (set to 0 to disable it).
* This timeout is read from the appserver's timeout.
* <p>
* The values which need to be configured in the directory are:
* <ul>
* <li><code>soap/wsdl</code>, a comma-separated list of WSDL files, relative to the root Java package.</li>
* <li><code>soap/basepath</code>, to specify the basepath. Defaults to <code>/ws</code>.</li>
* <li><code>soap/alias</code>, a process certificate alias associated with the workers which will process
* the SOAP requests; these will delegate the call to the appserver. This context will automatically
* be set as HEADLESS.</li>
* <li><code>soap/keyStore</code>, the key store with the certificate to authenticate this process. If is
* missing, the store will be built from the directory.</li>
* <li><code>soap/storePassword</code>, the store password, only if <code>soap/keyStore</code> is set.</li>
* <li><code>soap/poolSize</code> - the pool size of workers. Defaults to 10.</li>
* <li><code>soap/appserver</code> - the name of the appserver handling the requests.</li>
* </ul>
* <p>
* Each queue of SOAP workers is ordered by the number of pending tasks to be executed - for each incoming
* request, the worker with the smallest number of pending tasks will be chosen.
*/
public class SoapHandler
extends LegacyWebServiceHandler
{
/** Anonymous log instance. */
private static final CentralLogger LOG = CentralLogger.get(SoapHandler.class.getName());
/** The list of instances for this handler. */
private static List<SoapHandler> instances;
/** The context handler. */
private static ContextHandlerCollection handler;
/** Set of WSDL files served by this instance, by their namespace. */
private Map<String, String> wsdlFiles;
/** Map of WSDL configuration by their namespace. */
private Map<String, WsdlConfig> wsdl = new HashMap<>();
/** Map of WSDL DOM documents by their namespace. */
private Map<String, Document> wsdlDocs = new HashMap<>();
/**
* Initialize this handler with the 'soap' type and '/ws' default basepath.
*
* @param basePath
* The basepath served by this handler.
* @param wsdls
* The set of WSDL files.
*/
public SoapHandler(String basePath, Map<String, String> wsdls)
{
super(ServiceType.SOAP, basePath);
this.wsdlFiles = wsdls;
}
/**
* Determine if this web service is configured to authenticate the APIs.
*
* @return <code>true</code> if the authentication is enabled for this web service, in the directory.
*/
public static boolean useWebServiceAuthentication()
{
synchronized (SoapHandler.class)
{
if (instances == null)
{
return false;
}
for (SoapHandler instance : instances)
{
if (instance.useAuthorization())
{
return true;
}
}
return false;
}
}
/**
* Terminate the SOAP worker threads.
*/
public static void terminate()
{
synchronized (SoapHandler.class)
{
if (instances != null)
{
instances.forEach(instance -> instance.terminateWorkers());
}
}
}
/**
* Initialize the SOAP worker(s) and start the workers.
*
* @return The handler for SOAP services.
*/
public static AbstractHandler initialize()
{
synchronized (SoapHandler.class)
{
if (handler != null)
{
return handler;
}
// empty basepath is allowed, but it defaults to '/ws'
String basePath = Utils.getDirectoryNodeString(null, "soap/basepath", "/ws", false);
// collect all mappings between the endpoints and the WSDLs served on that endpoint
Map<String, Map<String, String>> mappings = getWsdlByEndpoints(basePath);
instances = new ArrayList<>();
handler = new ContextHandlerCollection();
for (String endpoint : mappings.keySet())
{
Map<String, String> wsdls = mappings.get(endpoint);
SoapHandler instance = new SoapHandler(endpoint, wsdls);
instance.initializeHandler();
instances.add(instance);
ContextHandler instanceHandler = new ContextHandler();
instanceHandler.setContextPath(instance.getBasePath());
instanceHandler.setAllowNullPathInfo(true);
instanceHandler.setHandler(instance);
handler.addHandler(instanceHandler);
}
return handler;
}
}
/**
* Initialization performed after the {@link #initialize main initialization} was completed.
* <p>
* This will load all the configured WSDL files for each handler, and will set their service location to
* the current host/port handling the SOAP services.
*/
public static void initializePost()
{
// empty basepath is allowed, but it defaults to '/ws'
String basePath = Utils.getDirectoryNodeString(null, "soap/basepath", "/ws", false);
// overrides the web server host:port for the soap:address location in wsdl
String wsdlHost = Utils.getDirectoryNodeString(null, "soap/wsdlHost", null, false);
Integer wsdlPort = Utils.getDirectoryNodeInt(null, "soap/wsdlPort", 443, false);
for (SoapHandler instance : instances)
{
instance.loadWsdls(basePath, wsdlHost, wsdlPort);
}
}
/**
* Register a legacy service associated with a legacy OO class method.
*
* @param ls
* The annotation describing the service, at the legacy class.
* @param cls
* The {@link com.goldencode.p2j.oo.lang.BaseObject} sub-class implementing this
* service.
* @param mls
* The annotation describing the service, at the legacy method.
* @param m
* The target operation.
*/
public static void addService(LegacyService ls,
Class<? extends _BaseObject_> cls,
LegacyService mls,
Method m)
{
synchronized (SoapHandler.class)
{
if (handler == null)
{
LOG.warning("Trying to register SOAP service " + mls.name() + ", but SOAP is not enabled.");
return;
}
}
registerService(ls.namespace(), (instance) -> instance.registerService(ls, cls, mls, m));
}
/**
* Register a legacy service associated with a legacy external program.
*
* @param ls
* The annotation describing the service.
* @param pname
* The legacy program name.
* @param m
* The target Java method for this service.
*/
public static void addService(LegacyService ls, String pname, Method m)
{
synchronized (SoapHandler.class)
{
if (handler == null)
{
LOG.warning("Trying to register SOAP service " + pname + ", but SOAP is not enabled.");
return;
}
}
registerService(ls.namespace(), (instance) -> instance.registerService(ls, pname, m));
}
/**
* Register a legacy service associated with an internal entry for an external program.
*
* @param ls
* The annotation describing the service, at the external program.
* @param pname
* The legacy program name.
* @param iels
* The annotation describing the service, at the internal entry.
* @param ie
* The target internal entry for this service.
* @param function
* Flag identifying if the internal entry is a function.
*/
public static void addService(LegacyService ls,
String pname,
LegacyService iels,
InternalEntry ie,
boolean function)
{
synchronized (SoapHandler.class)
{
if (handler == null)
{
LOG.warning("Trying to register SOAP service " + pname + "." + ie.getLegacyName() +
", but SOAP is not enabled.");
return;
}
}
registerService(ls.namespace(), (instance) -> instance.registerService(ls, pname, iels, ie, function));
}
/**
* Remove all namespaces from the specified element.
* <p>
* This will remove only namespaces which are meant to be for the SOAP envelope, keeping everything else.
*
* @param el
* The element.
*/
@SuppressWarnings("unchecked")
static void removeNamespaces(OMElement el)
{
Iterator<OMNamespace> iter = el.getAllDeclaredNamespaces();
while (iter.hasNext())
{
OMNamespace ns = iter.next();
String nsURI = ns.getNamespaceURI();
if (nsURI.isEmpty() ||
nsURI.startsWith("urn:schemas-progress-com") ||
nsURI.equals("http://www.w3.org/2001/XMLSchema"))
{
// don't remove the built-in namespaces
continue;
}
iter.remove();
}
if (el.getNamespace() != null)
{
OMNamespace ns = el.getNamespace();
String nsURI = ns.getNamespaceURI();
OMNamespace defaultNs = el.getDefaultNamespace();
if (!(nsURI.isEmpty() ||
nsURI.startsWith("urn:schemas-progress-com") ||
nsURI.equals("http://www.w3.org/2001/XMLSchema")) &&
(defaultNs == null || !nsURI.equals(defaultNs.getNamespaceURI())))
{
el.setNamespace(null);
}
}
Iterator<OMElement> iterEl = el.getChildElements();
while (iterEl.hasNext())
{
removeNamespaces(iterEl.next());
}
}
/**
* Get all the WSDLs by their endpoints.
*
* @param basePath
* The base path.
*
* @return A map of endpoints to the WSDLs and their target namespace.
*/
private static Map<String, Map<String, String>> getWsdlByEndpoints(String basePath)
{
Map<String, Map<String, String>> mappings = new HashMap<>();
String wsdlFiles = Utils.getDirectoryNodeString(null, "soap/wsdl", null, false);
if (wsdlFiles == null)
{
throw new IllegalStateException("No WSDL files specified!");
}
String[] fileList = wsdlFiles.split(",");
for (String file : fileList)
{
try
{
Map<String, List<Object[]>> maps = loadWsdl("", basePath, file); // no URL is needed now.
for (String endpoint : maps.keySet())
{
Map<String, String> files = mappings.computeIfAbsent(endpoint, (s) -> new HashMap<>());
WsdlConfig cfg = (WsdlConfig) maps.get(endpoint).get(0)[1];
files.put(cfg.getDefinition().getTargetNamespace(), file);
}
}
catch (Exception e)
{
throw new RuntimeException("Could not load WSDL file " + file, e);
}
}
return mappings;
}
/**
* Register the specified operation with all instance handling that namespace.
*
* @param namespace
* The WSDL namespace.
* @param consumer
* The code used to register the WSDL file.
*/
private static void registerService(String namespace, Consumer<SoapHandler> consumer)
{
boolean found = false;
for (SoapHandler instance : instances)
{
if (instance.wsdlFiles.containsKey(namespace))
{
consumer.accept(instance);
found = true;
}
}
if (!found)
{
LOG.log(Level.INFO, "Could not resolve the handler for WSDL namespace: " +
namespace +
". Has the .wsdl file been excluded?");
}
}
/**
* Find the input stream for a file relative to the project's root Java package.
*
* @param file
* The file to resolve.
*
* @return The stream for this file.
*/
private static InputStream getWsdlStream(String file)
{
String rootPkg = SourceNameMapper.getPackageRoot();
file = rootPkg.replace('.', '/') + "/" + file;
return MultiClassLoader.getClassLoader().getResourceAsStream(file);
}
/**
* Execute the SOAP request.
*
* @param service
* The SOAP service instance which will handle the request.
* @param token
* When not null, it represents the token of a FWD context created for an authenticated and
* authorized web service call. The API call will be performed in that context.
* @param worker
* The SOAP worker.
* @param target
* The target path.
* @param request
* The HTTP servlet request instance.
* @param response
* The HTTP servlet response instance.
* @param soapRequest
* The SOAP body element which encodes the request.
* @param wsdlConfig
* The WSDL configuration to which the request belongs.
* @param soapOperation
* The WSDL operation defining this request.
* @param soapBinding
* The WSDL binding to which this SOAP operation definition belongs.
*/
private static void invoke(RestService service,
String token,
LegacyServiceWorker worker,
String target,
HttpServletRequest request,
HttpServletResponse response,
OMElement soapRequest,
WsdlConfig wsdlConfig,
Operation soapOperation,
BindingOperation soapBinding)
throws IOException
{
OutputStream stream = response.getOutputStream();
response.setContentType("text/xml");
LegacyServiceParameter[] sparams = wsdlConfig.resolveParameters(service.serviceSignature,
soapOperation);
SoapRequestParser rs = new SoapRequestParser(sparams, soapRequest, soapOperation, wsdlConfig);
SoapResponseArguments rw = new SoapResponseArguments(soapOperation, wsdlConfig, worker.isMultiSession());
String reason = "An error was detected while executing the Web Service request. (10893)";
AppServerHelper helper = worker.getHelper();
try
{
try
{
Object[] args = rs.parseArguments(sparams, service.getDefinitionParameters(), request);
Object retVal = service.invoke(helper, token, args);
if (service.isPersistent())
{
handle h = service.getProcedureHandle();
ProxyProcedureWrapper proxy = (ProxyProcedureWrapper) h.getResource();
String resourceID = proxy.getResourceId();
int agentId = proxy.getAgentId();
rw.writeProcedureID(helper.getConnectionID(), resourceID, agentId, soapBinding);
}
rw.writeArguments(retVal, args, sparams, stream, response);
}
catch (RequestArgumentError e)
{
rw.setRequestId(service.getRequestID());
rw.writeError(e, worker.isMultiSession());
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR_500);
rw.flushArguments(stream, response);
}
catch (Throwable exc)
{
rw.setRequestId(service.getRequestID());
Throwable t = exc;
while (t != null)
{
if (t.getCause() != null && t.getCause() instanceof QuitConditionException)
{
throw (QuitConditionException) t.getCause();
}
if (t.getCause() != null && t.getCause() instanceof StopConditionException)
{
throw (StopConditionException) t.getCause();
}
if (t instanceof LegacyErrorException)
{
LegacyErrorException lex = (LegacyErrorException) t;
LegacyError error = lex.getErrorRef();
rw.writeError(error);
rw.flushArguments(stream, response);
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR_500);
return;
}
if (t instanceof ErrorConditionException)
{
rw.writeError(t.getMessage(), reason, true);
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR_500);
rw.flushArguments(stream, response);
return;
}
else if (t instanceof ConditionException)
{
throw (ConditionException) t;
}
t = t.getCause();
}
// other failure?
LOG.log(Level.SEVERE, "Could not execute " + target, exc);
rw.writeError(exc.getMessage(), reason, true);
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR_500);
rw.flushArguments(stream, response);
}
}
catch (QuitConditionException |
StopConditionException e)
{
if (e instanceof QuitConditionException)
{
helper.disconnect(true);
}
rw.setRequestId(service.getRequestID());
rw.writeError("An internal Web Service error occurred in the Web Services Adapter. (10902)",
reason, true);
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR_500);
rw.flushArguments(stream, response);
}
catch (ConditionException e)
{
rw.setRequestId(service.getRequestID());
rw.writeError(e.getMessage(), reason, true);
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR_500);
rw.flushArguments(stream, response);
}
finally
{
// reset the config
service.reset();
rw.reset();
}
}
/**
* From a SOAP request, read the procedure handle ID, as the UUID element from the SOAP header.
*
* @param wsdlConfig
* The WSDL configuration to which the request belongs.
* @param header
* The SOAP header for the request.
* @param binding
* The WSDL binding to which this SOAP operation definition belongs.
*
* @return The procedure handle ID, or <code>null</code> if it can't be found.
*/
private static String readProcedureHandle(WsdlConfig wsdlConfig, SOAPHeader header, BindingOperation binding)
{
BindingInput input = binding.getBindingInput();
Iterator<ExtensibilityElement> iter = input.getExtensibilityElements().iterator();
javax.wsdl.extensions.soap.SOAPHeader inputHeader = null;
while (iter.hasNext())
{
ExtensibilityElement exEl = iter.next();
if (exEl instanceof javax.wsdl.extensions.soap.SOAPHeader)
{
inputHeader = (javax.wsdl.extensions.soap.SOAPHeader) exEl;
break;
}
}
if (inputHeader != null)
{
String inputPart = inputHeader.getPart();
QName inputMsg = inputHeader.getMessage();
Message msg = wsdlConfig.getDefinition().getMessage(inputMsg);
WSDLSchema schema = wsdlConfig.resolveSchema(msg, inputPart);
OMElement partEl = header.getFirstChildWithName(new QName(schema.getTargetNamespace(), inputPart));
OMElement uuidEl = partEl.getFirstChildWithName(new QName(null, "UUID"));
return uuidEl == null ? null : uuidEl.getText();
}
return null;
}
/**
* Code to process web requests.
*/
@Override
public void handle(String target,
Request baseRequest,
HttpServletRequest request,
HttpServletResponse response)
throws IOException
{
if (baseRequest.isHandled())
{
return;
}
if (target.endsWith("/wsdl") || target.endsWith("/wsdl/"))
{
String targetURI = request.getParameter("targetURI");
Document doc = targetURI == null ? null : wsdlDocs.get(targetURI);
if (doc != null)
{
// dump the WSDL file
ByteArrayOutputStream baos = new ByteArrayOutputStream();
XmlHelper.write(doc, baos);
response.getOutputStream().write(baos.toByteArray());
}
else
{
response.setStatus(HttpStatus.NOT_FOUND_404);
}
baseRequest.setHandled(true);
return;
}
// allow the WSDL URI; protect only the actual SOAP calls.
// first check the authentication
super.handle(target, baseRequest, request, response);
if (baseRequest.isHandled())
{
return;
}
baseRequest.setHandled(true);
SOAPEnvelope env = null;
try
{
byte[] body = readBody(request);
String encoding = request.getCharacterEncoding();
if (encoding == null)
{
encoding = "UTF-8";
}
Reader reader = new StringReader(new String(body, encoding));
SOAPModelBuilder builder = OMXMLBuilderFactory.createSOAPModelBuilder(reader);
env = builder.getSOAPEnvelope();
}
catch (Throwable t)
{
writeError("Error in SOAP: " + t.getMessage(),
"An error was detected in the Web Service request. (10894)",
response);
return;
}
OMElement elRequest = null;
try
{
elRequest = env.getBody().getFirstElement();
}
catch (Throwable t)
{
writeError("Error in SOAP body: " + t.getMessage(),
"An error was detected in the Web Service request. (10894)",
response);
return;
}
if (elRequest == null)
{
writeError("Error in SOAP body: 0 >= 0 (10915)",
"An error was detected in the Web Service request. (10894)",
response);
return;
}
// if there are more than one SOAP operations in the BODY, only the first one is executed; other
// elements are simply ignored
OMNamespace operationNs = elRequest.getNamespace();
String operationNsUri = operationNs.getNamespaceURI();
// TODO: is the service name always present in customer code?
if (operationNsUri.indexOf(':') == operationNsUri.lastIndexOf(':'))
{
String errorMessage = "No service binding found in operation namespace Uri " + operationNsUri;
LOG.warning(errorMessage);
writeError("Error in SOAP body: " + errorMessage,
"An error was detected in the Web Service request. (10894)",
response);
}
String serviceName = operationNsUri.substring(operationNsUri.lastIndexOf(':') + 1);
String operationName = elRequest.getLocalName();
// find the WSDL based on the URN
WsdlConfig wsdlConfig = wsdl.get(operationNsUri);
Operation foundOperation = null;
BindingOperation operationBinding = null;
if (wsdlConfig != null)
{
Definition def = wsdlConfig.getDefinition();
Map<QName, PortType> portTypes = def.getPortTypes();
for (QName portQName : portTypes.keySet())
{
PortType port = def.getPortType(portQName);
Operation portOp = port.getOperation(operationName, null, null);
if (portOp != null)
{
// check if this is from the same namespace as the SOAP request
if (namespaceMatch(operationNsUri, portOp.getInput()) &&
namespaceMatch(operationNsUri, portOp.getOutput()) &&
namespaceMatch(operationNsUri, portOp.getFault("FaultDetailMessage")))
{
Binding wsdlBinding = def.getBinding(portQName);
operationBinding = wsdlBinding.getBindingOperation(operationName, null, null);
foundOperation = portOp;
break;
}
}
}
}
if (foundOperation == null)
{
writeError("Error in SOAP method: Not found. (10916)",
"An error was detected in the Web Service request. (10894)",
response);
return;
}
// build a key
String namespace = wsdlConfig.getDefinition().getTargetNamespace();
SoapServiceKey key = new SoapServiceKey(namespace, serviceName, operationName);
RestService service = services.get(key);
if (service == null)
{
writeError("Error in SOAP method: Not found. (10916)",
"An error was detected in the Web Service request. (10894)",
response);
return;
}
OMElement elReq = elRequest;
Operation soapOper = foundOperation;
SOAPHeader envHeader = env.getHeader();
BindingOperation soapBind = operationBinding;
WsdlConfig wsdlCfg = wsdlConfig;
String[] targetConnection = new String[1];
if (service instanceof IESoapService || (DELETE.equals(service.getExecutionMode())))
{
String uuid = readProcedureHandle(wsdlConfig, env.getHeader(), operationBinding);
if (uuid.indexOf('#') > 0)
{
targetConnection[0] = uuid.substring(0, uuid.lastIndexOf('#'));
targetConnection[0] = targetConnection[0].substring(0, targetConnection[0].lastIndexOf('#'));
}
}
LegacyAppServerWork work = (worker) ->
{
// now do the authorization - must be in the worker thread
String token;
if (useAuthorization())
{
token = authorize(target, request, response);
if (token == null)
{
return;
}
}
else
{
token = null;
}
try
{
// this must be performed on the worker thread, before the invoke
if (service instanceof IESoapService || (DELETE.equals(service.getExecutionMode())))
{
String uuid = readProcedureHandle(wsdlConfig, envHeader, soapBind);
if (uuid.indexOf('#') > 0)
{
// format is: connectionID#resourceID#agentID
String resourceId = null;
String agentId = null;
String connectionID = null;
agentId = uuid.substring(uuid.lastIndexOf('#') + 1);
connectionID = uuid.substring(0, uuid.lastIndexOf('#'));
resourceId = connectionID.substring(connectionID.lastIndexOf('#') + 1);
connectionID = connectionID.substring(0, connectionID.lastIndexOf('#'));
((ProgRestService) service).setResourceId(resourceId, Integer.parseInt(agentId));
}
}
invoke(service, token, worker, target, request, response, elReq, wsdlCfg, soapOper, soapBind);
}
catch (IOException e)
{
throw new RuntimeException(e);
}
finally
{
postHandle(token);
}
};
handle(this, targetConnection[0], work, target, response, request);
}
/**
* Register a legacy service associated with an internal entry for an external program.
*
* @param ls
* The annotation describing the service, at the external program.
* @param pname
* The legacy program name.
* @param iels
* The annotation describing the service, at the internal entry.
* @param ie
* The internal entry name.
* @param function
* Flag identifying if the internal entry is a function.
*
* @return The registered service
*/
protected RestService registerService(LegacyService ls,
String pname,
LegacyService iels,
InternalEntry ie,
boolean function)
{
ServiceKey key = buildServiceKey(iels);
if (services.containsKey(key))
{
throw new RuntimeException("More than one " + getType().toUpperCase() + " service mapped to " + key);
}
RestService srv = new IESoapService(ls, pname, iels, ie, function, key, getTimeout());
services.put(srv.key, srv);
return srv;
}
/**
* Build a {@link SoapServiceKey} for the specified legacy service.
*
* @param ls
* The legacy service annotation.
*
* @return The service key.
*/
@Override
protected ServiceKey buildServiceKey(LegacyService ls)
{
return new SoapServiceKey(ls.namespace(), ls.binding(), ls.name());
}
/**
* Resolve the configured basepath for this handler.
*
* @param basePath
* The default base path.
*
* @return The unchanged <code>basePath</code>.
*/
@Override
protected String resolveBasePath(String basePath)
{
// already resolved during initialization
return basePath;
}
/**
* Serialize the error, to be included in the response.
*
* @param msg
* The message to be used as the SOAP fault code ('faultString').
* @param reason
* The message to be used as the SOAP fault detail.
* @param response
* The servlet response.
*/
private void writeError(String msg, String reason, HttpServletResponse response)
throws IOException
{
SoapResponseArguments resp = new SoapResponseArguments(null, null, false);
resp.writeError(msg, reason);
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR_500);
resp.flushArguments(response.getOutputStream(), response);
}
/**
* Check if the specified element is part of this namespace.
*
* @param ns
* The namespace to match.
* @param el
* The WSDL message (input, output or fault).
*
* @return <code>true</code> if the element is part of the namespace.
*/
private boolean namespaceMatch(String ns, WSDLElement el)
{
if (el == null)
{
return true;
}
Message msg = null;
if (el instanceof Input)
{
msg = ((Input) el).getMessage();
}
else if (el instanceof Output)
{
msg = ((Output) el).getMessage();
}
else if (el instanceof Fault)
{
msg = ((Output) el).getMessage();
}
else
{
return false;
}
if (msg == null)
{
return true;
}
Part part = msg.getPart(el instanceof Fault ? "FaultDetail" : "parameters");
return part.getElementName().getNamespaceURI().equals(ns);
}
/**
* Load all the configured <code>soap/wsdl</code> files and will set their service location to the current
* host/port handling the SOAP services.
*
* @param basePath
* The basePath to append to the WSDL endpoint path.
* @param wsdlHost
* The host used for the soap:address location in wsdl.
* @param wsdlPort
* The port used for the soap:address location in wsdl. Defaults to 443, when not specified.
*/
private void loadWsdls(String basePath, String wsdlHost, int wsdlPort)
{
String url = wsdlHost != null ?
String.format("https://%s:%d", wsdlHost, wsdlPort) :
WebServer.getURL();
for (String file : wsdlFiles.values())
{
try
{
Map<String, List<Object[]>> maps = loadWsdl(url, basePath, file);
for (String endpoint : maps.keySet())
{
List<Object[]> wsdls = maps.get(endpoint);
for (Object[] cfg : wsdls)
{
Document dom = (Document) cfg[0];
WsdlConfig wsdlCfg = (WsdlConfig) cfg[1];
for (Object prefix : wsdlCfg.getDefinition().getNamespaces().keySet())
{
Definition wsdlDef = wsdlCfg.getDefinition();
String targetNS = wsdlDef.getTargetNamespace();
String ns = wsdlDef.getNamespace((String) prefix);
if (!ns.startsWith(targetNS))
{
continue;
}
if (wsdl.containsKey(ns))
{
throw new IllegalStateException("More than one WSDL registered for namespace: " + ns);
}
wsdl.put(ns, wsdlCfg);
}
String targetNs = wsdlCfg.getDefinition().getTargetNamespace();
wsdlDocs.put(targetNs, dom);
}
}
}
catch (Exception e)
{
throw new RuntimeException("Could not load WSDL file " + file, e);
}
}
}
/**
* Load the WSDL document from the specified file. This will also set the service URL address.
*
* @param basePath
* The basePath to append to the WSDL endpoint path.
* @param file
* The file name, relative to project's root Java package.
*
* @return The mappings between the endpoint and the WSDL document handled by that endpoint.
*
* @throws ParserConfigurationException
* if the XML parser has been configured incorrectly.
* @throws IOException
* if an I/O error occurs during parsing or stream cleanup.
* @throws SAXException
* if an error occurs parsing the XML document.
* @throws WSDLException
* if an error occurs parsing the WSDL document.
*/
private static Map<String, List<Object[]>> loadWsdl(String url, String basePath, String file)
throws ParserConfigurationException,
IOException,
SAXException,
WSDLException
{
Document dom = XmlHelper.parse(getWsdlStream(file), false, null, true);
while (url.endsWith("/"))
{
url = url.substring(0, url.length() - 1);
}
if (!basePath.startsWith("/"))
{
basePath = "/" + basePath;
}
while (basePath.endsWith("/"))
{
basePath = basePath.substring(0, basePath.length() - 1);
}
String location = url + basePath;
Set<String> endpoints = new TreeSet<>();
// get the /definitions/port/address/#location and update it
NodeList nl = dom.getElementsByTagName("wsdl:service");
for (int i = 0; i < nl.getLength(); i++)
{
Element n = (Element) nl.item(i);
NodeList nl2 = n.getElementsByTagName("wsdl:port");
for (int j = 0; j < nl2.getLength(); j++)
{
Element n2 = (Element) nl2.item(j);
NodeList nl3 = n2.getElementsByTagName("soap:address");
for (int k = 0; k < nl3.getLength(); k++)
{
Element elAddress = (Element) nl3.item(k);
String path = elAddress.getAttribute("location");
if (!path.startsWith("/"))
{
path = "/" + path;
}
endpoints.add(basePath + path);
elAddress.setAttribute("location", location + path);
}
}
}
WSDLReader reader = WSDLFactory.newInstance().newWSDLReader();
Definition wsdlDef = reader.readWSDL(null, dom);
Map<String, List<Object[]>> mappings = new HashMap<>();
for (String endpoint : endpoints)
{
List<Object[]> wsdls = mappings.computeIfAbsent(endpoint, (s) -> new ArrayList<>());
wsdls.add(new Object[] { dom, new WsdlConfig(wsdlDef) });
}
return mappings;
}
}