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