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 &gt;= 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;
   }
}