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