# SoapOperationParameter.java

    /*
    ** Module   : SoapOperationParameter.java
    ** Abstract : Definition of a parameter used for a SOAP service exported via .wsm files.
    **
    ** Copyright (c) 2020-2023, 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.  Added BEFORE-TABLE support.
    ** 002 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).
    ** 003 OM  20230115 Replaced absolutePath(), relativePath(), upPath() and downPath() with faster
    **                  versions, based on node types rather on string paths.
    */

    /*
    ** 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.lang.annotation.*;
    import java.util.*;

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

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

    /**
     * Definition of a SOAP parameter exported via a .wsm file.
     * <p>
     * The data is read from the actual 4GL target (external program, internal entry, class or method).
     */
    class SoapOperationParameter
    {
       /** The legacy parameter name. */
       final String name;

       /** The parameter token type. */
       final long type;

       /** The parameter mode (a token type). */
       final long mode;

       /** Flag indicating if this parameter will be returned by the SOAP request, if is unknown. */
       final boolean nillable;

       /** The extent value (-2 for no extent, -1 for dynamic extent, the extent value otherwise). */
       final int extent;

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

       /** The name to be used at the XML node (for datasets). */
       String nodeName; // TODO: NAMESPACE-PREFIX, NAMESPACE-URI

       /** List of table fields, if this is a TABLE parameter. */
       Map<String, SoapOperationParameter> tableFields = null;

       /** List of indexes for this table. */
       List<Index> tableIndexes = null;

       /** Relation list in case of datasets. */
       List<DataSetRelation> relations = null;

       /** List of table fields, if this is a DATASET parameter. */
       List<SoapOperationParameter> datasetTables = null;

       /** The before-table name. */
       String beforeTable = null;

       /**
        * Create a new instance.
        *
        * @param    name
        *           The legacy parameter name.
        * @param    type
        *           The parameter token type.
        * @param    mode
        *           The parameter mode (a token type).
        * @param    nillable
        *           Flag indicating if this parameter will be returned by the SOAP request, if is unknown.
        */
       public SoapOperationParameter(String name, long type, long mode, boolean nillable)
       {
          this(name, type, mode, nillable, SourceNameMapper.NO_EXTENT);
       }

       /**
        * Create a new instance.
        *
        * @param    name
        *           The legacy parameter name.
        * @param    type
        *           The parameter token type.
        * @param    mode
        *           The parameter mode (a token type).
        * @param    nillable
        *           Flag indicating if this parameter will be returned by the SOAP request, if is unknown.
        * @param    useBeforeImage
        *           Flag indicating if before-image is used for a dataset.
        */
       public SoapOperationParameter(String name, long type, long mode, boolean nillable, boolean useBeforeImage)
       {
          this.name = name;
          this.type = type;
          this.mode = mode;
          this.nillable = nillable;
          this.extent = SourceNameMapper.NO_EXTENT;
          this.useBeforeImage = useBeforeImage;
       }

       /**
        * Create a new instance.
        *
        * @param    name
        *           The legacy parameter name.
        * @param    type
        *           The parameter token type.
        * @param    mode
        *           The parameter mode (a token type).
        * @param    nillable
        *           Flag indicating if this parameter will be returned by the SOAP request, if is unknown.
        * @param    extent
        *           The extent value (-2 for no extent, -1 for dynamic extent, the extent value otherwise).
        */
       public SoapOperationParameter(String name, long type, long mode, boolean nillable, int extent)
       {
          this.name = name;
          this.type = type;
          this.mode = mode;
          this.nillable = nillable;
          this.extent = extent;
          this.useBeforeImage = false;
       }

       /**
        * For a table parameter, load its fields, as they appear defined in the schema dictionary.
        *
        * @param    ref
        *           The parameter definition AST.
        * @param    dictionary
        *           The schema dictionary.
        * @param    forDataset
        *           Flag indicating that this table is for a dataset
        */
       public void loadTable(Aast ref, SchemaDictionary dictionary, boolean forDataset)
       {
          tableFields = new LinkedHashMap<>();

          ref = ref.getType() == TEMP_TABLE ? ref : ref.getImmediateChild(TEMP_TABLE, null);

          // load the fields
          String schema = (String) ref.getAnnotation("schemaname");
          try
          {
             List<Aast> fields = dictionary.getAllFields(schema);
             for (Aast field : fields)
             {
                String fname = (String) field.getAnnotation("historical");
                int ftype = field.getType();
                long extent = SourceNameMapper.NO_EXTENT;
                if (field.downPath(KW_EXTENT))
                {
                   Aast extAst = field.getImmediateChild(KW_EXTENT, null);
                   extAst = extAst.getImmediateChild(NUM_LITERAL, null);
                   extent = Integer.parseInt(extAst.getText());
                }
                SoapOperationParameter fparam = new SoapOperationParameter(fname, ftype, mode, true, (int) extent);
                tableFields.put(fname.toLowerCase(), fparam);

                if (forDataset)
                {
                   String serializeName = readProperty(dictionary.getFieldProperty(name + "." + fname, KW_SERIALZN));
                   String xmlNodeName = readProperty(dictionary.getFieldProperty(name + "." + fname, KW_XML_NNAM));
                   fparam.setNodeName(serializeName, xmlNodeName);
                }
             }

             Aast table = dictionary.getTable(schema);
             if (forDataset)
             {
                String serializeName = readProperty(dictionary.getTableProperty(name, KW_SERIALZN));
                String xmlNodeName = readProperty(dictionary.getTableProperty(name, KW_XML_NNAM));
                setNodeName(serializeName, xmlNodeName);
             }

             this.beforeTable = (String) table.getAnnotation("before-table");

             Iterator<Aast> iter = dictionary.indices(table);
             while (iter.hasNext())
             {
                Aast next = iter.next();

                boolean unique = false;
                boolean primary = false;
                Aast props = next.getImmediateChild(PROPERTIES, null);
                while (props != null)
                {
                   unique = unique || (props.getImmediateChild(KW_UNIQUE, null) != null);
                   primary = primary || (props.getImmediateChild(KW_PRIMARY, null) != null);

                   props = next.getImmediateChild(PROPERTIES, props);
                }
                if (!(unique || primary))
                {
                   continue;
                }

                List<IndexComponent> indexFields = new ArrayList<>();
                Aast field = null;
                while ((field = next.getImmediateChild(INDEX_FIELD, field)) != null)
                {
                   String fieldName = field.getText();
                   SoapOperationParameter pfield = tableFields.get(fieldName.toLowerCase());
                   boolean descending = field.downPath(KW_DESCEND);
                   IndexComponent component = new IndexComponent()
                   {
                      @Override
                      public Class<? extends Annotation> annotationType()
                      {
                         return IndexComponent.class;
                      }

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

                      @Override
                      public String legacy()
                      {
                         return pfield.nodeName == null ? fieldName : pfield.nodeName;
                      }

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

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

                   indexFields.add(component);
                }

                String indexName = next.getText();
                boolean indexUnique = unique;
                boolean indexPrimary = primary;
                Index index = new Index()
                {
                   @Override
                   public Class<? extends Annotation> annotationType()
                   {
                      return Index.class;
                   }

                   @Override
                   public String name()
                   {
                      // here we hold the table name.  Used by SoapWsdl.createDataSetParam
                      String tableName = SoapOperationParameter.this.name;
                      String xmlNodeName = SoapOperationParameter.this.nodeName;
                      return xmlNodeName == null ? tableName : xmlNodeName;
                   }

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

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

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

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

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

                   @Override
                   public IndexComponent[] components()
                   {
                      return indexFields.toArray(new IndexComponent[0]);
                   }

                };

                if (tableIndexes == null)
                {
                   tableIndexes = new ArrayList<>();
                }

                tableIndexes.add(index);
             }

          }
          catch (SchemaException e)
          {
             throw new RuntimeException(e);
          }
       }

       /**
        * Check if this table is nested (via the {@link #relations}) as a child for another table.
        *
        * @param    table
        *           The table reference.
        *
        * @return   See above.
        */
       public boolean isNestedTable(SoapOperationParameter table)
       {
          if (relations == null)
          {
             return false;
          }

          for (DataSetRelation dsr : relations)
          {
             if (dsr.child.equalsIgnoreCase(table.name))
             {
                return true;
             }
          }

          return false;
       }

       /**
        * Get all the child tables for this parent table.
        *
        * @param    parent
        *           The parent table.
        *
        * @return   The child tables in the {@link #relations}, for this parent.
        */
       public List<SoapOperationParameter> getChildTables(SoapOperationParameter parent)
       {
          if (relations == null)
          {
             return Collections.emptyList();
          }

          List<SoapOperationParameter> children = new ArrayList<>();
          for (DataSetRelation dsr : relations)
          {
             if (dsr.parent.equalsIgnoreCase(parent.name))
             {
                for (SoapOperationParameter table : datasetTables)
                {
                   if (table.name.equalsIgnoreCase(dsr.child))
                   {
                      children.add(table);
                   }
                }
             }
          }

          return children;
       }

       /**
        * For a dataset parameter, load its tables, as they appear defined in the schema dictionary.
        *
        * @param    ref
        *           The parameter definition AST.
        * @param    dictionary
        *           The schema dictionary.
        */
       public void loadDataSet(Aast ref, SchemaDictionary dictionary)
       throws SchemaException
       {
          datasetTables = new ArrayList<>();

          AstSymbolResolver resolver = AstSymbolResolver.getResolver();

          ref = ref.getImmediateChild(DATA_SET, null);
          ref = resolver.getAst((Long) ref.getAnnotation("refid"));
          ref = ref.getImmediateChild(KW_FOR, null);

          Aast tast = ref.getImmediateChild(TEMP_TABLE, null);
          while (tast != null)
          {
             SoapOperationParameter top = new SoapOperationParameter(tast.getText(),
                                                                     KW_TABLE,
                                                                     mode,
                                                                     true);
             top.loadTable(tast, dictionary, true);
             datasetTables.add(top);
             tast = ref.getImmediateChild(TEMP_TABLE, tast);
          }

          // TODO: PARENT-ID-RELATION

          // add relations
          ref = ref.getParent();
          Aast rast = ref.getImmediateChild(KW_DATA_REL, null);
          while (rast != null)
          {
             Aast parent = rast.getImmediateChild(TEMP_TABLE, null);
             Aast child = rast.getImmediateChild(TEMP_TABLE, parent);

             parent = dictionary.getTable((String) parent.getAnnotation("schemaname"));
             child = dictionary.getTable((String) child.getAnnotation("schemaname"));

             String name = (String) rast.getImmediateChild(SYMBOL, null).getText();
             String parentName = (String) parent.getAnnotation("historical");
             String childName = (String) child.getAnnotation("historical");

             String pairs = "";
             Aast relFields = rast.getImmediateChild(KW_REL_FLDS, null);
             Aast fieldPair = relFields.getImmediateChild(PARENT_CHILD_RELATION, null);
             while (fieldPair != null)
             {
                Aast f1 = fieldPair.getChildAt(0);
                Aast f2 = fieldPair.getChildAt(1);

                f1 = dictionary.getField(parentName + "." + f1.getAnnotation("name"));
                f2 = dictionary.getField(parentName + "." + f2.getAnnotation("name"));

                pairs = pairs + (pairs.isEmpty() ? "" : ",") +
                        f1.getAnnotation("historical") + "," +
                        f2.getAnnotation("historical");

                fieldPair = relFields.getImmediateChild(PARENT_CHILD_RELATION, fieldPair);
             }

             boolean nested = rast.downPath(KW_NESTED);
             boolean fkHidden = rast.downPath(KW_F_KEY_H);

             if (relations == null)
             {
                relations = new ArrayList<>();
             }

             relations.add(new DataSetRelation(name, parentName, childName, pairs, nested, fkHidden));

             rast = ref.getImmediateChild(KW_DATA_REL, rast);
          }
       }

       /**
        * Set the {@link #nodeName} for this parameter from the XML-NODE-NAME and SERIALIZE-NAME attributes.
        *
        * @param    serializeName
        *           The SERIALIZE-NAME attribute.
        * @param    xmlNodeName
        *           The XML-NODE-NAME attribute.
        */
       void setNodeName(String serializeName, String xmlNodeName)
       {
          if (xmlNodeName == null)
          {
             if (serializeName == null)
             {
                this.nodeName = null;
             }
             else
             {
                this.nodeName = serializeName;
             }
          }
          else
          {
             this.nodeName = xmlNodeName;
          }
       }

       /**
        * Read a property from a schema dict AST.
        *
        * @param    propAst
        *           The property AST.
        *
        * @return   The property value.
        */
       private String readProperty(Aast propAst)
       {
          if (propAst == null)
          {
             return null;
          }

          Aast ref = propAst.getImmediateChild(STRING, null);
          if (ref == null)
          {
             return null;
          }

          String txt = ref.getText();
          txt = StringHelper.stripEnclosing(txt, new char[] { '\"', '\'' });

          return txt;
       }
    }