# WsdlConfig.java

    /*
    ** Module   : WsdlConfig.java
    ** Abstract : Helper to manage a WSDL document.
    **
    ** Copyright (c) 2020-2024, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- ---------------------------------------Description----------------------------------------
    ** 001 CA  20200518 First version.
    **     CA  20200528 Fixes for extent parameters, BEFORE-TABLE and error management.
    ** 002 CA  20211012 Fixed generated WSDL and reworked SOAP support to rely on namespace, when resolving an
    **                  operation.
    **     CA  20211013 DATASET parameters have the table table indexes, too, at the WSDL schema.
    **     CA  20211015 Avoid a NPE for TABLE/DATASET-HANDLE params until the runtime is in place.
    **     CA  20211112 Refactored to allow transfer of the DATASET or TABLE via XML.  Added support for
    **                  DATASET-HANDLE and TABLE-HANDLE.
    ** 003 SBI 20230216 Fixed BaseSchemaType.buildParameter(..) because defaultValue() was added to LegacyServiceParameter.
    ** 004 TJD 20240214 Removed dependency on castor
    ** 005 RNC 20241030 Fixed a wrong variable used in the inner loop from readIndexes().
    */

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

    package com.goldencode.p2j.soap;

    import java.lang.annotation.*;
    import java.util.*;

    import javax.wsdl.*;
    import javax.wsdl.extensions.*;
    import javax.wsdl.extensions.schema.*;
    import javax.xml.namespace.*;

    //import org.exolab.castor.xml.*;
    import org.w3c.dom.*;

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

    /**
     * Provide helper APIs to access the SOAP operations and others, from a WSDL definition.
     */
    class WsdlConfig
    {
       /** The WSDL definition. */
       private final Definition definition;

       /** All the defined schemas in the WSDL document. */
       private final Map<QName, WSDLSchema> schemas = new HashMap<>();

       /**
        * Create new instance.  This will load all the schemas from the WSDL document into {@link #schemas}.
        *
        * @param    definition
        *           The WSDL definition.
        */
       public WsdlConfig(Definition definition)
       {
          this.definition = definition;

          // create a mapping of all complex types

          List<ExtensibilityElement> lext = definition.getTypes().getExtensibilityElements();
          Iterator<ExtensibilityElement> iter = lext.iterator();
          while (iter.hasNext())
          {
             ExtensibilityElement exEl = iter.next();

             if (exEl instanceof Schema)
             {
                Schema schema = (Schema) exEl;
                WSDLSchema wsdlSchema = new WSDLSchema(schema);
                schemas.put(wsdlSchema.qname, wsdlSchema);
             }
          }
       }

       /**
        * Remove the namespace from the specified name.
        *
        * @param    name
        *           The qualified name.
        *
        * @return   The local part in this qname.
        */
       private static String removeNamespace(String name)
       {
          if (name.indexOf(":") >= 0)
          {
             name = name.substring(name.lastIndexOf(":") + 1);
          }

          return name;
       }

       /**
        * Get the WSDL definition.
        *
        * @return   The {@link #definition}.
        */
       public Definition getDefinition()
       {
          return definition;
       }

       /**
        * Resolve the input and output parameters for the specified operation.
        *
        * @param    op
        *           The SOAP operation.
        *
        * @return   The parameter mappings.
        */
       public Map<String, BaseSchemaType> resolveParameters(Operation op)
       {
          Map<String, BaseSchemaType> res = new HashMap<>();

          Input input = op.getInput();
          Output output = op.getOutput();
          if (input != null && input.getMessage() != null)
          {
             res.putAll(resolveParameters(input.getMessage()));
          }
          if (output != null && output.getMessage() != null)
          {
             res.putAll(resolveParameters(output.getMessage()));
          }

          return res;
       }

       /**
        * From the given legacy signature, resolve the service parameters.
        *
        * @param    ls
        *           The legacy signature associated with the 4GL target (internal entry, program, class, method).
        * @param    op
        *           The WSDL operation.
        *
        * @return   The list of service parameters.
        */
       public LegacyServiceParameter[] resolveParameters(LegacySignature ls, Operation op)
       {
          Map<String, Integer> parOrdinals = new HashMap<>();
          Map<String, String> parTypes = new HashMap<>();
          Map<String, String> parModes = new HashMap<>();
          int ordinal = 1;
          for (LegacyParameter lsp : ls.parameters())
          {
             String key = lsp.name();
             if (key.isEmpty())
             {
                if (lsp.type().equalsIgnoreCase("TABLE"))
                {
                   key = lsp.table();
                }
                else if (lsp.type().equalsIgnoreCase("DATASET"))
                {
                   key = lsp.dataset();
                }
             }

             key = key.toLowerCase();
             parOrdinals.put(key, ordinal);
             parTypes.put(key, lsp.type());
             parModes.put(key, lsp.mode()); // string representation, INPUT/OUTPUT/INPUT-OUTPUT

             ordinal = ordinal + 1;
          }

          Input input = op.getInput();
          Output output = op.getOutput();
          List<LegacyServiceParameter> params = new ArrayList<>();
          if (input != null && input.getMessage() != null)
          {
             params.addAll(resolveParameters(input.getMessage(), true, parOrdinals, parTypes, parModes));
          }
          if (output != null && output.getMessage() != null)
          {
             params.addAll(resolveParameters(output.getMessage(), false, parOrdinals, parTypes, parModes));
          }

          Collections.sort(params, new Comparator<LegacyServiceParameter>()
          {
             @Override
             public int compare(LegacyServiceParameter lsp1, LegacyServiceParameter lsp2)
             {
                return lsp1.ordinal() - lsp2.ordinal();
             }
          });

          return params.toArray(new LegacyServiceParameter[params.size()]);
       }

       /**
        * Resolve the WSDL parameters for the specified WSDL message.
        *
        * @param    msg
        *           The message (input or output, for an operation).  Must have a <code>parameters</code> attribute.
        *
        * @return   The parameter mapping for this message.
        */
       public Map<String, BaseSchemaType> resolveParameters(Message msg)
       {
          WSDLSchema schema = resolveSchema(msg, "parameters");

          Part part = msg.getPart("parameters");
          String typeName = part.getElementName().getLocalPart();
          ComplexSchemaType type = schema.types.get(typeName);
          if (type == null)
          {
             return Collections.emptyMap();
          }

          Map<String, BaseSchemaType> res = new HashMap<>();
          for (BaseSchemaType bst : type.params)
          {
             res.put(bst.name, bst);
          }

          return res;
       }

       /**
        * Resolve the schema for the specified message.
        *
        * @param    msg
        *           The message (input or output, for an operation).  Must have a <code>parameters</code> attribute.
        * @param    partName
        *           The part defining the parameters.
        *
        * @return   The schema configuration for this message's parameters.
        */
       public WSDLSchema resolveSchema(Message msg, String partName)
       {
          Part part = msg.getPart(partName);
          String schemaNS = part.getElementName().getNamespaceURI();
          String schemaLocal = removeNamespace(schemaNS);
          schemaNS = schemaNS.substring(0, schemaNS.lastIndexOf(":"));

          QName schemaQName = new QName(schemaNS, schemaLocal);
          return schemas.get(schemaQName);
       }

       /**
        * For the specified message, resolve the service's input or output parameters.
        *
        * @param    msg
        *           The WSDL message.  Must have a <code>parameters</code> part.
        * @param    input
        *           Flag indicating if this is needed for input or output parameters.
        * @param    ordinals
        *           The parameter's mapping to their position at the 4GL definition.
        * @param    types
        *           The parameter's mapping to their legacy type.
        * @param    modes
        *           The parameter's mapping to their mode (INPUT/OUTPUT/INPUT-OUTPUT).
        *
        * @return   The service parameters for this message.
        */
       private List<LegacyServiceParameter> resolveParameters(Message              msg,
                                                              boolean              input,
                                                              Map<String, Integer> ordinals,
                                                              Map<String, String>  types,
                                                              Map<String, String>  modes)
       {
          WSDLSchema schema = resolveSchema(msg, "parameters");

          Part part = msg.getPart("parameters");
          String typeName = part.getElementName().getLocalPart();
          ComplexSchemaType type = schema.types.get(typeName);
          if (type == null)
          {
             return Collections.emptyList();
          }

          List<LegacyServiceParameter> res = new ArrayList<>();
          res.addAll(type.buildParameters(input, "result", ordinals, types, modes));
          return res;
       }

       /**
        * Base definition for a custom type defined in the WSDL schema.
        */
       static class BaseSchemaType
       {
          /** The type name. */
          protected final String name;

          /** The XML name for this type. */
          protected final String xmlName;

          /** Flag indicating if the null values must be emitted. */
          protected final boolean nillable;

          /** The associated XST type. */
          protected final String xsdType;

          /** The associated legacy type. */
          protected final String proDataType;

          /** The parameter's extent. */
          protected final int extent;

          /**
           * Create a new instance.
           *
           * @param    name
           *           The table name.
           * @param    xmlName
           *           The XML node name.
           * @param    nillable
           *           Flag indicating if the elements can be <code>null</code>.
           * @param    xsdType
           *           The associated XST type.
           * @param    proDataType
           *           The associated legacy type.
           * @param    extent
           *           The parameter's extent.
           */
          public BaseSchemaType(String name, String xmlName, boolean nillable, String xsdType, String proDataType, int extent)
          {
             this.name = name;
             this.xmlName = xmlName;
             this.nillable = nillable;
             this.xsdType = xsdType;
             this.proDataType = proDataType;
             this.extent = extent;
          }

          /**
           * Build the associated service parameter for this type.
           *
           * @return   See above.
           */
          protected LegacyServiceParameter buildParameter()
          {
             return buildParameter(-1, false, false, false, null, SourceNameMapper.NO_EXTENT, false);
          }

          /**
           * Build the associated service parameter for this type.
           *
           * @param    ordinal
           *           The parameter's index at the definition.
           * @param    input
           *           Flag indicating this is an INPUT parameter.
           * @param    output
           *           Flag indicating this is an OUTPUT parameter.
           * @param    returnValue
           *           Flag indicating this parameter is for the return type.
           * @param    type
           *           The parameter's legacy type.
           * @param    extent
           *           The parameter's extent.
           * @param    useB4Image
           *           Flag indicating if the before-image must be included.
           *
           * @return   See above.
           */
          protected final LegacyServiceParameter buildParameter(int     ordinal,
                                                                boolean input,
                                                                boolean output,
                                                                boolean returnValue,
                                                                String  type,
                                                                int     extent,
                                                                boolean useB4Image)
          {
             return new LegacyServiceParameter()
             {
                @Override
                public Class<? extends Annotation> annotationType()
                {
                   return LegacyServiceParameter.class;
                }

                public int ordinal()
                {
                   return ordinal;
                };

                @Override
                public String name()
                {
                   return name;
                }

                @Override
                public String type()
                {
                   return type == null ? resolveType() : type.toLowerCase();
                };

                @Override
                public boolean allowUnknown()
                {
                   return nillable;
                }

                @Override
                public boolean returnValue()
                {
                   return returnValue;
                }

                @Override
                public boolean output()
                {
                   return output;
                }

                @Override
                public boolean input()
                {
                   return input;
                }

                @Override
                public String target()
                {
                   return output ? "SOAP:" + name : null; // this is for OUTPUT
                }

                @Override
                public String source()
                {
                   return input ? "SOAP:" + name : null; // this is for INPUT
                }

                @Override
                public int extent()
                {
                   return extent;
                }

                @Override
                public String defaultValue()
                {
                   return "";
                }
             };
          }

          /**
           * Resolve the {@link BaseDataType} type name for the {@link #xsdType}.
           *
           * @return   The resolved type.
           */
          protected String resolveType()
          {
             switch (xsdType)
             {
                case "xsd:string":
                   return "prodata:clob".equals(proDataType) ? "clob" : "character";

                case "xsd:int":
                   return "integer";
                case "xsd:long":
                   return "int64";
                case "xsd:decimal":
                   return "decimal";

                case "xsd:date":
                   return "date";
                case "xsd:dateTime":
                   return "datetime";

                case "xsd:boolean":
                   return "logical";

                case "xsd:base64Binary":
                   return "prodata:blob".equals(proDataType) ? "blob" : "memptr";
             }

             throw new RuntimeException("Unsupported XSD type: " + xsdType);
          }
       }

       /**
        * Definition of a type associated with a DATASET.
        */
       static class DataSetSchemaType
       extends BaseSchemaType
       {
          /** The list of tables in this dataset. */
          protected final List<TableSchemaType> tables;

          /** Flag indicating the 'before-image' must be written. */
          protected boolean useBeforeImage = false;

          /**
           * Create a new instance.
           *
           * @param    name
           *           The dataset name.
           * @param    xmlName
           *           The XML node name.
           * @param    tables
           *           The table list.
           * @param    useBeforeImage
           *           Flag indicating the 'before-image' must be written.
           */
          public DataSetSchemaType(String name, String xmlName, List<TableSchemaType> tables, boolean useBeforeImage)
          {
             super(name, xmlName, false, "DATASET", null, SourceNameMapper.NO_EXTENT);

             this.tables = tables;
             this.useBeforeImage = useBeforeImage;
          }

          /**
           * Get the service parameter type.
           *
           * @return   Always <code>DATASET</code>.
           */
          protected String resolveType()
          {
             return "DATASET";
          }
       }

       /**
        * Definition of a type associated with a TABLE.
        */
       static class TableSchemaType
       extends BaseSchemaType
       {
          /** The table fields, as defined in the WSDL. */
          protected final List<BaseSchemaType> fields;

          /** The list of indexes for this table. */
          protected final List<Index> indexes;

          /** The before-table name. */
          protected final String beforeTable;

          /**
           * Create a new table schema with the specified name.
           *
           * @param    name
           *           The table name.
           */
          public TableSchemaType(String name)
          {
             this(name, name, false, null, null, null);
          }

          /**
           * Create a new instance.
           *
           * @param    name
           *           The table name.
           * @param    xmlName
           *           The XML node name.
           * @param    nillable
           *           Flag indicating if the elements can be <code>null</code>.
           * @param    fields
           *           The table fields, as defined at the schema.
           */
          public TableSchemaType(String               name,
                                 String               xmlName,
                                 boolean              nillable,
                                 List<BaseSchemaType> fields,
                                 List<Index>          indexes,
                                 String               beforeTable)
          {
             super(name, xmlName, nillable, "TABLE", null, SourceNameMapper.NO_EXTENT);

             this.fields = fields;
             this.indexes = indexes;
             this.beforeTable = beforeTable;
          }

          /**
           * Get the service parameter type.
           *
           * @return   Always <code>DATASET</code>.
           */
          protected String resolveType()
          {
             return "TABLE";
          }

          /**
           * Get a string representation of the indexes in this table.
           *
           * @return   The string representation of the indexes.
           */
          public String getIndexes()
          {
             String res = "";
             // "[primeUniqueFlag,primeFld1[,primeFldn]...:primeIdxName.][uniqueIdxfld1[,uniqueIdxfldn]...:uniqueIdxName.]..."
             for (Index index : indexes)
             {
                if (index.primary())
                {
                   res = res + (index.unique() ? "1" : "0") + ",";
                }

                for (int i = 0; i < index.components().length; i++)
                {
                   IndexComponent comp = index.components()[i];

                   res = res + comp.legacy() + (i == index.components().length - 1 ? "" : ",");
                }

                res = res + ":" + index.legacy() + ".";
             }

             return res;
          }
       }

       /**
        * A loaded schema configuration from a WSDL file.
        */
       static class WSDLSchema
       {
          /** A mapping of all the schema elements defining a type, by their name. */
          private final Map<String, Element> allTypes = new HashMap<>();

          /** A mapping of all the schema complexType nodes, by their name. */
          private final Map<String, Element> allComplexTypes = new HashMap<>();

          /** A mapping of all loaded schema types, by their name. */
          private final Map<String, ComplexSchemaType> types = new HashMap<>();

          /** The name of this schema. */
          private final QName qname;

          /** The target namespace. */
          private final String targetNamespace;

          /**
           * Load and initialize this instance from the specified schema.
           *
           * @param    schema
           *           The schema definition.
           */
          public WSDLSchema(Schema schema)
          {
             Element schemaEl = schema.getElement();
             this.targetNamespace = schemaEl.getAttribute("targetNamespace");
             String localName = removeNamespace(targetNamespace);
             String tns = targetNamespace.substring(0, targetNamespace.lastIndexOf(":"));
             this.qname = new QName(tns, localName);

             List<Element> schemaElements = new ArrayList<>();
             List<Element> schemaComplexElements = new ArrayList<>();

             NodeList schemanl = schemaEl.getChildNodes();
             for (int i = 0; i < schemanl.getLength(); i++)
             {
                Node n = schemanl.item(i);

                if (n.getNodeType() == Node.ELEMENT_NODE)
                {
                   if ("element".equals(n.getLocalName()))
                   {
                      schemaElements.add((Element) n);
                   }
                   else if ("complexType".equals(n.getLocalName()))
                   {
                      schemaComplexElements.add((Element) n);
                   }
                }
             }

             // save all types first, to solve references later
             schemaElements.forEach(el -> allTypes.put(el.getAttribute("name"), el));
             schemaComplexElements.forEach(el -> allComplexTypes.put(el.getAttribute("name"), el));

             schemaElements.forEach(el ->
             {
                NodeList snl = el.getElementsByTagName("element");
                if (snl.getLength() == 0 || el.getElementsByTagName("complexType").getLength() > 1)
                {
                   return;
                }

                String elName = removeNamespace(el.getAttribute("name"));
                if (elName.endsWith("Response"))
                {
                   elName = elName.substring(0, elName.length() - "Response".length());
                }

                List<BaseSchemaType> params = new ArrayList<>();
                for (int j = 0; j < snl.getLength(); j++)
                {
                   Element childEl = (Element) snl.item(j);

                   if (!childEl.getAttribute("ref").isEmpty())
                   {
                      params.add(buildDataSetType(childEl));
                   }
                   else if (!childEl.getAttribute("type").startsWith("xsd"))
                   {
                      String type = removeNamespace(childEl.getAttribute("type"));
                      if (allComplexTypes.containsKey(type))
                      {
                         if ("TableHandleParam".equals(type))
                         {
                            String tableName = childEl.getAttribute("name");
                            TableSchemaType dsHandleType = new TableSchemaType(tableName);
                            params.add(dsHandleType);
                         }
                         else if ("DataSetHandleParam".equals(type))
                         {
                            String dsName = childEl.getAttribute("name");
                            DataSetSchemaType dsHandleType = new DataSetSchemaType(dsName, dsName, null, false);
                            params.add(dsHandleType);
                         }
                         else if (type.startsWith(elName))
                         {
                            // table types are prefixed with the operation name
                            TableSchemaType tableType = buildTableType(childEl);
                            params.add(tableType);
                         }
                         else
                         {
                            // this is a dataset...
                            DataSetSchemaType dstype = buildDataSetType(childEl);
                            params.add(dstype);
                         }
                      }
                      else
                      {
                         params.add(buildTableType(childEl));
                      }
                   }
                   else
                   {
                      params.add(buildBaseType(childEl, null));
                   }
                }

                if (!params.isEmpty())
                {
                   types.put(el.getAttribute("name"), new ComplexSchemaType(el.getAttribute("name"), params));
                }
             });
          }

          /**
           * Get the target namespace from this schema.
           *
           * @return   See above.
           */
          public String getTargetNamespace()
          {
             return targetNamespace;
          }

          /**
           * Read all indexes from this dataset schema.
           *
           * @param    dsType
           *           The dataset schema type element.
           * @param    indexTag
           *           The element tag for the indexes.
           * @param    tableTag
           *           The element tag for the index table.
           * @param    fieldTag
           *           The element tag for the index components (fields).
           * @param    nameAttr
           *           The attribute holding the name at the table and field element.
           *
           * @return   The map of indexes, per table.
           */
          private Map<String, List<Index>> readIndexes(Element dsType,
                                                       String  indexTag,
                                                       String  tableTag,
                                                       String  fieldTag,
                                                       String  nameAttr)
          {
             Map<String, List<Index>> indexes = new HashMap<>();

             NodeList primarynl = dsType.getElementsByTagName(indexTag);
             for (int i = 0; i < primarynl.getLength(); i++)
             {
                Element elIndex = (Element) primarynl.item(i);
                String indexName = elIndex.getAttribute("name");
                Element elTable = (Element) elIndex.getElementsByTagName(tableTag).item(0);
                String tableName = removeNamespace(elTable.getAttribute(nameAttr));
                boolean primary = "true".equals(elIndex.getAttribute("prodata:primaryIndex"));

                NodeList fieldnl = elIndex.getElementsByTagName(fieldTag);
                IndexComponent[] indexFields = new IndexComponent[fieldnl.getLength()];
                for (int j = 0; j < fieldnl.getLength(); j++)
                {
                   Element elField = (Element) fieldnl.item(j);
                   String fieldName = removeNamespace(elField.getAttribute(nameAttr));

                   IndexComponent field = new IndexComponent()
                   {
                      @Override
                      public Class<? extends Annotation> annotationType()
                      {
                         return IndexComponent.class;
                      }

                      @Override
                      public String name()
                      {
                         return fieldName;
                      }

                      @Override
                      public String legacy()
                      {
                         return fieldName;
                      }

                      @Override
                      public int extent()
                      {
                         return SourceNameMapper.NO_EXTENT;
                      }

                      @Override
                      public boolean descending()
                      {
                         return false; // the WSDL does not record ascending/descending state.
                      }
                   };

                   indexFields[j]  = field;
                }

                Index index = new Index()
                {
                   @Override
                   public Class<? extends Annotation> annotationType()
                   {
                      return Index.class;
                   }

                   @Override
                   public String name()
                   {
                      return indexName;
                   }

                   @Override
                   public String legacy()
                   {
                      return indexName;
                   }

                   @Override
                   public String wordtablename()
                   {
                      return null;
                   }

                   @Override
                   public boolean primary()
                   {
                      return primary;
                   }

                   @Override
                   public boolean unique()
                   {
                      return true;
                   }

                   @Override
                   public boolean word()
                   {
                      return false;
                   }

                   @Override
                   public IndexComponent[] components()
                   {
                      return indexFields;
                   }
                };

                List<Index> tableIndexes = indexes.computeIfAbsent(tableName, (n) -> new ArrayList<>());
                tableIndexes.add(index);
             }

             return indexes;
          }

          /**
           * Build a {@link DataSetSchemaType} from the specified schema element.
           *
           * @param    el
           *           The schema element.
           *
           * @return   A dataset type.
           */
          private DataSetSchemaType buildDataSetType(Element el)
          {
             List<TableSchemaType> tables = new ArrayList<>();
             String type = removeNamespace(el.getAttribute("type"));

             Element dsType = allComplexTypes.get(type);
             boolean useBeforeImage = dsType != null;

             String dsXmlName;
             if (useBeforeImage)
             {
                dsXmlName = type.substring(0, type.length() - "Changes".length());
             }
             else
             {
                dsXmlName = removeNamespace(el.getAttribute("ref"));
             }
             dsType = allTypes.get(dsXmlName);

             String dsName = dsXmlName;
             if (dsType.hasAttribute("prodata:datasetName"))
             {
                dsName = dsType.getAttribute("prodata:datasetName");
             }

             // gather all indexes for this dataset and save them by the table
             Map<String, List<Index>> indexes = new HashMap<>();
             // the primary indexes first (so they are first in the list)
             indexes.putAll(readIndexes(dsType, "prodata:index", "prodata:table", "prodata:field", "name"));
             // the unique indexes
             indexes.putAll(readIndexes(dsType, "unique", "selector", "field", "xpath"));

             Element elDsSeq = (Element) dsType.getElementsByTagName("sequence").item(0);
             NodeList children = elDsSeq.getChildNodes();
             for (int k = 0; k < children.getLength(); k++)
             {
                Node n = children.item(k);
                if (!(n instanceof Element))
                {
                   continue;
                }

                Element elTables = (Element) n;
                if (!elTables.getNodeName().equals("element"))
                {
                   continue;
                }

                NodeList tablenl = elTables.getElementsByTagName("complexType");
                for (int i = 0; i < tablenl.getLength(); i++)
                {
                   Element elTableType = (Element) tablenl.item(i);
                   Element elTable = (Element) elTableType.getParentNode();

                   List<BaseSchemaType> fields = new ArrayList<>();
                   // sequence children
                   NodeList rownl = elTableType.getElementsByTagName("sequence").item(0).getChildNodes();
                   for (int j = 0; j < rownl.getLength(); j++)
                   {
                      if (rownl.item(j) instanceof Element)
                      {
                         Element childEl = (Element) rownl.item(j);
                         if (childEl.getElementsByTagName("complexType").getLength() == 0)
                         {
                            fields.add(buildBaseType(childEl, "prodata:fieldName"));
                         }
                      }
                   }

                   String beforeTable = elTable.getAttribute("prodata:beforeTable");
                   String tableName = elTable.getAttribute("name");
                   List<Index> tableIndexes = indexes.get(tableName);

                   String xmlName = tableName;
                   if (elTable.hasAttribute("prodata:tableName"))
                   {
                      tableName = elTable.getAttribute("prodata:tableName");
                   }
                   tables.add(new TableSchemaType(tableName, xmlName, false, fields, tableIndexes, beforeTable));
                }
             }

             // the DATASET schema at the WSDL is no longer used to parse the requester's DATASET, the payload
             // is transferred directly as XML to the appserver, which will parse it via READ-XML.

             return new DataSetSchemaType(dsName, dsXmlName, tables, useBeforeImage);
          }

          /**
           * Build a {@link TableSchemaType} from the specified schema element.
           *
           * @param    el
           *           The schema element.
           *
           * @return   A table type.
           */
          private TableSchemaType buildTableType(Element el)
          {
             List<BaseSchemaType> fields = new ArrayList<>();

             String tableType = removeNamespace(el.getAttribute("type"));
             Element elTableParam = allComplexTypes.get(tableType);
             Element elSeqParam = (Element) elTableParam.getElementsByTagName("element").item(0);
             String tableRowName = elSeqParam.getAttribute("name");
             String tableRowType = removeNamespace(elSeqParam.getAttribute("type"));

             Element elTableRow = allComplexTypes.get(tableRowType);
             NodeList rownl = elTableRow.getElementsByTagName("element");
             for (int j = 0; j < rownl.getLength(); j++)
             {
                Element childEl = (Element) rownl.item(j);
                fields.add(buildBaseType(childEl, "prodata:fieldName"));
             }

             String tableName = el.getAttribute("name");
             return new TableSchemaType(tableName,
                                        tableName,
                                        "true".equals(el.getAttribute("nillable")),
                                        fields,
                                        null, // no indexes are in the WSDL for non-dataset tables
                                        null);
          }

          /**
           * Build a base type from the specified schema element.
           *
           * @param    el
           *           The schema element.
           * @param    legacyAttr
           *           An attribute from which to resolve the legacy name.
           *
           * @return   A base type.
           */
          private BaseSchemaType buildBaseType(Element el, String legacyAttr)
          {
             boolean isExtent = el.getAttribute("minOccurs").length() > 0 &&
                                el.getAttribute("maxOccurs").length() > 0;
             int extent = SourceNameMapper.NO_EXTENT;
             if (isExtent)
             {
                if ("0".equals(el.getAttribute("minOccurs")))
                {
                   extent = SourceNameMapper.DYNAMIC_EXTENT;
                }
                else
                {
                   extent = Integer.parseInt(el.getAttribute("maxOccurs"));
                }
             }

             String xmlName = el.getAttribute("name");
             String legacyName = xmlName;
             if (legacyAttr != null && el.hasAttribute(legacyAttr))
             {
                legacyName = el.getAttribute(legacyAttr);
             }

             return new BaseSchemaType(legacyName,
                                       xmlName,
                                       "true".equals(el.getAttribute("nillable")),
                                       el.getAttribute("type"),
                                       el.getAttribute("prodata:dataType"),
                                       extent);
          }
       }

       /**
        * Defines a complex schema type.
        */
       private static class ComplexSchemaType
       extends BaseSchemaType
       {
          /** The parameters for this complex type. */
          protected final List<BaseSchemaType> params;

          /**
           * Create a new instance.
           *
           * @param    name
           *           The complex type name.
           * @param    params
           *           The list of parameters.
           */
          public ComplexSchemaType(String name, List<BaseSchemaType> params)
          {
             super(name, name, false, null, null, SourceNameMapper.NO_EXTENT);

             this.params = params;
          }

          /**
           * Resolve the complex type's parameters as service parameters.
           *
           * @param    input
           *           Flag indicating if this is needed for input or output parameters.
           * @param    retName
           *           The name of the <code>return</code> parameter.
           * @param    ordinals
           *           The parameter's mapping to their position at the 4GL definition.
           * @param    types
           *           The parameter's mapping to their legacy type.
           * @param    modes
           *           The parameter's mapping to their mode (INPUT/OUTPUT/INPUT-OUTPUT).
           *
           * @return   The service parameters for this message.
           */
          public List<LegacyServiceParameter> buildParameters(boolean              input,
                                                              String               retName,
                                                              Map<String, Integer> ordinals,
                                                              Map<String, String>  types,
                                                              Map<String, String>  modes)
          {
             List<LegacyServiceParameter> res = new ArrayList<>();

             int idx = 0;
             for (BaseSchemaType ptype : params)
             {
                boolean returnValue = !input && retName.equalsIgnoreCase(ptype.name);
                int ordinal = returnValue ? 0 : ordinals.get(ptype.name.toLowerCase());
                String type = types.get(ptype.name.toLowerCase());
                String mode = modes.get(ptype.name.toLowerCase());

                if ("INPUT-OUTPUT".equals(mode) && !input)
                {
                   // these are reported for the INPUT params
                   continue;
                }
                boolean pinput = input || "INPUT-OUTPUT".equals(mode);
                boolean poutput = !input || "INPUT-OUTPUT".equals(mode);
                boolean useBeforeImage = ptype instanceof DataSetSchemaType &&
                                         ((DataSetSchemaType) ptype).useBeforeImage;
                if ("HANDLE".equalsIgnoreCase(type) && ptype instanceof DataSetSchemaType)
                {
                   type = "DATASET-HANDLE";
                }
                else if ("HANDLE".equalsIgnoreCase(type) && ptype instanceof TableSchemaType)
                {
                   type = "TABLE-HANDLE";
                }

                LegacyServiceParameter lsp = ptype.buildParameter(ordinal,
                                                                  pinput,
                                                                  poutput,
                                                                  returnValue,
                                                                  type,
                                                                  ptype.extent,
                                                                  useBeforeImage);
                if (returnValue)
                {
                   res.add(0, lsp);
                }
                else
                {
                   res.add(lsp);
                }

                idx = idx + 1;
             }

             return res;
          }
       }
    }