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