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();
      }
   }
}