# WebServiceImpl.java

    /*
    ** Module   : WebServiceImpl.java
    ** Abstract : Implementation of APIs specific to a client-side access to Web Services.
    **
    ** Copyright (c) 2016-2024, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- ---------------------------------Description---------------------------------
    ** 001 OM  20160323 Created initial version.
    ** 002 EVL 20180207 Fix for invalid char in comment issue.
    ** 003 OM  20180528 Replace backslash characters with slash to make the URL rfc1738 compliant.
    ** 004 SBI 20181224 Fixed validateParameters.
    ** 005 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
    ** 006 HC  20230517 Implemented server-side web service calling.
    ** 007 OM  20240510 Used constants instead of hardcoded literals.
    ** 008 TJD 20240208 Java 17 dependencies updates
    ** 009 GBB 20240610 Namespace for request body to be resolved from schema instead of wsdl definition.
    **                  SOAP fault to be handled server-side.
    ** 010 CA  20240620 The context-local 'nohostverify' must be set and unset only when the SOAP request is executing,
    **                  when in SSL mode.
    ** 011 GBB 20240826 Moving to osresource package. Rename 'single' to 'local'.
    */

    /*
    ** 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.util.osresource;

    import com.goldencode.p2j.net.*;
    import com.goldencode.p2j.security.*;
    import com.goldencode.p2j.util.*;
    import com.goldencode.p2j.util.ErrorManager;
    import com.goldencode.p2j.util.logging.*;
    import org.apache.axiom.om.*;
    import org.apache.axiom.om.util.*;
    import org.apache.axiom.soap.*;
    import org.apache.axiom.soap.SOAPHeader;
    import org.apache.axis2.*;
    import org.apache.axis2.addressing.*;
    import org.apache.axis2.client.*;
    import org.apache.axis2.context.*;
    import org.apache.axis2.kernel.http.*;
    import org.apache.axis2.transport.http.impl.httpclient4.*;
    import org.apache.axis2.wsdl.*;
    import org.apache.ws.commons.schema.*;
    import javax.wsdl.*;
    import javax.wsdl.Binding;
    import javax.wsdl.Message;
    import javax.wsdl.extensions.*;
    import javax.wsdl.extensions.schema.*;
    import javax.wsdl.extensions.soap.*;
    import javax.wsdl.extensions.soap12.*;
    import javax.wsdl.factory.*;
    import javax.wsdl.xml.*;
    import javax.xml.namespace.*;
    import javax.xml.stream.*;
    import java.util.*;
    import java.util.logging.*;

    /**
     * Client-side implementation of WebService. P2J delegates all call here so that the 'remote'
     * procedure is called form P2J client-side and benefit from true client network environment
     * (firewall setting, host of request from POV of the machine that hosts the WebService.)
     */
    class WebServiceImpl
    implements WebService,
               SessionListener
    {
       /** Logger */
       private static final CentralLogger LOG = CentralLogger.get(WebServiceImpl.class.getName());

       /** The context-local data of this class. */
       private static ContextLocal<WorkArea> local =
          new ContextLocal<WorkArea>()
          {
             protected WorkArea initialValue()
             {
                return new WorkArea();
             }
          };

       /** Token used to authenticate with the dispatcher when registering APIs. */
       private static Object modToken = null;

       /** The single instance available of this class. */
       private static final ContextLocal<WebServiceImpl> instance = new ContextLocal<>();

       /** A SOAP factory to create message data. */
       private static final org.apache.axiom.soap.SOAPFactory soapFactory =
             OMAbstractFactory.getSOAP11Factory();

       /**
        * Private c'tor, to not allow explicit usage of this class. Its intended goal is to be
        * exported as a network server.
        *
        * @param    local
        *           <code>true</code> to register purely on a local basis (no remote/network access to these
        *           methods).
        */
       private WebServiceImpl(boolean local)
       {
          synchronized (WebServiceImpl.class)
          {
             modToken = RemoteObject.registerServer(WebService.class, this, modToken, local);
          }
       }

       /**
        * Retrieve the target instance.
        *
        * @param    local
        *           <code>true</code> to register purely on a local basis (no remote/network access to these
        *           methods).
        */
       static synchronized WebServiceImpl getInstance(boolean local)
       {
          WebServiceImpl ws = instance.get();

          if (ws == null)
          {
             ws = new WebServiceImpl(local);
             SessionManager.get().getSession().addSessionListener(ws);
             instance.set(ws);
          }

          return ws;
       }

       /**
        * This callback will be called after the security context has been set up.
        *
        * @param   session
        *          The session that is starting.
        */
       @Override
       public void initialize(Session session)
       {
          // no-ops
       }

       /**
        * This method is called when the session is ending. We close and cleanup all resources
        * allocated in the current context.
        *
        * @param   session
        *          The session that is ending.
        */
       @Override
       public void terminate(Session session)
       {
          WorkArea wa = local.get();
          synchronized (wa)
          {
             Map<Integer, WebServiceData> copy = new HashMap<>(wa.webServices);
             for (WebServiceData webService : copy.values())
             {
                disconnect(webService.getId());
             }
          }
       }

       /**
        * Validate parameters and establish a new connection using the given options.
        * <p>
        * The Web Services are not real connection and no actual connection remains open. Instead, the
        * following actions are taken:
        * <ol>
        *    <li>use network protocols to acquire the requested WSDL document
        *    <li>parses the document and verifies the passed in parameters against the configurations
        *          from WSDl document
        *    <li>in case of errors {@link ErrorManager} is used to report them
        *    <li>a new {@link WebServiceData} structure is created and populated with validated values
        *          and stored in context local data structures. At this moment object is populated
        *          only with <i>final</i> information needed for further service invocation. The
        *          object is assigned to a unique identifier that will be used from this point forward
        *          for accessing this connection. This {@code id} is returned in case of success.
        * </ol>
        * The returned id will be used in further references to this connection until the connection
        * is disconnected.
        * <p>
        * The {@code #CONNECT_OPTION_MAX_CONNECTIONS}, {@code #CONNECT_OPTION_CONNECTION_LIFETIME},
        * {@code #CONNECT_OPTION_NO_SESSION_REUSE} and {@code #CONNECT_OPTION_NO_HOST_VERIFY} options
        * are not supported.
        *
        * @param   options
        *          A collection of all parameters used for this connection.
        *
        * @return  A positive (possible 0) integer representing the id of this connection to
        *          web-service server and a negative value representing an error code if any error
        *          occurred during validation and initialization of this connection. In the latter
        *          case the error was already reported. Check the {@link ErrorManager} for details
        *          about any errors.
        */
       @Override
       @SuppressWarnings("unchecked")
       public int connect(WebServiceConnectOptions options)
       {
          // step 1. check if the WSDL document is available and can be parsed properly. Use the
          //         specified [WSDLUserid [WSDLPassword]]
          Definition wsdlDef = null;
          String url = options.getWsdlDocument().replace('\\', '/');
          try
          {
             // TODO: use the WSDL userId and password
             WSDLReader reader = WSDLFactory.newInstance().newWSDLReader();

             // replace backslash characters with slash to make the URL rfc1738 compliant
             wsdlDef = reader.readWSDL(url);
          }
          catch (WSDLException e)
          {
             String msg = String.format("Error loading WSDL document  %s :  %s",
                                        url,
                                        e.getMessage());
             ErrorManager.recordOrShowError(11748, msg, false, false, false);

             if (LOG.isLoggable(Level.SEVERE))
             {
                LOG.log(Level.SEVERE, msg, e);
             }
             return ERR_LOADING_WSDL_DOCUMENT;
          }

          // step2a. check if both service and binding are specified
          if ((options.getServiceNamespace() != null || options.getPortName() != null) &&
              (options.getBindingName() != null || options.getSoapEndpoint() != null))
          {
             final int[] errCodes;
             final String[] errMsgs;

             final String msg1 =
                   "If -Service and/or -Port are specified in the SERVER:CONNECT " +
                   "connection-parameters string, neither -Binding nor -SOAPEndpoint is valid";
             final String msg2 =
                   "If '-Binding' is specified in the SERVER:CONNECT connection-parameters string, " +
                   "'-SOAPEndPoint' must also be specified";

             if (options.getBindingName() != null && options.getSoapEndpoint() == null)
             {
                errCodes = new int[] { 11499, 11501 };
                errMsgs = new String[] { msg1, msg2 };
             }
             else
             {
                errCodes = new int[] { 11499 };
                errMsgs = new String[] { msg1 };
             }

             ErrorManager.recordOrShowError(errCodes, errMsgs, false, false, false);
             return ERR_INVALID_PARAMS_IN_CONNECTION_STRING;
          }

          // step 2b. if -Binding is specified, -SoapEndpoint must be specified
          if (options.getBindingName() != null && options.getSoapEndpoint() == null)
          {
             final String msg =
                   "If '-Binding' is specified in the SERVER:CONNECT connection-parameters string, " +
                   "'-SOAPEndPoint' must also be specified";
             ErrorManager.recordOrShowError(11501, msg, false, false, false);
             return ERR_INVALID_PARAMS_IN_CONNECTION_STRING;
          }

          // TODO: validate -ServiceNamespace and -BindingNamespace
          String ns = wsdlDef.getTargetNamespace();
          PortType portType   = null;
          Service service     = null;

          // step 3. only service or binding must be specified
          if (options.getServiceName() != null)
          {
             // step 3a. check if specified [Service [ServiceNamespace]] [Port] are valid
             service = wsdlDef.getService(new QName(ns, options.getServiceName()));

             if (service == null)
             {
                String msg = String.format("The Service name %s is not in the WSDL document",
                                           options.getServiceName());
                ErrorManager.recordOrShowError(11736, msg, false, false, false);
                return ERR_SERVICE_NOT_IN_WSDL_DOCUMENT;
             }

             final Port servicePort;
             if (options.getPortName() != null)
             {
                servicePort = service.getPort(options.getPortName());
             }
             else
             {
                servicePort = getImplicitSoapPort(service.getPorts().values());

                if (servicePort == null)
                {
                   noServicesError();
                   return ERR_NO_SERVICE_ERROR;
                }
             }

             // validate the port
             Binding binding = servicePort == null ? null : servicePort.getBinding();
             if (servicePort == null ||
                 binding == null     ||
                 getExtension(binding.getExtensibilityElements(), SOAPBinding.class) == null)
             {
                String msg = String.format("The Port name %s is not in the WSDL document",
                                           options.getPortName());
                ErrorManager.recordOrShowError(11742, msg, false, false, false);
                return ERR_PORT_NOT_IN_WSDL_DOCUMENT;
             }

             portType = binding.getPortType();

             SOAPAddress soapAddress = getExtension(servicePort.getExtensibilityElements(),
                                                    SOAPAddress.class);
             options.setSoapEndpoint((soapAddress == null) ? null : soapAddress.getLocationURI());
             options.setSoapEndpointPassword(null);
             options.setSoapEndpointUserid(null);
          }
          else if (options.getBindingName() != null)
          {
             // step 3b. check if specified [[Binding [BindingNamespace]] [SOAPEndpoint]] are valid
             Binding binding = wsdlDef.getBinding(new QName(ns, options.getBindingName()));
             if (binding != null &&
                 getExtension(binding.getExtensibilityElements(), SOAP12Binding.class) != null)
             {
                String msg =  String.format(
                   "The %s:%s Binding is not supported, it must be a SOAP 1.1 binding",
                   ns, options.getBindingName());
                ErrorManager.recordOrShowError(12100, msg, false, false, false);
                return ERR_BINDING_NOT_SUPPORTED;
             }

             if (binding == null ||
                 getExtension(binding.getExtensibilityElements(), SOAPBinding.class) == null)
             {
                String msg =  String.format(
                      "The %s:%s Binding name cannot be found in the WSDL document",
                      ns, binding);
                ErrorManager.recordOrShowError(11740, msg, false, false, false);
                return ERR_BINDING_NOT_IN_WSDL_DOCUMENT;
             }

             // compute the port type
             portType = binding.getPortType();

             // determine the target service, based on the binding and associated port type
             for (Service srv : (Iterable<Service>) wsdlDef.getAllServices().values())
             {
                for (Port p : (Iterable<Port>) srv.getPorts().values())
                {
                   if (p.getBinding().equals(binding))
                   {
                      service = srv;
                      break;
                   }
                }
             }
             if (service == null)
             {
                String msg =  String.format(
                      "Internal error locating Service and Port for Binding %s:%s",
                      ns, binding);
                ErrorManager.recordOrShowError(11739, msg, false, false, false, true);
                return ERR_SERVICE_AND_PORT_NOT_FOUND;
             }

             // TODO: check if [soapEndpoint] endpoint is a valid URL
          }
          else
          {
             // no explicit binding or service, get the default one.
             Map services = wsdlDef.getServices();
             if (services.size() != 1)
             {
                if (services.isEmpty())
                {
                   noServicesError();
                   return ERR_NO_SERVICE_ERROR;
                }
                else
                {
                   String msg = "A specific Service name must be specified from the WSDL document";
                   ErrorManager.recordOrShowError(11737, msg, false, false, false);
                   return ERR_NO_SERVICE_SPECIFIED;
                }
             }

             service = (Service) services.values().iterator().next();
             Port servicePort = getImplicitSoapPort(service.getPorts().values());
             if (servicePort == null)
             {
                noServicesError();
                return ERR_NO_SERVICE_ERROR;
             }

             Binding binding = servicePort.getBinding();
             portType = binding.getPortType();

             SOAPAddress soapAddress = getExtension(binding.getExtensibilityElements(),
                                                    SOAPAddress.class);
             options.setSoapEndpoint((soapAddress == null ? null : soapAddress.getLocationURI()));
             options.setSoapEndpointPassword(null);
             options.setSoapEndpointUserid(null);
          }

          // step 4. check if the targetNamespace matches the WSDL document's targetNamespace
          if (options.getTargetNamespace() != null &&
              !options.getTargetNamespace().equals(wsdlDef.getTargetNamespace()))
          {
             String msg =  String.format(
                   "The TargetNamespace option %s does not match the WSDL's target namespace",
                   options.getTargetNamespace());
             ErrorManager.recordOrShowError(11750, msg, false, false, false, true);
             return  ERR_TARGETNAMESPACE_NOT_MATCH_WDSL;
          }

          // if everything is fine generate an id for this web service, register and return it
          int id = Utils.uniqueId();

          WebServiceData newWsd = new WebServiceData(id, wsdlDef, portType, service, options);

          WorkArea wa = local.get();
          synchronized (wa)
          {
             wa.addWebService(id, newWsd);
          }

          return id;
       }

       /**
        * Get the name of the PortType of the specified connection.
        *
        * @param   id
        *          The id of the connection.
        *
        * @return  the name of the PortType of specified connection or empty string if there is no
        *          connection with requested id.
        */
       public String getPortTypeName(int id)
       {
          WorkArea wa = local.get();
          synchronized (wa)
          {
             WebServiceData serviceData = wa.locate(id);
             if (serviceData != null)
             {
                return serviceData.portType.getQName().getLocalPart();
             }
          }
          return "";
       }

       /**
        * Disconnect the web service. If a web service invocation is in progress the internal state
        * if dropped first.
        *
        * @param   id
        *          The ID of the associated web service on P2J Client side.
        *
        * @return  {@code true} if no errors were encountered during disconnect.
        */
       @Override
       public boolean disconnect(int id)
       {
          WorkArea wa = local.get();
          synchronized (wa)
          {
             WebServiceData serviceData = wa.locate(id);
             if (serviceData == null)
             {
                return false;
             }
             serviceData.cleanupInvocation();
             wa.removeWebService(id);
          }

          return true;
       }

       /**
        * Prepare the connection for an invocation of an operation on the Web Service. The parameters
        * are validated and, on success, the internal state of the call is temporarily saved.
        *
        * @param   id
        *          The id of an open connection.
        * @param   portTypeName
        *          The name of the port type to be invoked.
        * @param   operationName
        *          The name of the operation to be invoked on the Web Service.
        * @param   modes
        *          A string representation of the modes of each parameter. May not be {@code null}.
        * @param   args
        *          The operation's arguments.
        *
        * @return  {@code true} if validation was successful and {@code false} if some errors occurred
        *          and invocation is not available (incorrect operation name, invalid parameters or
        *          could not build the request message).
        */
       @SuppressWarnings("unchecked")
       public boolean prepareInvoke(int       id,
                                    String    portTypeName,
                                    String    operationName,
                                    String    modes,
                                    Object... args)
       {
          WebServiceData wsd = null;
          WorkArea wa = local.get();
          synchronized (wa)
          {
             wsd = wa.locate(id);
             if (wsd == null)
             {
                return false;
             }
          }

          String ns = wsd.wsdlDef.getTargetNamespace();
          PortType portType = wsd.wsdlDef.getPortType(new QName(ns, portTypeName));
          Operation targetOp = null;

          // validate the operation name
          for (Operation op : (Iterable<Operation>) portType.getOperations())
          {
             // search is done case-insensitive
             if (op.getName().equalsIgnoreCase(operationName))
             {
                targetOp = op;
                break;
             }
          }
          if (targetOp == null)
          {
             final String msg = "Failure initializing SOAP Call: " +
                                "Cannot find a SOAP Operation named %s (11775)";
             ErrorManager.recordOrThrowError(11762, String.format(msg, operationName), false, true);
             return false;
          }

          Object[] inputParams = collectParameters(modes, args, true);
          Object[] outputParams = collectParameters(modes, args, false);

          QName[][] inMsgParams = getMessageParameters(targetOp.getInput().getMessage(), wsd);
          QName[][] outMsgParams = getMessageParameters(targetOp.getOutput().getMessage(), wsd);

          if (!validateParameters(targetOp,
                                  wsd.service,
                                  inputParams,
                                  outputParams,
                                  inMsgParams.length,
                                  outMsgParams.length))
          {
             return false;
          }

          // prepare the message to be sent as request to ws provider
          OMElement reqMsg = buildInputMessage(wsd, targetOp, inMsgParams, inputParams);

          if (reqMsg == null)
          {
             // if request message could not be created, abort
             return false;
          }

          wsd.setRequestMessage(reqMsg);
          wsd.setOutputParameters(outputParams, outMsgParams);
          wsd.setTargetOperation(targetOp);
          return true;
       }

       /**
        * Get the wsdl namespace of this connection.
        *
        * @param   id
        *          The id of an open connection.
        *
        * @return  the wsdl namespace of this connection or {@code null} if {@code id} does not
        *          represent the identifier of a valid open connection.
        */
       public String getNamespace(int id)
       {
          WorkArea wa = local.get();
          synchronized (wa)
          {
             WebServiceData serviceData = wa.locate(id);
             if (serviceData != null)
             {
                return serviceData.wsdlDef.getTargetNamespace();
             }
          }
          return null;
       }

       /**
        * Get the header entries of the response to last invoked operation on the specified connection.
        *
        * @param   id
        *          The id of an open connection.
        *
        * @return  the header entries of the specified connection or {@code null} if the {@code id} is
        *          not the identifier of an open and prepared connection or the last response envelope
        *          does not contain any headers.
        */
       public String[] getHeaderEntries(int id)
       {
          WebServiceData wsd = null;
          WorkArea wa = local.get();
          synchronized (wa)
          {
             wsd = wa.locate(id);
             if (wsd == null || wsd.getServiceClient() == null)
             {
                return null;
             }
          }

          MessageContext resp = null;
          try
          {
             resp = wsd.getServiceClient().getLastOperationContext()
                   .getMessageContext(WSDLConstants.MESSAGE_LABEL_IN_VALUE);
          }
          catch (AxisFault axisFault)
          {
             LOG.severe("", axisFault);
             return null;
          }

          SOAPHeader soapHeader = resp.getEnvelope().getHeader();
          if (soapHeader == null)
          {
             return null;
          }

          // collect the header entries
          ArrayList<String> resHeaders = new ArrayList<>();
          Iterator hiter = soapHeader.getHeadersToProcess(null);
          while (hiter.hasNext())
          {
             SOAPHeaderBlock respHeaderEntry = (SOAPHeaderBlock) hiter.next();
             resHeaders.add(respHeaderEntry.toString());
          }

          String[] res = new String[resHeaders.size()];
          return resHeaders.toArray(res);
       }

       /**
        * Clears the temporary state of an invocation. Does not close open connections. It is called
        * when the result of the invocation was obtained on server side or when an error occurred. If
        * {@code id} is invalid nothing happens.
        *
        * @param   id
        *          The id of an open connection.
        */
       public void cleanupInvocation(int id)
       {
          WorkArea wa = local.get();
          synchronized (wa)
          {
             WebServiceData wsd = wa.locate(id);
             if (wsd != null)
             {
                wsd.cleanupInvocation();
             }
          }
       }

       /**
        * Do the actual Web Service invocation. The call uses an already open and prepared connection
        * identified by {@code id}.
        *
        * @param   id
        *          The id of an open connection.
        * @param   operationName
        *          The name of the operation to be invoked on the Web Service.
        * @param   portTypeName
        *          The name of the port type to be invoked.
        * @param   requestHeader
        *          The request header if any was obtained form a call to a configured header callback.
        *
        * @return  {@code true} if the service operation was successful and {@code false} if
        *          {@code id} does not represent a valid connection or other error occurred.
        */
       @SuppressWarnings("unchecked")
       public boolean invoke(int    id,
                             String operationName,
                             String portTypeName,
                             String requestHeader)
       {
          WebServiceData wsd = null;
          WorkArea wa = local.get();
          synchronized (wa)
          {
             wsd = wa.locate(id);
             if (wsd == null || wsd.getTargetOperation() == null)
             {
                return false;
             }
          }

          String ns          = wsd.wsdlDef.getTargetNamespace();
          String serviceName = wsd.service.getQName().getLocalPart();
          Operation targetOp = wsd.getTargetOperation();

          // invoke the remote operation
          try
          {
             String portName = getPortName(portTypeName, wsd.wsdlDef);
             ServiceClient sc = new ServiceClient(
                   null, wsd.wsdlDef, new QName(ns, serviceName), portName);
             // compatibility issue, avoids "Transport error: 411 Error: Length Required" error
             sc.getOptions().setProperty(HTTPConstants.CHUNKED, false);
             sc.getOptions().setProperty(HTTPConstants.REUSE_HTTP_CLIENT, wsd.isSessionReuse());
             wsd.setServiceClient(sc);
             OMElement requestMessage = wsd.getRequestMessage();

             // Industry standards supported by OpenEdge according to web services manual:
             //
             // - WSDL 1.1 is the de facto standard according to the W3C.
             // - SOAP 1.1 HTTP Binding is the de facto standard according to the W3C.
             // - XML Schema W3C 2001 XML Schema Recommendation.
             //
             // By default axis2 use SOAP 1.2 for binding. For compatibility when port name
             // is automatically choose from the list of available ports by axis2 client we
             // force also the use of SOAP 1.1 binding as default.
             if (portName == null)
             {
                sc.getOptions().setSoapVersionURI(SOAP11Constants.SOAP_ENVELOPE_NAMESPACE_URI);
             }

             if (requestHeader != null)
             {
                // add the request header to the message
                try
                {
                   sc.addHeader(AXIOMUtil.stringToOM(requestHeader));
                }
                catch (XMLStreamException e)
                {
                   if (LOG.isLoggable(Level.SEVERE))
                   {
                      final String msg = "Could not add the SOAP Request header: %s";
                      LOG.log(Level.SEVERE, String.format(msg, requestHeader), e);
                   }
                }
             }

             // TODO: how to terminate a running operation?
             //       maybe invoke it async? (note that at this time, if asyncReq is not null, we
             //       are already in a different thread, thus we still need to act as we are in
             //       blocking mode.
             if (wsd.getSoapEndpoint() != null)
             {
                sc.getOptions().setTo(new EndpointReference(wsd.getSoapEndpoint()));
             }
             if (wsd.getSoapEndpointUserid() != null)
             {
                HttpTransportPropertiesImpl.Authenticator auth =
                      new HttpTransportPropertiesImpl.Authenticator();
                auth.setUsername(wsd.getSoapEndpointUserid());
                auth.setPassword(wsd.getSoapEndpointPassword());

                sc.getOptions().setProperty(HTTPConstants.AUTHENTICATE, auth);
             }

             LowLevelSocketImpl.setNoHostVerify(wsd.isNoHostVerify());

             String schemaTargetNs = null;
             for (XmlSchema schema : wsd.getXmlSchemas())
             {
                if (schema.getElementByName(operationName) != null)
                {
                   // the same operation needs to be defined in only one schema
                   schemaTargetNs = schema.getTargetNamespace();
                   break;
                }
             }

             if (schemaTargetNs == null)
             {
                LOG.log(Level.WARNING, "Operation %s not found in any schema in wsdl.", operationName);
                return false;
             }

             // use the resolved operation name, not the received one, as AXIS is case-sensitive
             OMElement res = sc.sendReceive(new QName(schemaTargetNs, targetOp.getName()), requestMessage);
             wsd.setResponseMessage(res);
          }
          catch (AxisFault fault)
          {
             // the response-header callback is not invoked in case of  SOAP faults

             String faultActor  = (fault.getNodeURI() == null) ? "" : fault.getNodeURI();
             String faultString = (fault.getReason() == null) ? "" : fault.getReason();
             String faultCode   =
                   (fault.getFaultCode() == null) ? "" : fault.getFaultCode().getLocalPart();

             int nlIdx = faultString.indexOf('\n');
             String faultMsg = (nlIdx >= 0) ? faultString.substring(0, nlIdx) : faultString;

             String faultDetailXML = fault.getFaultDetailElement() != null ?
                fault.getFaultDetailElement().toString() :
                null;

             ErrorManager.handleSoapFault(operationName, faultMsg, faultActor, faultCode, faultString,
                                          faultDetailXML);

             return false;
          }
          finally
          {
             LowLevelSocketImpl.setNoHostVerify(false);
          }

          // TODO: test how interruptions are treated when we are blocked invoking the operation
          return true;
       }

       /**
        * Obtain the values of output parameters after a successful Web Service invocation.
        *
        * @param   id
        *          The id of an open connection.
        *
        * @return  An array with output values returned by the last web service operation.
        */
       public Object[] getOutputValues(int id)
       {
          WebServiceData wsd = null;
          WorkArea wa = local.get();
          synchronized (wa)
          {
             wsd = wa.locate(id);
             if (wsd == null || wsd.getResponseMessage() == null)
             {
                return null;
             }
          }

          Object[] outputParams = wsd.getOutputParameters();

          // process the response and set the output parameters
          if (!processOutputMessage(wsd.getOutputMsgParameters(),
                                    outputParams,
                                    wsd.getResponseMessage()))
          {
             return null;
          }

          // in P4GL the response SOAP Envelope namespaces are merged with the namespace(s) in
          // response. The response SOAP Envelope namespaces are fetched in reverse order from right
          // to left
          if (outputParams.length == 1 && outputParams[0] instanceof Text)
          {
             // get SOAP Envelope namespaces an merge with response namespaces.
             String envns = null;
             try
             {
                envns = getEnvelopeNameSpaces(wsd.getServiceClient());
             }
             catch (AxisFault axisFault)
             {
                LOG.severe("", axisFault);
                return null;
             }
             mergeNameSpaces((Text) outputParams[0], envns);
          }
          return outputParams;
       }

       /**
        * Search the given parameter modes and return the input or output parameters, based on the
        * <code>input</code> flag.
        * <p>
        * The {@link InternalEntry#INPUT_OUTPUT_MODE INPUT-OUTPUT} will be returned in both cases.
        *
        * @param    modes
        *           The parameter modes.
        * @param    args
        *           The passed arguments.
        * @param    input
        *           On {@code true}, collect input parameters; else, collect output parameters.
        *
        * @return   The collected parameters from the <code>args</code> array.
        */
       private static Object[] collectParameters(String modes, Object[] args, boolean input)
       {
          List<Object> params = new ArrayList<>();
          for (int i = 0; i < modes.length(); i++)
          {
             char mode = modes.charAt(i);

             if (mode == InternalEntry.INPUT_OUTPUT_MODE     ||
                 (input && mode == InternalEntry.INPUT_MODE) ||
                 (!input && mode == InternalEntry.OUTPUT_MODE))
             {
                params.add(args[i]);
             }
          }

          return params.toArray();
       }

       /**
        * Resolve the parameters from the specified schema element and save them in the
        * <code>params</code> list: on index 0 the name and on index 1 the schema type.
        *
        *
        * @param    element
        *           The schema element which needs to be resolved.
        * @param    name
        *           The name of this element.
        * @param    params
        *           The list where to save the parameters.
        */
       private static void resolveParameters(XmlSchemaElement element,
                                             QName name,
                                             List<QName[]> params)
       {
          if (element == null)
          {
             return;
          }

          // TODO: what other features need to be treated?

          XmlSchemaType elType = element.getSchemaType();
          if (elType instanceof XmlSchemaSimpleType)
          {
             // found a simple type, use it
             params.add(new QName[] { name, element.getSchemaTypeName() });
          }
          else if (elType instanceof XmlSchemaComplexType)
          {
             XmlSchemaComplexType type = (XmlSchemaComplexType) elType;
             XmlSchemaParticle particle = type.getParticle();
             if (particle instanceof XmlSchemaAny)
             {
                // TODO:
             }
             else if (particle instanceof XmlSchemaElement)
             {
                // TODO:
             }
             else if (particle instanceof XmlSchemaAll)
             {
                // TODO:
             }
             else if (particle instanceof XmlSchemaChoice)
             {
                // TODO:
             }
             else if (particle instanceof XmlSchemaSequence)
             {
                // TODO:
                XmlSchemaSequence xmlSeq = (XmlSchemaSequence) particle;
                int no = xmlSeq.getItems().size();
                for (int i = 0; i < no; i++)
                {
                   XmlSchemaSequenceMember obj = xmlSeq.getItems().get(i);
                   if (obj instanceof XmlSchemaElement)
                   {
                      XmlSchemaElement elObj = (XmlSchemaElement) obj;
                      resolveParameters(elObj, elObj.getQName(), params);
                   }
                }
             }
             else if (particle instanceof XmlSchemaGroupRef)
             {
                // TODO:
             }

          }
       }

       /**
        * Search the through the list of {@link ExtensibilityElement}s for one matching the give
        * type.
        *
        * @param    extensions
        *           The list of extensibility elements.
        * @param    type
        *           The type to be matched.
        *
        * @return   The found element or {@code null} if not found.
        */
       @SuppressWarnings("unchecked")
       private static <T> T getExtension(List<?> extensions, Class<T> type)
       {
          if (extensions == null)
          {
             return null;
          }

          Iterator<ExtensibilityElement> extElIter =
                (Iterator<ExtensibilityElement>) extensions.iterator();
          while (extElIter.hasNext())
          {
             ExtensibilityElement extEl = extElIter.next();
             if (type.isAssignableFrom(extEl.getClass()))
             {
                return (T) extEl;
             }
          }

          return null;
       }

       /**
        * Get the implicit port from the given collection of ports.  The found {@link Port} must be
        * associated with a {@link Binding} with a {@link SOAPBinding SOAP 1.1. extensibility}.  If
        * more than one port matches or no port found, return {@code null}.
        *
        * @param    ports
        *           The {@link Port} instances to be searched.
        *
        * @return   The found {@link Port} following the above rules.
        */
       @SuppressWarnings("unchecked")
       private static Port getImplicitSoapPort(Collection<?> ports)
       {
          Iterator<Port> portIter = (Iterator<Port>) ports.iterator();
          Port soapPort = null;
          while (portIter.hasNext())
          {
             Port port = portIter.next();
             Binding binding = port.getBinding();

             if (getExtension(binding.getExtensibilityElements(), SOAPBinding.class) != null)
             {
                if (soapPort != null)
                {
                   // more than one SOAP port found, return "not found"
                   return null;
                }

                soapPort = port;
             }
          }

          return soapPort;
       }

       /**
        * Show a specific 4GL-style error meaning that no service could be resolved from the WSDL
        * document.
        */
       private static void noServicesError()
       {
          ErrorManager.recordOrShowError(11735, "No Services located in the WSDL document",
                false, false, false);
       }

       /**
        * Validate the operation's parameters against the input and output parameters sent by the
        * caller.
        * <p>
        * The rules are:
        * <ol>
        *    <li>all output parameters must be a {@link BaseDataType};
        *    <li>if only one input (or output) parameter is specified, is assumed to be wrapped;
        *    <li>if no input (or output) parameters are specified, then the operation must have no
        *        input (or output) parameters.
        * </ol>
        *
        * @param   op
        *          The service operation to be invoked.
        * @param   service
        *          TBA
        * @param   input
        *          The passed input parameters.
        * @param   output
        *          The passed output parameters.
        * @param   inParams
        *          The number of input parameters defined at the operation.
        * @param   outParams
        *          The number of output parameters defined at the operation.
        *
        * @return  {@code true} if the parameters are valid.
        */
       private static boolean validateParameters(Operation op,
                                                 Service service,
                                                 Object[] input,
                                                 Object[] output,
                                                 int inParams,
                                                 int outParams)
       {
          // all outputParams must be BDT
          int i = 0;
          for (Object param : output)
          {
             if (!(param instanceof BaseDataType))
             {
                String msg = "Error invoking operation %s on service %s: Parameter %d is of " +
                      "type %s, which is not BaseDataType compatible!";
                msg = String.format(msg, op.getName(), service.getQName().getLocalPart(), i,
                      (param == null ? "null" : param.getClass().getName()));

                throw new IllegalStateException(msg);
             }
             i++;
          }

          // validate input parameters
          boolean paramError = false;
          if (input.length > 0)
          {
             for (Object param : input)
             {
                if (!(param instanceof BaseDataType))
                {
                   // this is uncommon, the 'inline' parameters should be wrapped
                }
                else if (((BaseDataType) param).isUnknown())
                {
                   ErrorManager.recordOrThrowError(11790, "The 4GL value may not be UNKNOWN", false);
                   return false;
                }
             }

             if (input.length == 1)
             {
                // in this case, we assume that the parameter is passed as XML message OR there is
                // only one input parameter compatible with the one at the operation

                // type validation will be done later, when the input message is computed
             }
             else if (input.length != inParams)
             {
                paramError = true;
             }
          }
          else if (inParams > 0)
          {
             // no input parameters specified, fail as the operation has input parameters
             paramError = true;
          }

          // validate output parameters
          if (output.length > 0)
          {
             if (output.length == 1)
             {
                // in this case, we assume that the parameter is passed as XML message OR there is
                // only one output parameter compatible with the one at the operation

                // type validation will be done later, when the output message is computed
             }
             else if (output.length != outParams)
             {
                paramError = true;
             }
          }
          else if (outParams > 0)
          {
             // no output parameters specified, fail as the operation has output parameters
             paramError = true;
          }

          if (paramError)
          {
             // TODO: alternate errors found while testing with wrong parameters:
             //          Mismatched parameter types passed to procedure <procedure>. (3230)
             //          Mismatched number of parameters passed to routine <name>. (3234)
             String msg = "Failure initializing SOAP Call: Cannot match signature for operation '%s'";
             ErrorManager.recordOrThrowError(11762, String.format(msg, op.getName()));
          }

          return !paramError;
       }

       /**
        * Using the input parameters configured at the operation's input message and the input
        * parameters sent by the caller, create a new XML payload representing the input message
        * passed to the operation call.
        * <p>
        * The caller may not pass null or unknown values.  If the caller passes only one input
        * parameter of type {@link Text} or {@link String}, then it is assumed we are in wrapped
        * mode and the parameter holds the entire XML payload.
        *
        *
        * @param   wsd
        *          The definition of the WSDL that contains the operation.
        * @param   op
        *          The operation to be invoked.
        * @param   inMsgParams
        *          The (name, type) mappings of each parameter, defined at the operation's input.
        * @param   inputParams
        *          The input parameters sent by the caller.
        *
        * @return   A new {@link OMElement} structure with the XML payload for the input message.
        */
       @SuppressWarnings("unchecked")
       private static OMElement buildInputMessage(WebServiceData wsd,
                                                  Operation op,
                                                  QName[][] inMsgParams,
                                                  Object[] inputParams)
       {
          Definition wsdlDef = wsd.wsdlDef;

          // we need to build a new request message
          String ns     = wsdlDef.getTargetNamespace();
          String prefix = wsdlDef.getPrefix(ns);

          if (inputParams.length == 1)
          {
             // if there is only one input parameter, then that parameter must be a character/longchar
             // with the input message encoded as XML.

             // get the string representation of the parameter
             Object oxml = inputParams[0];
             String xml = null;
             if (oxml instanceof Text)
             {
                xml = ((Text) oxml).toStringMessage();
             }
             else if (oxml instanceof String) // never ?
             {
                xml = (String) oxml;
             }
             else
             {
                xml = (oxml == null ? "?" : oxml.toString());
             }

             // naive attempt to verify if we are in wrapped mode
             if (xml.startsWith("<?xml version=\"1.0\" encoding=\"utf-8\"?>"))
             {
                try
                {
                   // TODO: the exception to this is when the only argument is of type char
                   // in this case, it is possible to pass it in unwrapped mode

                   // 4GL does not care of what the XML contains - it must be a valid XML
                   OMElement om = AXIOMUtil.stringToOM(xml);

                   if (om.getNamespace() == null)
                   {
                      om.setNamespace(om.declareNamespace(ns, prefix));
                   }

                   return om;
                }
                catch (XMLStreamException e)
                {
                   final String msg = "Malformed XML fragment:  %s (11781)";
                   ErrorManager.recordOrThrowError(
                         11781, String.format(msg, e.getMessage()), false, true);
                   return null;
                }
             }
          }

          // reaching this point means we are in no-wrapping mode (if a single text parameter was
          // supplied, is it not in xml format)
          BaseDataType[] params = new BaseDataType[inputParams.length];
          for (int i = 0; i < inputParams.length; i++)
          {
             Object param = inputParams[i];

             if (param == null ||
                 (param instanceof BaseDataType && ((BaseDataType) param).isUnknown()))
             {
                ErrorManager.recordOrThrowError(11790, "The 4GL value may not be UNKNOWN (11790)",
                                                false, true);
                return null;
             }

             if (param instanceof BaseDataType)
             {
                params[i] = (BaseDataType) param;
             }
             else
             {
                // TODO: build a BDT based on the param's java type
                params[i] = new character(inputParams[i].toString());
             }
          }

          String schemaTargetNs = null;

          for (XmlSchema schema : wsd.getXmlSchemas())
          {
             if (schema.getElementByName(op.getName()) != null)
             {
                // the same operation needs to be defined in only one schema
                schemaTargetNs = schema.getTargetNamespace();
                break;
             }
          }

          OMElement root = soapFactory.createOMElement(new QName(schemaTargetNs, op.getName(), prefix));
          for (int i = 0; i < params.length; i++)
          {
             // the order of the passed parameters must match the order of the parts at the message
             // definition. the check is done by converting the value to the expected type

             // attempt to convert to the XSD type, and from there to string
             String sval = marshallValue(inMsgParams[i][0], inMsgParams[i][1], params[i]);
             QName elName = new QName(schemaTargetNs, inMsgParams[i][0].getLocalPart(), prefix);
             OMElement child = soapFactory.createOMElement(elName);
             child.addChild(soapFactory.createOMText(child, sval));
             root.addChild(child);
          }
          return root;
       }

       /**
        * Marshall a value from Java/Progress style to SOAP style.  This will also validate the input
        * value with the parameter's type defined at the schema.
        * <p>
        * See the "Table 3-2: Supported XML data types" from OpenEdge(R) Development: Web Services,
        * page 3-15.
        *
        * @param   paramName
        *          The parameter's name, as defined at the SOAP message.
        * @param   paramType
        *          The parameter's type, as defined at the schema.
        * @param   value
        *          The JAva or Progress-style value to be converted.
        *
        * @return  The string representation of this value. The method does the best effort to
        *          convert to a string representation of XSD parameter type. The last resort being
        *          {@link BaseDataType#toStringMessage()} method.
        */
       private static String marshallValue(QName paramName, QName paramType, BaseDataType value)
       {
          if (value instanceof Text)
          {
             return value.toStringMessage();
          }
          else if (value instanceof logical)
          {
             return Boolean.toString(((logical) value).booleanValue());
          }
          else if (value instanceof decimal)
          {
             return Double.toString(((decimal) value).doubleValue());
          }
          else if (value instanceof integer)
          {
             return Integer.toString(((integer) value).intValue());
          }
          else if (value instanceof int64)
          {
             return Long.toString(((int64) value).longValue());
          }
          else if (value instanceof datetime)
          {
             // format is ISO = 'CCYY-MM-DDThh:mm:ss[.zzzzzzzz][+/-hh:mm]
             return ((datetime) value).getIsoDate();
          }
          else if (value instanceof date)
          {
             // format is ISO = 'CCYY-MM-DD' (might contain a timezone, too)
             return ((date) value).getIsoDate();
          }
          else
          {
             return value.toStringMessage(); // in any other case
          }
       }

       /**
        * Unmarshall a value received as a response for an service operation request to a
        * {@link BaseDataType}, depending on the parameter's type defined at the operation's output
        * message.  This will also validate the value with the parameter's type defined at the schema.
        * <p>
        * See the "Table 3-2: Supported XML data types" from OpenEdge(R) Development: Web Services,
        * page 3-15.
        *
        * @param    paramName
        *           The parameter's name, as defined at the SOAP message.
        * @param    paramType
        *           The parameter's type, as defined at the schema.
        * @param    param
        *           The response received after an operation invocation.
        *
        * @return   A {@link BaseDataType} instance for the received parameter or {@code null} if
        *           conversion is not possible.
        */
       private static BaseDataType unmarshallValue(QName paramName, QName paramType, OMElement param)
       {
          // TODO: check if paramName.getLocalPart() == param.getLocalName() or throw exception ?
          // TODO: use some constants for types from: http://www.w3.org/2001/XMLSchema-datatypes
          switch (paramType.getLocalPart())
          {
             case "string":
                return new character(param.getText());
             case "boolean":
                return new logical(param.getText());
             case "float":
             case "double":
             case "decimal":
                // the web service will return the the value in 'standard' decimal format
                return new decimal(Double.parseDouble(param.getText()));
             case "byte":
             case "int":
             case "integer":
             case "short":
                return new integer(Integer.parseInt(param.getText()));
             case "long":
                return new int64(Long.parseLong(param.getText()));
             case "date":
                // format is ISO = 'CCYY-MM-DD'(might contain a timezone, too)
                return new date(param.getText(), date.DEFAULT_DATETIME_FORMAT, date.DEFAULT_DATE_WINDOWING_YEAR);
             case "datetime":
                // format is ISO = 'CCYY-MM-DDThh:mm:ss[.zzzzzzzz]/[+/-hh:mm]
                return datetimetz.fromLiteral(param.getText());

             // TODO: add other types
          }

          return null;
       }

       /**
        * Process the message received after an operation invocation and populate the output
        * parameters sent by the caller accordingly.
        * <p>
        * If the caller passes only one input parameter of type {@link Text}, then it is assumed we
        * are in wrapped mode and that parameter will hold the entire XML payload for the received
        * message, serialized as string.
        *
        * @param    outMsgParams
        *           The (name, type) mappings of each parameter, defined at the operation's output.
        * @param    outputParams
        *           The output parameters sent by the caller; each element is a {@link BaseDataType}.
        * @param    message
        *           The message received after the operation invocation.
        *
        * @return   {@code true} if message processed or {@code false} if there were
        *           problems during conversion of the received to their {@link BaseDataType} expected
        *           type.
        */
       @SuppressWarnings("unchecked")
       private static boolean processOutputMessage(QName[][] outMsgParams,
                                                   Object[]  outputParams,
                                                   OMElement message)
       {
    //      if (outputParams.length == 1)
    //      {
    //         // check if we are emitting the entire XML
    //         BaseDataType param = (BaseDataType) outputParams[0];
    //         if (param instanceof Text)
    //         {
    //            // TODO: what if there is only one message output parameter, of type char? should we
    //            // unwrap it?
    //
    //            Text txt = (Text) param;
    //            txt.assign(message.toString());
    //
    //            return true;
    //         }
    //      }

          // in all other cases, match the output parameters with the received message

          int i = 0;
          Iterator<?> itrElem = message.getChildren();
          while (itrElem.hasNext())
          {
             Object child = itrElem.next();
             if (!(child instanceof OMElement))
             {
                continue;
             }

             OMElement omChild = (OMElement) child;
             BaseDataType param = (BaseDataType) outputParams[i];

             BaseDataType value = unmarshallValue(outMsgParams[i][0], outMsgParams[i][1], omChild);
             if (value == null)
             {
                String xmlType = outMsgParams[i][1].getLocalPart();
                String legacyType = param.getTypeName();

                final String msg = "Unable to convert XML data type %s to 4GL type %s  (11793)";
                ErrorManager.recordOrThrowError(11793, String.format(msg, xmlType, legacyType));

                return false;
             }

             param.assign(value);

             i++;
          }

          return true;
       }

       /**
        * Get the WSDL port name we'd like to access. Check if {@code portTypeName} is in the list
        * of WSDL ports. If {@code true} the {@code portTypeName} is equals to port name and could be
        * used for bindings. If {@code false} return {@code null} and let service to choose the
        * appropriate SOAP port.
        *
        * @param   portTypeName
        *          port type name.
        * @param   wsdlDef
        *          The definition of the WSDL that contains the port type.
        *
        * @return  name of the WSDL port we'd like to access.
        */
       @SuppressWarnings({ "rawtypes", "unchecked" })
       private static String getPortName(String portTypeName, Definition wsdlDef)
       {
          // WSDL bindings map
          Map bindings = wsdlDef.getBindings();

          if ((bindings != null) && (portTypeName != null))
          {
             // list of ports names
             Set<QName> ports = bindings.keySet();

             for (QName port : ports)
             {
                if (portTypeName.equals(port.getLocalPart()))
                {
                   return portTypeName;
                }
             }
          }
          return null;
       }

       /**
        * Resolve the parameters for the given message.
        *
        * @param   msg
        *          The message.
        * @param   wsd
        *          A {@code WebServiceData} structure that contains information about schemas and
        *          parameters.
        *
        * @return  An array where, for each element, on index 0 is the parameter's name and on index
        *          1 is the parameter's type.
        */
       @SuppressWarnings("unchecked")
       private static QName[][] getMessageParameters(Message msg, WebServiceData wsd)
       {
          buildSchema(wsd);

          // TODO: complex types can define parameters as i.e. "sequence", "all" or "choice". this
          // means is not enough to determine the entire list of possible parameters, as for "all" and
          // "choice" matching might be done via the parameter's type (for input) or the passed name
          // and type (for output).  thus, this method might need to return some complex structure
          // instead of (name, type) mappings.

          QName msgName = msg.getQName();
          if (wsd.msgParams.containsKey(msgName))
          {
             return wsd.msgParams.get(msgName);
          }

          // resolve the parameters
          List<QName[]> params = new ArrayList<>();

          for (Part part : (Iterable<Part>) msg.getParts().values())
          {
             QName name = part.getElementName();

             /// walk the schemas and determine the element
             for (XmlSchema schema : wsd.xmlSchemas)
             {
                XmlSchemaElement element = schema.getElementByName(name);

                resolveParameters(element, name, params);
             }
          }

          QName[][] qparams = params.toArray(new QName[][] {});
          wsd.msgParams.put(msgName, qparams);

          return qparams;
       }

       /**
        * If the {@link WebServiceData#xmlSchemas} list is not yet build, extract all the schema
        * extensibility elements from the {@link WebServiceData#wsdlDef WSDL definition} and save
        * them in the {@link WebServiceData#xmlSchemas} list.
        *
        * @param   serviceData
        *          The structure that contains information about the current connection and where the
        *          schema will be stored.
        */
       @SuppressWarnings("unchecked")
       private static void buildSchema(WebServiceData serviceData)
       {
          if (serviceData.getXmlSchemas() != null)
          {
             return;
          }

          List<XmlSchema> xmlSchemas = new ArrayList<>();
          serviceData.setXmlSchemas(xmlSchemas);

          Types types = serviceData.wsdlDef.getTypes();
          for (ExtensibilityElement el :
                   (Iterable<ExtensibilityElement>) types.getExtensibilityElements())
          {
             if (el instanceof Schema)
             {
                Schema schema = (Schema) el;
                String baseUri = schema.getDocumentBaseURI();

                XmlSchemaCollection xsc = new XmlSchemaCollection();
                if (baseUri != null)
                {
                   xsc.setBaseUri(baseUri);
                }
                XmlSchema xmlSchema = xsc.read(schema.getElement());

                xmlSchemas.add(xmlSchema);
             }
          }
       }

       /**
        * Build a string containing the SOAP Envelope namespaces in reverse order.
        *
        * @param   sc
        *          ServiceClient instance.
        *
        * @return  SOAP Envelope namespaces serialized as as string.
        *
        * @throws  AxisFault
        *          In case of errors.
        */
       @SuppressWarnings("unchecked")
       private static String getEnvelopeNameSpaces(ServiceClient sc)
       throws AxisFault
       {
          StringBuilder sb = new StringBuilder();

          MessageContext messageContext = sc.getLastOperationContext()
                .getMessageContext(WSDLConstants.MESSAGE_LABEL_IN_VALUE);
          SOAPEnvelope soapEnv = messageContext.getEnvelope();

          // collect envelope namespaces.
          List<OMNamespace> names = new LinkedList<>();
          Iterator<OMNamespace> iter = soapEnv.getAllDeclaredNamespaces();
          while (iter.hasNext())
          {
             names.add(iter.next());
          }

          // iterate in reverse order
          ListIterator<OMNamespace> listIter = names.listIterator(names.size());
          while (listIter.hasPrevious())
          {
             OMNamespace ns = listIter.previous();
             sb.append(" xmlns:").append(ns.getPrefix()).
                   append("=\"").append(ns.getNamespaceURI()).append("\"");
          }

          return sb.toString();
       }

       /**
        * Merge response namespace with SOAP Envelope namespaces.
        *
        * @param   res
        *          Response string in XML format.
        * @param   envns
        *          A string containing serialized SOAP Envelope namespaces
        *
        * @return  Response merged with SOAP Envelope namespaces
        */
       private static Text mergeNameSpaces(Text res, String envns)
       {
          if (envns != null)
          {
             StringBuilder sb = new StringBuilder();

             String str = res.getValue().trim();

             if (str.charAt(0) == '<')
             {
                int pos = str.indexOf(">");
                if (pos > 0)
                {
                   sb.append(str.substring(0, pos));
                   sb.append(envns);
                   sb.append(str.substring(pos));
                   res.assign(sb.toString());
                }
             }
          }

          return res;
       }

       /**
        * Container for the context-local data. This will also ensure sync access to the
        * {@link #webServices} registry.
        */
       private static class WorkArea
       {
          /** A registry containing the connected Web Services. */
          private final Map<Integer, WebServiceData> webServices = new HashMap<>();

          /**
           * Add a new WebService to the {@link #webServices registry}.
           *
           * @param    id
           *           The ID of the associated WebService on P2J Client side.
           * @param    sd
           *           The data for this Web Service.
           */
          public synchronized void addWebService(int id, WebServiceData sd)
          {
             webServices.put(id, sd);
          }

          /**
           * Locate the {@link WebServiceData} for the the given ID.
           *
           * @param    id
           *           The ID of the associated WebService on P2J Client side.
           */
          private synchronized WebServiceData locate(int id)
          {
             return webServices.get(id);
          }

          /**
           * Remove the WebService with the given ID from the {@link #webServices registry}.
           *
           * @param    id
           *           The ID of the associated WebService on P2J Client side.
           */
          private synchronized void removeWebService(int id)
          {
             webServices.remove(id);
          }
       }

       /**
        * Data structure that is used to store data on client side between calls from P2J server side.
        * A {@code WebServiceData} uniquely identifies an <i>open</i> connection to a web service. It
        * can be in one of two states:
        * <ul>
        *    <li>idle - when no invocation is in progress. It only contains <i>final</i> data
        *       relative to connection itself;
        *    <li>busy - when waiting messages from server-side. In this case the structure also
        *       contains a second temporary state that is carried between those messages. This state
        *       is released when WebService operation is finished and the connection goes back to
        *       idle state and awaits for other operations.
        * </ul>
        *
        * The busy state is very similar to a state in a mini-session. However, at this moment the
        * implementation only allows single session/ invocation per connection at a given moment.
        * Serialized calls to same or different operations using the same connection are possible, of
        * course.
        */
       private static class WebServiceData
       {
          /** The id of the open connection. */
          private final int id;

          /** The WSDL document definition. */
          private final Definition wsdlDef;

          /** The WSDL port type. */
          private final PortType portType;

          /** The WSDL service. */
          private final Service service;

          /** A collection of all web service options used to create this connection. */
          private final WebServiceConnectOptions wsOptions;

          /** The {@link ServiceClient} for current invocation. {@code null} in idle state. */
          private ServiceClient sc = null;

          /** The {@link Operation} for current invocation. {@code null} in idle state. */
          private Operation targetOp = null;

          /** The built request message for current invocation. {@code null} in idle state. */
          private OMElement requestMessage = null;

          /** The response from WebService in current invocation. {@code null} in idle state. */
          private OMElement responseMessage = null;

          /** The output parameters for current invocation. {@code null} in idle state. */
          private Object[] outputParams = null;

          /** The description for output parameters. {@code null} in idle state. */
          private QName[][] outMsgParams = null;

          /** List of XML Schema's loaded from the WSDL document. */
          private List<XmlSchema> xmlSchemas = null;

          /** Cache of the resolved parameters, for each message. */
          private final Map<QName, QName[][]> msgParams = new HashMap<>();

          /**
           * The constructors initialize the final fields that compose the immutable part of the
           * connection. They are unchanged as long the connection is alive.
           *
           * @param   id
           *          The id of the open connection.
           * @param   wsdlDef
           *          The WSDL document definition.
           * @param   portType
           *          The WSDL port type.
           * @param   service
           *          The WSDL service.
           * @param   wsOptions
           *          A collection of all web service options used for connection.
           */
          public WebServiceData(int id,
                                Definition wsdlDef,
                                PortType portType,
                                Service service,
                                WebServiceConnectOptions wsOptions)
          {
             this.id = id;
             this.wsdlDef = wsdlDef;
             this.portType = portType;
             this.service = service;
             this.wsOptions = wsOptions;
          }

          /**
           * Get the id of this open connection.
           *
           * @return  the id of this connection.
           */
          public int getId()
          {
             return id;
          }

          /**
           * Get the optional the SOAP endpoint.
           *
           * @return  the SOAP endpoint.
           */
          public String getSoapEndpoint()
          {
             return wsOptions.getSoapEndpoint();
          }

          /**
           * Get the optional the SOAP endpoint user ID.
           *
           * @return  the SOAP endpoint user id.
           */
          public String getSoapEndpointUserid()
          {
             return wsOptions.getSoapEndpointUserid();
          }

          /**
           * Get the optional the SOAP endpoint password.
           *
           * @return  the SOAP endpoint password.
           */
          public String getSoapEndpointPassword()
          {
             return wsOptions.getSoapEndpointPassword();
          }

          /**
           * Get the {@link ServiceClient} for current invocation. {@code null} in idle state.
           *
           * @return  the {@link ServiceClient} for current invocation.
           */
          public ServiceClient getServiceClient()
          {
             return sc;
          }

          /**
           * Instruct the  connection whether to reuse or create a new SSL session when reconnecting
           * to the same Web server using HTTPS.
           *
           * @return  {@code true} if every SSL connections should be reused.
           */
          public boolean isSessionReuse()
          {
             return !wsOptions.isNoSessionReuse();
          }

          /**
           * Specifies whether host verification for connection using HTTPS should be disabled.
           *
           * @return  {@code true} if host verification for SSL connections will be skipped.
           *
           */
          public boolean isNoHostVerify()
          {
             return wsOptions.isNoHostVerify();
          }

          /**
           * Get the maximum number of simultaneous (parallel) connections maintained between the
           * client and Web service for asynchronous requests.
           *
           * @return  the maximum number of simultaneous (parallel) connections maintained between
           *          the client and Web service for asynchronous requests.
           */
          public int getMaxConnections()
          {
             return wsOptions.getMaxConnections();
          }

          /**
           * Get the maximum number of seconds that a given connection can be reused for asynchronous
           * requests before it is destroyed.
           *
           * @return the maximum number of seconds that a given connection can be reused for
           *         asynchronous requests before it is destroyed.
           */
          public int getConnectionLifetime()
          {
             return wsOptions.getConnectionLifetime();
          }

          /**
           * Set the {@link ServiceClient} for current invocation.
           *
           * @param   sc
           *          The new {@link ServiceClient} for current invocation.
           */
          public void setServiceClient(ServiceClient sc)
          {
             if (this.sc != null)
             {
                this.sc = null;
                // throw new InvalidStateException(
                //       "must finish current invocation before preparing a second one")
             }
             this.sc = sc;
          }

          /**
           * Get the built request message for current invocation.
           *
           * @return  the built request message for current invocation. In idle state it is
           *          {@code null}.
           */
          public OMElement getRequestMessage()
          {
             return requestMessage;
          }

          /**
           * Set the built request message for current invocation.
           *
           * @param   requestMessage
           *          the new built request message for current invocation.
           */
          public void setRequestMessage(OMElement requestMessage)
          {
             this.requestMessage = requestMessage;
          }

           /**
            * Get the response from WebService in current invocation.
            *
            * @return    the response from WebService in current invocation. It is {@code null} in
            *            idle state.
            */
          public OMElement getResponseMessage()
          {
             return responseMessage;
          }

          /**
           * Set the response from WebService in current invocation.
           *
           * @param   responseMessage
           *          the new response from WebService in current invocation.
           */
          public void setResponseMessage(OMElement responseMessage)
          {
             this.responseMessage = responseMessage;
          }

          /**
           * Get the {@link Operation} for current invocation.
           *
           * @return  the {@link Operation} for current invocation. {@code null} in idle state.
           */
          public Operation getTargetOperation()
          {
             return targetOp;
          }

          /**
           * Set the {@link Operation} for current invocation.
           *
           * @param  targetOp
           *          the new {@link Operation} for current invocation.
           */
          public void setTargetOperation(Operation targetOp)
          {
             this.targetOp = targetOp;
          }

          /**
           * Get the list of XML Schema's loaded from the WSDL document.
           *
           * @return  the list of XML Schema's loaded from the WSDL document.
           */
          public List<XmlSchema> getXmlSchemas()
          {
             return xmlSchemas;
          }

          /**
           * Get the list of XML Schema's loaded from the WSDL document.
           *
           * @param   xmlSchemas
           *          the new list of XML Schema's loaded from the WSDL document.
           */
          public void setXmlSchemas(List<XmlSchema> xmlSchemas)
          {
             this.xmlSchemas = xmlSchemas;
          }

          /**
           * Get the output parameters for current invocation.
           *
           * @return  the output parameters for current invocation. {@code null} in idle state.
           */
          public Object[] getOutputParameters()
          {
             return outputParams;
          }

          /**
           * Get the description for output parameters.
           *
           * @return  the description for output parameters. {@code null} in idle state.
           */
          public QName[][]  getOutputMsgParameters()
          {
             return outMsgParams;
          }

          /**
           * Set details about output parameters that will be used in a future call.
           *
           * @param   outputParams
           *          the output parameters for current invocation.
           * @param   outMsgParams
           *          the description for output parameters.
           */
          public void setOutputParameters(Object[] outputParams, QName[][] outMsgParams)
          {
             this.outputParams = outputParams;
             this.outMsgParams = outMsgParams;
          }

          /**
           * Cleans the temporary state used during invocation sequence. Must be done after each
           * successful call or if the invocation fails.
           */
          public void cleanupInvocation()
          {
             setRequestMessage(null);
             setResponseMessage(null);
             setOutputParameters(null, null);
             setTargetOperation(null);
             setServiceClient(null);
             setXmlSchemas(null);
             msgParams.clear();
          }
       }
    }