SoapWsdl.java

/*
** Module   : SoapWsdl.java
** Abstract : Helper class to generate a WSDL document from a .wsm file and its exported operations.
**
** Copyright (c) 2020-2021, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------------Description----------------------------------------
** 001 CA  20200514 First version.
**     CA  20200528 The SOAP input are the .wsm files, not .xpxg (as these contain info configured when 
**                  running ProxyGen).
**                  Fixes for extent parameters and BEFORE-TABLE support.
**     CA  20210911 The targetNamespace was not properly set from the DeploymentWizard/WebServiceNamespace.
**     CA  20211012 Fixed generated WSDL and reworked SOAP support to rely on namespace, when resolving an
**                  operation.
**     CA  20211013 Added DATASET-HANDLE and TABLE-HANDLE parameter support for WSDL.
**                  DATASET parameters have the table table indexes, too, at the WSDL schema.
**     CA  20211015 Added the schema for dataset relations.
**     CA  20211112 Fixed cases when the XML node name differences from the parameter name (XML-NODE-NAME and
**                  SERIALIZE-NAME at the dataset, table and field).
*/

/*
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU Affero General Public License as
** published by the Free Software Foundation, either version 3 of the
** License, or (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
** GNU Affero General Public License for more details.
**
** You may find a copy of the GNU Affero GPL version 3 at the following
** location: https://www.gnu.org/licenses/agpl-3.0.en.html
** 
** Additional terms under GNU Affero GPL version 3 section 7:
** 
**   Under Section 7 of the GNU Affero GPL version 3, the following additional
**   terms apply to the works covered under the License.  These additional terms
**   are non-permissive additional terms allowed under Section 7 of the GNU
**   Affero GPL version 3 and may not be removed by you.
** 
**   0. Attribution Requirement.
** 
**     You must preserve all legal notices or author attributions in the covered
**     work or Appropriate Legal Notices displayed by works containing the covered
**     work.  You may not remove from the covered work any author or developer
**     credit already included within the covered work.
** 
**   1. No License To Use Trademarks.
** 
**     This license does not grant any license or rights to use the trademarks
**     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
**     of Golden Code Development Corporation. You are not authorized to use the
**     name Golden Code, FWD, or the names of any author or contributor, for
**     publicity purposes without written authorization.
** 
**   2. No Misrepresentation of Affiliation.
** 
**     You may not represent yourself as Golden Code Development Corporation or FWD.
** 
**     You may not represent yourself for publicity purposes as associated with
**     Golden Code Development Corporation, FWD, or any author or contributor to
**     the covered work, without written authorization.
** 
**   3. No Misrepresentation of Source or Origin.
** 
**     You may not represent the covered work as solely your work.  All modified
**     versions of the covered work must be marked in a reasonable way to make it
**     clear that the modified work is not originating from Golden Code Development
**     Corporation or FWD.  All modified versions must contain the notices of
**     attribution required in this license.
*/

package com.goldencode.p2j.pattern;

import java.net.*;
import java.util.*;
import java.util.concurrent.atomic.*;

import javax.xml.parsers.*;

import org.w3c.dom.*;

import com.goldencode.p2j.*;
import com.goldencode.p2j.persist.annotation.*;
import com.goldencode.p2j.uast.*;
import com.goldencode.p2j.util.*;
import com.goldencode.util.*;

import static com.goldencode.p2j.uast.ProgressParserTokenTypes.*;

/**
 * Generate the WSDL file for all services exported by a .wsm file.
 */
class SoapWsdl
{
   /** The namespace for the SOAP elements. */
   private static final String NS_SOAP = "http://schemas.xmlsoap.org/wsdl/soap/";
   
   /** The namespace for the WSDL elements. */
   private static final String NS_WSDL = "http://schemas.xmlsoap.org/wsdl/";

   /** The namespace for the XMLNS elements. */
   private static final String NS_XMLNS = "http://www.w3.org/2000/xmlns/";

   /** The namespace for the PRODATA elements. */
   private static final String NS_PRODATA = "urn:schemas-progress-com:xml-prodata:0001";

   /** The WSDL exported services. */
   private final Map<String, ServiceConfig> services = new HashMap<>();
   
   /** The WSDL document. */
   private final Document dom;

   /** The WSDL root element. */
   private final Element root;

   /** The element where to append the schema elements defining all the types in a namespace. */
   private final Element elTypes;

   /** The default namespace for the WSDL. */
   private String targetNamespace = "urn:tempuri-org";
   
   /** The target endpoint (only the path, without protocol, host and port). */
   private String endpoint = "";
   
   /** The default actioan name. */
   private String soapAction = "";
   
   /** Flag to append the object name to the {@link #soapAction}. */
   private boolean appendObjNameToSoapAction = false;
   
   /** The suffix for the port-type binding operation. */
   private String portTypeBindingSuffix =  "Obj";
   
   /** The suffix for the service. */
   private String serviceSuffix = "Service";

   /** The default service. */
   private String defaultService;
   
   /** Map of datasets defined in this WSDL. */
   private Map<String, Element> datasets = new HashMap<>(); 
   
   /** Set of added table parameters. */
   private Set<String> tableParams = new HashSet<>();
   
   /** The help string to be used as service documentation. */
   private String helpString;
   
   /**
    * Create a new instance.
    */
   public SoapWsdl()
   {
      try
      {
         dom = XmlHelper.newDocument();
      }
      catch (ParserConfigurationException e)
      {
         throw new RuntimeException(e);
      }
      
      root = dom.createElementNS(NS_WSDL, "wsdl:definitions");
      dom.appendChild(root);
      
      // create the 'types' container
      elTypes = dom.createElementNS(NS_WSDL, "wsdl:types");
      // create the soap-fault type
      Element elSchema = dom.createElement("schema");
      elSchema.setAttribute("elementFormDefault", "unqualified");
      elSchema.setAttribute("targetNamespace", "urn:soap-fault:details");
      elSchema.setAttribute("xmlns", "http://www.w3.org/2001/XMLSchema");
      List<SoapOperationParameter> faultParameters = new ArrayList<>();
      faultParameters.add(new SoapOperationParameter("errorMessage", VAR_CHAR, KW_OUTPUT, false));
      faultParameters.add(new SoapOperationParameter("requestID", VAR_CHAR, KW_OUTPUT, false));
      Element elTypeFault = createComplexType(null, "FaultDetail", faultParameters, true);
      elSchema.appendChild(elTypeFault);
      elTypes.appendChild(elSchema);
   }
   
   /**
    * Set the service documentation.
    * 
    * @param    helpString
    *           The help string.
    */
   public void setHelpString(String helpString)
   {
      this.helpString = helpString;
   }
   
   /**
    * Set the endpoint for this WSDL.
    * 
    * @param    endpoint
    *           The configured endpoint from the .wsm file.
    */
   public void setEndpoint(String endpoint)
   {
      try
      {
         URI uri = new URI(endpoint);
         endpoint = uri.getPath();
         
         this.endpoint = endpoint;
      } 
      catch (URISyntaxException e)
      {
         throw new RuntimeException("Could not resolve endpoint path: " + endpoint, e);
      }
   }
   
   /**
    * Set the {@link #appendObjNameToSoapAction} flag.
    * 
    * @param    appendObjNameToSoapAction
    *           The new {@link #appendObjNameToSoapAction} value.
    */
   public void setAppendObjNameToSoapAction(boolean appendObjNameToSoapAction)
   {
      this.appendObjNameToSoapAction = appendObjNameToSoapAction;
   }
   
   /**
    * Set the port-type binding suffix. It will override the default 'Obj' suffix only if the new value is
    * not-null and not empty.
    * 
    * @param    portTypeBindingSuffix
    *           The new {@link #portTypeBindingSuffix} value.
    */
   public void setPortTypeBindingSuffix(String portTypeBindingSuffix)
   {
      if (portTypeBindingSuffix == null)
      {
         return;
      }
      
      this.portTypeBindingSuffix = portTypeBindingSuffix;
   }
   
   /**
    * Set the service suffix.  It will override the default 'Service' suffix only if the new value is not
    * null and not empty.
    * 
    * @param    serviceSuffix
    *           The new service suffix.
    */
   public void setServiceSuffix(String serviceSuffix)
   {
      if (serviceSuffix == null)
      {
         return;
      }
      
      this.serviceSuffix = serviceSuffix;
   }
   
   /**
    * Set the soap action.
    * 
    * @param    soapAction
    *           The SOAP action.
    */
   public void setSoapAction(String soapAction)
   {
      if (soapAction == null)
      {
         return;
      }
      
      this.soapAction = soapAction;
   }
   
   /**
    * Add a WSDL service.
    * 
    * @param    service
    *           The service name.
    * @param    parentService
    *           The parent service name.
    */
   public void addService(String service, String parentService)
   {
      if (services.containsKey(service))
      {
         return;
      }
      
      boolean first = services.isEmpty();
      if (first)
      {
         root.setAttribute("name", service);
         defaultService = service;
      }
      
      ServiceConfig srv = new ServiceConfig(service, services.size() + 2, parentService);
      services.put(service, srv);
      
      if (first)
      {
         Element el = dom.createElementNS(NS_WSDL, "wsdl:message");
         el.setAttribute("name", "FaultDetailMessage");
         Element elPart = dom.createElementNS(NS_WSDL, "wsdl:part");
         elPart.setAttribute("name", "FaultDetail");
         elPart.setAttribute("element", "S1:FaultDetail");
         el.appendChild(elPart);

         srv.messages.add(el);
      }
   }

   /**
    * Add a SOAP operation to a service.
    * 
    * @param    operation
    *           The SOAP operation configuration.
    */
   public void addOperation(SoapOperation operation)
   {
      // TODO: createAO - is it needed? If not, document why.
      if (operation.parent == null && operation.persistent)
      {
         // add the 'CreatePO_%s'
         ServiceConfig srv = services.get(operation.parentService);
         String typeName = "CreatePO_" + operation.operation;
         
         List<SoapOperationParameter> inputParms = new ArrayList<>();
         srv.elSchema.appendChild(createComplexType(operation, typeName, inputParms, true));

         List<SoapOperationParameter> outputParms = new ArrayList<>();
         outputParms.add(new SoapOperationParameter("result", operation.retValType, KW_OUTPUT, true));
         srv.elSchema.appendChild(createComplexType(operation, typeName + "Response", outputParms, true));

         // add messages
         srv.messages.add(createMessage(srv, operation.operation, typeName, null));
         srv.messages.add(createMessage(srv, operation.operation + "Response", typeName + "Response", null));

         // add operation to port
         srv.elPortType.appendChild(createPortTypeOperation(srv, typeName, operation.operation));

         // add operation to binding
         srv.elBinding.appendChild(createBindingOperation(srv, operation, typeName, false));
         
         ////////////////////////////////////////////////////////////////////////////////////////
         
         // create the 'Release_%s' in the operation's service
         
         srv = services.get(operation.service);
         typeName = "Release_" + operation.operation;
         
         List<SoapOperationParameter> idParms = new ArrayList<>();
         idParms.add(new SoapOperationParameter("UUID", VAR_CHAR, KW_OUTPUT, false));
         srv.elSchema.appendChild(createComplexType(operation, operation.operation + "ID", idParms, true));
         srv.elSchema.appendChild(createComplexType(operation, typeName, null, true));
         srv.elSchema.appendChild(createComplexType(operation, typeName + "Response", null, true));
         
         // add messages
         srv.messages.add(createMessage(srv, operation.operation + "ID", operation.operation + "ID", operation.operation + "ID"));
         srv.messages.add(createMessage(srv, operation.operation + "Release", typeName, null));
         srv.messages.add(createMessage(srv, operation.operation + "ReleaseResponse", typeName + "Response", null));
         
         // add operation to port
         srv.elPortType.appendChild(createPortTypeOperation(srv, typeName, operation.operation + "Release"));

         // add operation to binding
         srv.elBinding.appendChild(createBindingOperation(srv, operation, typeName, true));
         
         return;
      }
      
      ServiceConfig srv = services.get(operation.service);
      
      srv.elSchema.appendChild(createComplexType(operation, 
                                                 operation.operation, 
                                                 operation.getParameters(true),
                                                 true));
      List<SoapOperationParameter> outputParms = operation.getParameters(false);
      if (operation.useRetVal)
      {
         outputParms.add(0, new SoapOperationParameter("result", operation.retValType, KW_OUTPUT, true));
      }
      srv.elSchema.appendChild(createComplexType(operation,
                                                 operation.operation + "Response", 
                                                 outputParms,
                                                 true));

      // add messages
      srv.messages.add(createMessage(srv, operation.operation, null, null));
      srv.messages.add(createMessage(srv, operation.operation + "Response", null, null));
      
      // add operation to port
      srv.elPortType.appendChild(createPortTypeOperation(srv, operation.operation, null));
      
      // add operation to binding
      Boolean hID = (operation.parent == null ? null : operation.parent.persistent);
      srv.elBinding.appendChild(createBindingOperation(srv, operation, operation.operation, hID));
   }
   
   /**
    * Generate the WSDL document.  This will iterate over all the {@link #services} and create the WSDL 
    * message, port-type, binding and service nodes.
    * 
    * @return   The WSDL document.
    */
   public Document generate()
   {
      for (ServiceConfig service : services.values())
      {
         root.setAttributeNS(NS_XMLNS, "xmlns:" + service.ns, targetNamespace + ":" + service.name);
      }

      // append all message nodes
      for (ServiceConfig service : services.values())
      {
         for (Element el : service.messages)
         {
            root.appendChild(el);
         }
      }

      // append all portType nodes
      for (ServiceConfig service : services.values())
      {
         root.appendChild(service.elPortType);
      }
      
      // append all binding nodes
      for (ServiceConfig service : services.values())
      {
         root.appendChild(service.elBinding);
      }

      // append all service nodes
      Element elService = dom.createElementNS(NS_WSDL, "wsdl:service");
      elService.setAttribute("name", root.getAttribute("name") + serviceSuffix);
      root.appendChild(elService);
      for (ServiceConfig service : services.values())
      {
         elService.appendChild(service.elServicePort);
      }

      return dom;
   }
   
   /**
    * Initialize this WSDL.
    * 
    * @param    author
    *           The author name.
    * @param    targetNS
    *           The target namespace.
    */
   void initialize(String author, String targetNS)
   {
      if (targetNS != null)
      {
         targetNamespace = targetNS;
      }
      
      root.setAttribute("targetNamespace", targetNamespace);
      root.setAttributeNS(NS_XMLNS, "xmlns:S1", "urn:soap-fault:details");
      root.setAttributeNS(NS_XMLNS, "xmlns:wsdl", "http://schemas.xmlsoap.org/wsdl/");
      root.setAttributeNS(NS_XMLNS, "xmlns:tns", targetNamespace);
      root.setAttributeNS(NS_XMLNS, "xmlns:xsd", "http://www.w3.org/2001/XMLSchema");
      root.setAttributeNS(NS_XMLNS, "xmlns:soap", "http://schemas.xmlsoap.org/wsdl/soap/");
      root.setAttributeNS(NS_XMLNS, "xmlns:soapenc", "http://schemas.xmlsoap.org/soap/encoding/");
      root.setAttributeNS(NS_XMLNS, "xmlns:prodata", "urn:schemas-progress-com:xml-prodata:0001");
      
      Element elDoc = dom.createElementNS(NS_WSDL, "wsdl:documentation");
      String serviceDoc = "EncodingType=DOC_LITERAL, FWD=" + Version.getFWDVersion().replace(' ', '_');
      if (author != null && !author.isEmpty())
      {
         serviceDoc = "Author=" + author + ", " + serviceDoc;
      }
      elDoc.appendChild(dom.createTextNode(serviceDoc));
      root.appendChild(elDoc);
      
      root.appendChild(elTypes);
   }
   
   /**
    * Create an operation associated with a WSDL binding element.
    *  
    * @param    srv
    *           The service configuration.
    * @param    operation
    *           The SOAP operation.
    * @param    name
    *           The binding operation name.
    * @param    headerID
    *           Flag indicating if the input message contains a header ID.
    *           
    * @return   The created binding operation element.
    */
   private Element createBindingOperation(ServiceConfig srv, 
                                          SoapOperation operation, 
                                          String        name, 
                                          Boolean       headerID)
   {
      Element elHeader = null;
      if (headerID != null)
      {
         String hid = operation.parent == null ? operation.operation : operation.parent.operation;
         hid = hid + "ID";
         
         elHeader = dom.createElementNS(NS_SOAP, "soap:header");
         elHeader.setAttribute("message", "tns:" + hid);
         elHeader.setAttribute("part", hid);
         elHeader.setAttribute("use", "literal");
         if (headerID)
         {
            // only INPUT requires it
            elHeader.setAttributeNS(NS_WSDL, "wsdl:required", "true");
         }
      }

      Element el = dom.createElementNS(NS_WSDL, "wsdl:operation");
      el.setAttribute("name", name);
      
      Element elSoap = dom.createElementNS(NS_SOAP, "soap:operation");
      String action = soapAction;
      if (appendObjNameToSoapAction)
      {
         action = action + "/" + srv.name;
      }
      elSoap.setAttribute("soapAction", action);
      elSoap.setAttribute("style", "document");
      el.appendChild(elSoap);
      
      Element elInput = dom.createElementNS(NS_WSDL, "wsdl:input");
      if (headerID != null && headerID)
      {
         elInput.appendChild(elHeader);
      }
      Element elInputBody = dom.createElementNS(NS_SOAP, "soap:body");
      elInputBody.setAttribute("use", "literal");
      elInput.appendChild(elInputBody);
      el.appendChild(elInput);

      Element elOutput = dom.createElementNS(NS_WSDL, "wsdl:output");
      if (headerID != null && !headerID)
      {
         elOutput.appendChild(elHeader);
      }
      Element elOutputBody = dom.createElementNS(NS_SOAP, "soap:body");
      elOutputBody.setAttribute("use", "literal");
      elOutput.appendChild(elOutputBody);
      el.appendChild(elOutput);

      Element elFault = dom.createElementNS(NS_WSDL, "wsdl:fault");
      elFault.setAttribute("name", srv.name + "Fault");
      Element elSoapFault = dom.createElementNS(NS_SOAP, "soap:fault");
      elSoapFault.setAttribute("name", srv.name + "Fault");
      elSoapFault.setAttribute("use", "literal");
      elFault.appendChild(elSoapFault);
      el.appendChild(elFault);
      
      return el;
   }
   
   /**
    * Create an operation associated with the WSDL port-type.
    * 
    * @param    srv
    *           The service configuration.
    * @param    name
    *           The operation name.
    * @param    typeName
    *           The operation's type for the input, output or fault messages.
    *           
    * @return   The port-type operation.
    */
   private Element createPortTypeOperation(ServiceConfig srv, String name, String typeName)
   {
      String tnsName = (typeName == null ? srv.name + "_" + name : typeName);

      Element el = dom.createElementNS(NS_WSDL, "wsdl:operation");
      el.setAttribute("name", name);
      
      Element elInput = dom.createElementNS(NS_WSDL, "wsdl:input");
      elInput.setAttribute("message", "tns:" + tnsName);
      el.appendChild(elInput);
      
      Element elOutput = dom.createElementNS(NS_WSDL, "wsdl:output");
      elOutput.setAttribute("message", "tns:" + tnsName + "Response");
      el.appendChild(elOutput);
      
      Element elFault = dom.createElementNS(NS_WSDL, "wsdl:fault");
      elFault.setAttribute("name", srv.name + "Fault");
      elFault.setAttribute("message", "tns:FaultDetailMessage");
      el.appendChild(elFault);

      return el;
   }
   
   /**
    * Create a message associated with a WSDL port-type operation.
    * 
    * @param    srv
    *           The service configuration.
    * @param    name
    *           The message name.
    * @param    element
    *           The overriding name for the 'element' attribute at the message.
    * @param    partName
    *           The 'name' attribute value.  Defaults to 'parameters'.
    *           
    * @return   The XML element for this message.
    */
   private Element createMessage(ServiceConfig srv, String name, String element, String partName)
   {
      Element el = dom.createElementNS(NS_WSDL, "wsdl:message");
      el.setAttribute("name", element == null ? srv.name + "_" + name : name);
      
      Element elPart = dom.createElementNS(NS_WSDL, "wsdl:part");
      elPart.setAttribute("name", partName == null ? "parameters" : partName);
      elPart.setAttribute("element", srv.ns + ":" + (element == null ? name : element));
      el.appendChild(elPart);
      
      return el;
   }
   
   /**
    * Create a table parameter for a SOAP operation.
    * 
    * @param    operation
    *           The SOAP operation.
    * @param    par
    *           The table parameter.
    *           
    * @return   The XML element for this parameter.
    */
   private Element createTableParam(SoapOperation operation, SoapOperationParameter par)
   {
      ServiceConfig srv = services.get(operation.service);

      Element elType = dom.createElement("complexType");
      elType.setAttribute("name", operation.operation + "_" + par.name + "Param");
      Element elSeq = dom.createElement("sequence");
      Element el = dom.createElement("element");
      el.setAttribute("maxOccurs", "unbounded");
      el.setAttribute("minOccurs", "0");
      el.setAttribute("name", par.name + "Row");
      el.setAttribute("type", srv.ns + ":" + operation.operation + "_" + par.name + "Row");
      elSeq.appendChild(el);
      elType.appendChild(elSeq);
      
      return elType;
   }
   
   /**
    * Create the before-image type.
    * 
    * @param    service
    *           The service to which this type belongs.
    * @param    dsname
    *           The dataset name.
    *           
    * @return   The XML element for this before-image type.
    */
   private Element createDataSetBeforeImage(ServiceConfig service, String dsname)
   {
      Element elType = dom.createElement("complexType");
      elType.setAttribute("name", dsname + "Changes");
      elType.setAttribute("prodata:datasetName", dsname);
      elType.setAttribute("prodata:isDsChanges", "true");
      elType.setAttribute("prodata:namespace", targetNamespace + ":" + service.name);
      Element elSeq = dom.createElement("sequence");
      Element elAny = dom.createElement("any");
      elSeq.appendChild(elAny);
      elType.appendChild(elSeq);
      
      return elType;
   }
   
   /**
    * Check if there already exists a dataset with this name.  If it exists, return <code>null</code> so it
    * will not be added again to the schema.  Otherwise, create an unique name for it, in the {@link #datasets}
    * map.
    * 
    * @param    el
    *           The dataset schema element.
    *           
    * @return   The new name or <code>null</code> if it already exists.
    */
   private String checkDataSetName(Element el)
   {
      String dsname = el.getAttribute("name");
      
      Element dsEl = datasets.get(dsname);
      if (dsEl != null)
      {
         if (identicalElements(el, dsEl))
         {
            return null;
         }
         
         int idx = 2;
         while (datasets.containsKey(dsname + idx))
         {
            idx = idx + 1;
         }
         
         dsname = dsname + idx;
      }
      
      datasets.put(dsname, el);
      
      return dsname;
   }
   
   /**
    * Check if these two XML element nodes are syntactically identical (attributes, child nodes).
    * 
    * @param    el1
    *           The first element.
    * @param    el2
    *           The second element.
    *           
    * @return   <code>true</code> if the elements are identical.
    */
   private boolean identicalElements(Element el1, Element el2)
   {
      Map<String, String> attr1 = readAttributes(el1);
      Map<String, String> attr2 = readAttributes(el1);
      if (attr1.size() != attr2.size())
      {
         return false;
      }
      
      Set<String> keys = new HashSet<>();
      keys.addAll(attr1.keySet());
      keys.addAll(attr2.keySet());
      if (keys.size() != attr1.size())
      {
         return false;
      }
      
      for (String key : keys)
      {
         String v1 = attr1.get(key);
         String v2 = attr2.get(key);
         
         if (!v1.equals(v2)) 
         {
            return false;
         }
      }
      
      NodeList nl1 = el1.getChildNodes();
      NodeList nl2 = el2.getChildNodes();
      if (nl1.getLength() != nl2.getLength())
      {
         return false;
      }
      
      for (int i = 0; i < nl1.getLength(); i++)
      {
         Node n1 = nl1.item(i);
         Node n2 = nl2.item(i);
         
         if (n1.getNodeType() == Node.ELEMENT_NODE && n2.getNodeType() == Node.ELEMENT_NODE)
         {
            if (!identicalElements((Element) n1, (Element) n2))
            {
               return false;
            }
         }
         else if (n1.getNodeType() != n2.getNodeType())
         {
            return false;
         }
      }
      
      return true;
   }
   
   /**
    * Read all attributes from the specified node.
    * 
    * @param    el
    *           The XML node.
    *           
    * @return   A map of attribute names and their values.
    */
   private Map<String, String> readAttributes(Element el)
   {
      Map<String, String> res = new HashMap<>();
      
      NamedNodeMap nnm = el.getAttributes();
      for (int i = 0; i < nnm.getLength(); i++)
      {
         Node n = nnm.item(i);
         if (n.getNodeType() == Node.ATTRIBUTE_NODE)
         {
            res.put(n.getNodeName(), n.getNodeValue());
         }
      }
      
      return res;
   }
   
   /**
    * Create a table schema, considering the dataset relations (nested, etc).
    * 
    * @param    operation
    *           The SOAP operation.
    * @param    dataset
    *           The parent dataset.
    * @param    tparam
    *           The table reference.
    *           
    * @return   The root element for this table.
    */
   private Element createTableParam(SoapOperation operation, 
                                    SoapOperationParameter dataset, 
                                    SoapOperationParameter tparam)
   {
      Element elTable = createComplexType(operation, tparam.name, tparam.tableFields.values(), true);
      
      if (tparam.nodeName != null)
      {
         elTable.setAttribute("name", tparam.nodeName);
         elTable.setAttribute("prodata:tableName", tparam.name);
      }
      
      if (tparam.beforeTable != null)
      {
         elTable.setAttribute("prodata:beforeTable", "BI" + tparam.name);
      }
      elTable.setAttribute("maxOccurs", "unbounded");
      elTable.setAttribute("minOccurs", "0");
      
      Element elSeq = (Element) elTable.getElementsByTagName("sequence").item(0);
      // append recursively all child tables...
      List<SoapOperationParameter> childTables = dataset.getChildTables(tparam);
      for (SoapOperationParameter child : childTables)
      {
         Element elChild = createTableParam(operation, dataset, child);
         elSeq.appendChild(elChild);
      }
      
      return elTable;
   }
   
   /**
    * Create a dataset parameter for a SOAP operation.
    * 
    * @param    operation
    *           The SOAP operation.
    * @param    par
    *           The dataset parameter.
    *           
    * @return   The XML element for this parameter.
    */
   private Element createDataSetParam(SoapOperation operation, SoapOperationParameter par)
   {
      Element el = dom.createElement("element");
      String serviceNS = services.get(operation.service).ns;
      
      Element elAppinfo = null;

      // append the dataset-relation(s)
      // 1. to annotation/appinfo - non-PK based relations
      // 2. TODO: as 'keyref' - PK based relations are emitted as children of root 'el' node
      if (par.relations != null)
      {
         for (DataSetRelation relation : par.relations)
         {
            // TODO: skip 'keyref' relations
            
            if (elAppinfo == null)
            {
               Element elAnno = dom.createElement("annotation");
               el.appendChild(elAnno);
               elAppinfo = dom.createElement("appinfo");
               elAnno.appendChild(elAppinfo);
            }
            
            Element elRel = dom.createElementNS(NS_PRODATA, "prodata:relation");
            elAppinfo.appendChild(elRel);
            
            elRel.setAttribute("name", relation.name);
            elRel.setAttributeNS(NS_PRODATA, "prodata:child", relation.child);
            if (relation.nested)
            {
               elRel.setAttributeNS(NS_PRODATA, "prodata:nested", "true");
            }
            elRel.setAttributeNS(NS_PRODATA, "prodata:parent", relation.parent);
            elRel.setAttributeNS(NS_PRODATA, "prodata:relationFields", relation.pairs);
         }
      }
      
      List<Index> uniqueIndexes = new ArrayList<>();
      for (SoapOperationParameter tparam : par.datasetTables)
      {
         if (tparam.tableIndexes == null)
         {
            continue;
         }
         
         for (Index index : tparam.tableIndexes)
         {
            if (index.primary() && !index.unique())
            {
               if (elAppinfo == null)
               {
                  Element elAnno = dom.createElement("annotation");
                  el.appendChild(elAnno);
                  elAppinfo = dom.createElement("appinfo");
                  elAnno.appendChild(elAppinfo);
               }
               
               Element elIndex = dom.createElementNS(NS_PRODATA, "prodata:index");
               elAppinfo.appendChild(elIndex);
               
               // these names are NOT made unique
               elIndex.setAttribute("name", index.legacy());
               elIndex.setAttributeNS(NS_PRODATA, "prodata:primaryIndex", "true");
               
               for (IndexComponent comp : index.components())
               {
                  Element elComp = dom.createElementNS(NS_PRODATA, "prodata:field");
                  elComp.setAttribute("name", comp.legacy());
                  elIndex.appendChild(elComp);
               }

               Element elTable = dom.createElementNS(NS_PRODATA, "prodata:table");
               elTable.setAttribute("name", tparam.name);
               elIndex.appendChild(elTable);
            }
            else
            {
               uniqueIndexes.add(index);
            }
         }
      }
      
      if (par.nodeName != null)
      {
         el.setAttribute("name", par.nodeName); 
         el.setAttribute("prodata:datasetName", par.name);
      }
      else
      {
         el.setAttribute("name", par.name);
      }
      Element elType = dom.createElement("complexType");
      el.appendChild(elType);
      Element elSeq = dom.createElement("sequence");
      
      for (SoapOperationParameter tparam : par.datasetTables)
      {
         // if the table is NESTED or PARENT-ID-RELATION, then skip it
         
         if (par.isNestedTable(tparam))
         {
            continue;
         }
         
         Element elTable = createTableParam(operation, par, tparam);
         elSeq.appendChild(elTable);
      }

      elType.appendChild(elSeq);

      Map<String, AtomicInteger> counters = new HashMap<>();
      for (Index index : uniqueIndexes)
      {
         Element elUnique = dom.createElement("unique");
         // these 'unique' tags must have an unique name
         String indexName = index.legacy();
         
         AtomicInteger counter = counters.computeIfAbsent(indexName, (n) -> new AtomicInteger(0));
         int next = counter.incrementAndGet();
         if (next != 1)
         {
            indexName += "_" + next;
         }
         
         elUnique.setAttribute("name", indexName);
         if (index.primary())
         {
            elUnique.setAttributeNS(NS_PRODATA, "prodata:primaryIndex", "true");
         }
         el.appendChild(elUnique);
         
         Element elSelector = dom.createElement("selector");
         elSelector.setAttribute("xpath", ".//" + serviceNS + ":" + index.legacy());
         elUnique.appendChild(elSelector);
         
         for (IndexComponent comp : index.components())
         {
            Element elField = dom.createElement("field");
            elField.setAttribute("xpath", serviceNS + ":" + comp.legacy());
            elUnique.appendChild(elField);
         }
      }
      
      return el;
   }

   /**
    * Create a complex type to declare the operation's parameters.
    * 
    * @param    operation
    *           The SOAP operation.
    * @param    name
    *           The type's name.
    * @param    parameters
    *           The operation's parameters.
    * @param    asElement
    *           Flag indicating if an <code>element</code> node should be used as parent.
    *           
    * @return   The XML element for this type.
    */
   private Element createComplexType(SoapOperation                      operation, 
                                     String                             name, 
                                     Collection<SoapOperationParameter> parameters,
                                     boolean                            asElement)
   {
      Element parent = null;
      if (asElement)
      {
         Element el = dom.createElement("element");
         el.setAttribute("name", name);
         parent = el;
      }
      
      Element elType = dom.createElement("complexType");
      if (parent != null)
      {
         parent.appendChild(elType);
      }
      else
      {
         elType.setAttribute("name", name);
         parent = elType;
      }
      Element elSeq = dom.createElement("sequence");
      elType.appendChild(elSeq);
      
      if (parameters != null)
      {
         ServiceConfig srv = (operation == null ? null : services.get(operation.service));
         for (SoapOperationParameter par : parameters)
         {
            String type = null;
            String proType = null;
   
            if (par.type == KW_TAB_HAND)
            {
               type = srv.ns + ":TableHandleParam";
               if (!srv.hasTableHandleParam)
               {
                  Element elComplexType = dom.createElement("complexType");
                  elComplexType.setAttribute("name", "TableHandleParam");
                  srv.elSchema.appendChild(elComplexType);
                  
                  Element elSeq2 = dom.createElement("sequence");
                  elComplexType.appendChild(elSeq2);
                  
                  Element elAny = dom.createElement("any");
                  elAny.setAttribute("namespace", "##local");
                  elSeq2.appendChild(elAny);
                  
                  srv.hasTableHandleParam = true;
               }
            }
            else if (par.type == KW_DSET_HND)
            {
               type = srv.ns + ":DataSetHandleParam";
               if (!srv.hasDataSetHandleParam)
               {
                  Element elComplexType = dom.createElement("complexType");
                  elComplexType.setAttribute("name", "DataSetHandleParam");
                  srv.elSchema.appendChild(elComplexType);
                  
                  Element elAnno = dom.createElement("annotation");
                  elComplexType.appendChild(elAnno);
                  Element elDoc = dom.createElement("documentation");
                  elDoc.setTextContent("This is a schema definition for a dataset-handle parameter. " + 
                                       "The first element must be the schema definition for this dataset, " + 
                                       "while the second element will be the serialized dataset.");
                  elAnno.appendChild(elDoc);

                  Element elSeq2 = dom.createElement("sequence");
                  elComplexType.appendChild(elSeq2);
                  
                  Element elAny = dom.createElement("any");
                  elAny.setAttribute("maxOccurs", "2");
                  elAny.setAttribute("minOccurs", "2");
                  elSeq2.appendChild(elAny);

                  srv.hasDataSetHandleParam = true;
               }
            }
            else if (par.type == KW_TABLE)
            {
               // this is a table
               String tname = operation.operation + "_" +  par.name + "Param";
               String rname = operation.operation + "_" +  par.name + "Row";
               type = srv.ns + ":" + tname;
               
               if (tableParams.add(tname))
               {
                  srv.elSchema.appendChild(createComplexType(null, rname, par.tableFields.values(), false));
                  srv.elSchema.appendChild(createTableParam(operation, par));
               }
            }
            else if (par.type == KW_DATASET)
            {
               Element el = createDataSetParam(operation, par);
               String dsname = checkDataSetName(el);
               if (dsname != null)
               {
                  // a new dataset, register it with the schema.
                  if (par.useBeforeImage)
                  {
                     Element elb4 = createDataSetBeforeImage(srv, dsname);
                     elb4.setAttribute("name", dsname + "Changes");
                     srv.elSchema.appendChild(elb4);
                  }

                  srv.elSchema.appendChild(el);
               }
               else
               {
                  dsname = el.getAttribute("name");
               }

               // this is a dataset
               type = srv.ns + ":" + dsname;

               Element elPar = dom.createElement("element");
               if (par.useBeforeImage)
               {
                  elPar.setAttribute("name", dsname);
                  elPar.setAttribute("type", type + "Changes");
               }
               else
               {
                  elPar.setAttribute("ref", type);
               }
               
               elSeq.appendChild(elPar);
               
               continue;
            }
            else
            {
               switch ((int) par.type)
               {
                  case FIELD_BLOB:
                     type = "base64Binary";
                     proType = "blob";
                     break;
   
                  case FIELD_CLOB:
                     proType = "clob";
                  case FUNC_CHAR:
                  case FUNC_LONGCHAR:
                  case FIELD_CHAR:
                  case VAR_CHAR:
                  case VAR_LONGCHAR:
                     type = "string";
                     break;
                     
                  case FUNC_DATE:
                  case FIELD_DATE:
                  case VAR_DATE:
                     type = "date";
                     break;
                     
                  case FIELD_DATETIME:
                     proType = "dateTime";
                  case FUNC_DATETIME:
                  case FUNC_DATETIME_TZ:
                  case FIELD_DATETIME_TZ:
                  case VAR_DATETIME:
                  case VAR_DATETIME_TZ:
                     type = "dateTime";
                     break;
                     
                  case FUNC_INT:
                  case FIELD_INT:
                  case VAR_INT:
                     type = "int"; // signed 32-bit
                     break;
                     
                  case FUNC_INT64:
                  case FUNC_RECID:
                  case FUNC_HANDLE:
                  case FIELD_INT64:
                  case FIELD_RECID:
                  case FIELD_HANDLE:
                  case VAR_INT64:
                  case VAR_RECID:
                  case VAR_HANDLE:
                     type = "long";
                     break;
                     
                  case FUNC_DEC:
                  case FIELD_DEC:
                  case VAR_DEC:
                     type = "decimal";
                     break;
                     
                  case FIELD_LOGICAL:
                  case FUNC_LOGICAL:
                  case VAR_LOGICAL:
                     type = "boolean";
                     break;
                     
                  case FUNC_MEMPTR:
                  case FUNC_ROWID:
                  case FUNC_RAW:
                  case FIELD_ROWID:
                  case FIELD_RAW:
                  case VAR_MEMPTR:
                  case VAR_ROWID:
                  case VAR_RAW:
                     type = "base64Binary";
                     break;
               }
               
               if (type == null)
               {
                  throw new NullPointerException("Unknown XSD type: " + 
                                                 ProgressParser.lookupTokenName((int) par.type));
               }
               
               type = "xsd:" + type;
            }
            
            Element elPar = dom.createElement("element");
            if (par.nodeName != null )
            {
               // this is non-null only for table fields
               elPar.setAttribute("name", par.nodeName);
               elPar.setAttribute("prodata:fieldName", par.name);
            }
            else
            {
               elPar.setAttribute("name", par.name);
            }
            elPar.setAttribute("type", type);
            if (par.nillable)
            {
               elPar.setAttribute("nillable", "true");
            }
            if (proType != null)
            {
               elPar.setAttributeNS(NS_PRODATA, "prodata:dataType", "prodata:" + proType);
            }
            if (par.extent != SourceNameMapper.NO_EXTENT)
            {
               if (par.extent == -1)
               {
                  elPar.setAttribute("maxOccurs", "unbounded");
                  elPar.setAttribute("minOccurs", "0");
               }
               else
               {
                  elPar.setAttribute("maxOccurs", "" + par.extent);
                  elPar.setAttribute("minOccurs", "" + par.extent);
               }
            }
            
            elSeq.appendChild(elPar);
         }
      }
      
      return parent;
   }
   
   /**
    * The configuration for the exported WSDL service.
    */
   private class ServiceConfig
   {
      /** The parent service name. */
      private final String parentService;
      
      /** The service name. */
      private final String name;
      
      /** The service namespace. */
      private final String ns;

      /** The list of declared messages. */
      private List<Element> messages = new ArrayList<>();

      /** The root binding element for this service. */
      private final Element elBinding;
      
      /** The root port-type element for this service. */
      private final Element elPortType;
      
      /** The port elemnet for this service. */
      private final Element elServicePort;

      /** The schema element where to attach the declared service types. */
      private final Element elSchema;
      
      /** Flag indicating if a dataset-handle schema element was generated. */
      private boolean hasDataSetHandleParam = false;

      /** Flag indicating if a table-handle schema element was generated. */
      private boolean hasTableHandleParam = false;
      
      /**
       * Create a new service.
       * 
       * @param    service
       *           The service name.
       * @param    idx
       *           The service index, used to compute the namespace.
       * @param    parentService
       *           The parent service name.
       */
      public ServiceConfig(String service, int idx, String parentService)
      {
         this.name = service;
         this.parentService = parentService;
         this.ns = "S" + idx;

         // create schema node for the types
         elSchema = dom.createElement("schema");
         elSchema.setAttribute("elementFormDefault", "qualified");
         elSchema.setAttribute("targetNamespace", targetNamespace + ":" + name);
         elSchema.setAttribute("xmlns", "http://www.w3.org/2001/XMLSchema");
         elTypes.appendChild(elSchema);
         
         // create the portType node
         elPortType = dom.createElementNS(NS_WSDL, "wsdl:portType");
         elPortType.setAttribute("name", name + portTypeBindingSuffix);
         
         // create the binding node
         elBinding = dom.createElementNS(NS_WSDL, "wsdl:binding");
         elBinding.setAttribute("name", name + portTypeBindingSuffix);
         elBinding.setAttribute("type", "tns:" + name + portTypeBindingSuffix);
         Element elSoapBinding = dom.createElementNS(NS_SOAP, "soap:binding");
         elSoapBinding.setAttribute("style", "document");
         elSoapBinding.setAttribute("transport", "http://schemas.xmlsoap.org/soap/http");
         elBinding.appendChild(elSoapBinding);
         
         // create port node
         elServicePort = dom.createElementNS(NS_WSDL, "wsdl:port");
         elServicePort.setAttribute("name", name + portTypeBindingSuffix);
         elServicePort.setAttribute("binding", "tns:" + name + portTypeBindingSuffix);
         
         Element elDoc = dom.createElementNS(NS_WSDL, "wsdl:documentation");
         elDoc.setTextContent(helpString);
         elServicePort.appendChild(elDoc);
         
         Element elAddress = dom.createElementNS(NS_SOAP, "soap:address");
         elAddress.setAttribute("location", endpoint);
         elServicePort.appendChild(elAddress);
      }
   }
}