# SoapOperation.java

    /*
    ** Module   : SoapOperation.java
    ** Abstract : Definition of a SOAP operation 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.
    ** 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.util.*;

    import org.w3c.dom.*;

    import com.goldencode.ast.*;
    import com.goldencode.p2j.schema.*;
    import com.goldencode.p2j.uast.*;
    import com.goldencode.p2j.util.*;

    /**
     * Definition of a SOAP operation exported via .wsm files.
     * <p>
     * Data for an exported SOAP operation is read from the .wsm <code>Procedure</code> node; if this is a
     * persistent procedure, its internal entries are loaded from <code>ProcDetail</code> nodes.
     */
    public class SoapOperation
    {
       /** The file targeted by this operation. */
       final String file;

       /** The operation name (the <code>Procedure/Name</code> node). */
       final String name;

       /** Flag indicating if this is a persistent procedure (<code>Procedure#isPersistent</code>). */
       final boolean persistent;

       /** The computed WSDL operation name. */
       String operation;

       /** Flag indicating that the return value parameter is used (named <code>result</code>). */
       boolean useRetVal = false;

       /** The return parameter's type. */
       int retValType = ProgressParserTokenTypes.VAR_CHAR;

       /** The parent operation (for persistent procedure). */
       SoapOperation parent = null;

       /** The associated root SOAP configuration (.wsm file) for this operation. */
       SoapConfig soap = null;

       /**
        * Flag indicating this operation is excluded (<code>Procedure/ProcDetail/InternalProc#isExcluded</code>).
        */
       boolean excluded = false;

       /** The parent service name (for WSDL binding). */
       String parentService = null;

       /** The computed SOAP service name (for WSDL binding). */
       String service = null;

       /** Flag indicating if this exported operation from the .wsm file was registered in WSDL. */
       boolean registered = false;

       /** The operation's parameter list. */
       List<SoapOperationParameter> parameters = new ArrayList<>();

       /** The operation's parameters, by their name. */
       private Map<String, Map<String, String>> paramCfgs = new HashMap<>();

       /** The operation's parameter XML elements, by their name. */
       private Map<String, Element> paramEls = new HashMap<>();

       /**
        * Initialize this instance as a copy of the source operation.
        *
        * @param    op
        *           The source operation.
        */
       public SoapOperation(SoapOperation op)
       {
          this.file = op.file;
          this.name = op.name;
          this.operation = op.operation;
          this.persistent = op.persistent;
          this.useRetVal = op.useRetVal;
          this.retValType = op.retValType;
          this.parent = op.parent;
          this.soap = op.soap;
          this.excluded = op.excluded;
          this.parentService = op.parentService;
          this.service = op.service;
          this.registered = op.registered;
          this.parameters = op.parameters;
       }

       /**
        * Create a new instance with the specified details.
        *
        * @param    soap
        *           The root SOAP configuration.
        * @param    file
        *           The target file name.
        * @param    name
        *           The operation name.
        * @param    portType
        *           The target WSDL port type.
        * @param    isPersistent
        *           Flag indicating that this is a persistent operation.
        */
       public SoapOperation(SoapConfig soap, String file, String name, String portType, boolean isPersistent)
       {
          this.soap = soap;
          this.file = file;
          this.name = name;
          this.operation = portType;
          this.persistent = isPersistent;
       }

       /**
        * Create a new instance with the specified details.
        *
        * @param    soap
        *           The root SOAP configuration.
        * @param    parent
        *           The parent operation.
        * @param    operation
        *           The operation name.
        */
       public SoapOperation(SoapConfig soap, SoapOperation parent, String operation)
       {
          this.soap = soap;
          this.parent = parent;
          this.name = operation;
          this.persistent = false;
          this.operation = operation;
          this.file = null;
       }

       /**
        * Check if this operation applies to the specified file, program and internal entry.
        *
        * @param    file
        *           The exported file for this operation.  May be <code>null</code>, in which case this is ignored.
        *           Otherwise, the file must end with the parent's or this instance's {@link #file}.
        * @param    program
        *           The program name.  If {@link #parent} is not-null, must match with {@link #name}, otherwise
        *           with parent's {@link #name}.
        * @param    internalEntry
        *           The internal entry.  Matched against {@link #name} only when {@link #parent} is set.
        *
        * @return   <code>true</code> if this operation matches the specified configuration.
        */
       public boolean isFor(String file, String program, String internalEntry)
       {
          return (file == null || file.endsWith((parent == null ? this.file : parent.file))) &&
                 (internalEntry == null
                    ? parent == null && name.equals(program)
                    : parent != null && parent.name.equals(program) && name.equals(internalEntry));
       }

       /**
        * Add a parameter to this operation, from the service's target 4GL code.
        *
        * @param    parameter
        *           The parameter AST.
        */
       public void addParameter(Aast parameter)
       {
          String name = (String) parameter.getAnnotation("name");
          long type = (long) parameter.getAnnotation("type");

          if (parameter.downPath(ProgressParserTokenTypes.KW_DSET_HND))
          {
             type = ProgressParserTokenTypes.KW_DSET_HND;
          }
          else if (parameter.downPath(ProgressParserTokenTypes.KW_TAB_HAND))
          {
             type = ProgressParserTokenTypes.KW_TAB_HAND;
          }

          long mode = -1;
          if (parameter.isAnnotation("parmtype"))
          {
             mode = (long) parameter.getAnnotation("parmtype");
          }
          else if (parameter.downPath(ProgressParserTokenTypes.KW_INPUT))
          {
             mode = ProgressParserTokenTypes.KW_INPUT;
          }
          else if (parameter.downPath(ProgressParserTokenTypes.KW_OUTPUT))
          {
             mode = ProgressParserTokenTypes.KW_OUTPUT;
          }
          else if (parameter.downPath(ProgressParserTokenTypes.KW_IN_OUT))
          {
             mode = ProgressParserTokenTypes.KW_IN_OUT;
          }

          if (mode == -1)
          {
             throw new IllegalArgumentException("Invalid mode!");
          }

          if (type == -1)
          {
             throw new IllegalArgumentException("Invalid type!");
          }

          long extent = SourceNameMapper.NO_EXTENT;
          if (parameter.isAnnotation("extent"))
          {
             extent = (long) parameter.getAnnotation("extent");
          }
          else if (parameter.isAnnotation("oldextent"))
          {
             extent = (long) parameter.getAnnotation("oldextent");
          }

          Map<String, String> paramCfg = paramCfgs.get(name);
          if (paramCfg == null)
          {
             System.out.println("WARNING: no .wsm configuration for parameter " + name + " in " + this);
          }

          boolean nillable = true; // paramCfg != null && "true".equals(paramCfg.get("Parameter#allowUnknown"))
          SoapOperationParameter p = new SoapOperationParameter(name, type, mode, nillable, (int) extent);
          parameters.add(p);
       }

       /**
        * Add a table parameter and load its fields, as they appear defined in the schema dictionary.
        *
        * @param    parameter
        *           The parameter definition AST.
        * @param    dictionary
        *           The schema dictionary.
        */
       public void addTableParameter(Aast parameter, SchemaDictionary dictionary)
       {
          int type = ProgressParserTokenTypes.KW_TABLE;

          Aast ref = parameter.getImmediateChild(type, null);
          String name = ref.getImmediateChild(ProgressParserTokenTypes.TEMP_TABLE, null).getText();

          long mode = -1;
          if (parameter.isAnnotation("parmtype"))
          {
             mode = (long) parameter.getAnnotation("parmtype");
          }
          else if (parameter.downPath(ProgressParserTokenTypes.KW_INPUT))
          {
             mode = ProgressParserTokenTypes.KW_INPUT;
          }
          else if (parameter.downPath(ProgressParserTokenTypes.KW_OUTPUT))
          {
             mode = ProgressParserTokenTypes.KW_OUTPUT;
          }
          else if (parameter.downPath(ProgressParserTokenTypes.KW_IN_OUT))
          {
             mode = ProgressParserTokenTypes.KW_IN_OUT;
          }

          if (mode == -1)
          {
             throw new IllegalArgumentException("Invalid mode!");
          }

          if (type == -1)
          {
             throw new IllegalArgumentException("Invalid type!");
          }

          try
          {
             Aast tableAst = dictionary.getTable(name);
             name = (String) tableAst.getAnnotation("legacy_name");
          }
          catch (SchemaException e)
          {
             throw new RuntimeException("Could not resolve table name:" + name);
          }

          Map<String, String> paramCfg = paramCfgs.get(name);
          if (paramCfg == null)
          {
             System.out.println("WARNING: no .wsm configuration for parameter " + name + " in " + this);
          }

          boolean nillable = paramCfg != null && "true".equals(paramCfg.get("Parameter/NamespaceUri#xsi:nil"));
          SoapOperationParameter p = new SoapOperationParameter(name, type, mode, nillable);
          parameters.add(p);

          p.loadTable(ref, dictionary, false);
       }

       /**
        * Add a dataset parameter and load its fields, as they appear defined in the schema dictionary.
        *
        * @param    parameter
        *           The parameter definition AST.
        * @param    dictionary
        *           The schema dictionary.
        */
       public void addDataSetParameter(Aast parameter, SchemaDictionary dictionary)
       {
          int type = ProgressParserTokenTypes.KW_DATASET;
          Aast ref = parameter.getImmediateChild(type, null);
          String name = ref.getImmediateChild(ProgressParserTokenTypes.DATA_SET, null).getText();

          long mode = -1;
          if (parameter.isAnnotation("parmtype"))
          {
             mode = (long) parameter.getAnnotation("parmtype");
          }
          else if (parameter.downPath(ProgressParserTokenTypes.KW_INPUT))
          {
             mode = ProgressParserTokenTypes.KW_INPUT;
          }
          else if (parameter.downPath(ProgressParserTokenTypes.KW_OUTPUT))
          {
             mode = ProgressParserTokenTypes.KW_OUTPUT;
          }
          else if (parameter.downPath(ProgressParserTokenTypes.KW_IN_OUT))
          {
             mode = ProgressParserTokenTypes.KW_IN_OUT;
          }

          if (mode == -1)
          {
             throw new IllegalArgumentException("Invalid mode!");
          }

          if (type == -1)
          {
             throw new IllegalArgumentException("Invalid type!");
          }

          Map<String, String> paramCfg = paramCfgs.get(name);
          if (paramCfg == null)
          {
             System.out.println("WARNING: no .wsm configuration for parameter " + name + " in " + this);
          }

          AstSymbolResolver resolver = AstSymbolResolver.getResolver();
          boolean useBeforeImage = paramCfg != null && "true".equals(paramCfg.get("Parameter#writeXmlBeforeImage"));
          boolean nillable = paramCfg != null && "true".equals(paramCfg.get("Parameter/DataSetMetaData/NamespaceUri#allowUnknown"));
          SoapOperationParameter p = new SoapOperationParameter(name, type, mode, nillable, useBeforeImage);
          Aast dsref = ref.getImmediateChild(ProgressParserTokenTypes.DATA_SET, null);
          dsref = resolver.getAst((Long) dsref.getAnnotation("refid"));
          String xmlNodeName = (String) dsref.getAnnotation("xmlnodename");
          String serializeName = (String) dsref.getAnnotation("serializename");
          p.setNodeName(serializeName, xmlNodeName);
          if (p.nodeName == null && name.isEmpty())
          {
             p.nodeName = "ProDataSet";
          }
          parameters.add(p);

          try
          {
             p.loadDataSet(ref, dictionary);
          }
          catch (SchemaException e)
          {
             throw new RuntimeException(e);
          }
       }

       /**
        * Get all the input or output parameters for this operation.
        *
        * @param    input
        *           Flag indicating if input or output parameters are required.  INPUT-OUTPUT are included for
        *           either case.
        *
        * @return   The list of parameters for this operation.
        */
       public List<SoapOperationParameter> getParameters(boolean input)
       {
          List<SoapOperationParameter> res = new ArrayList<>();
          for (SoapOperationParameter p : parameters)
          {
             if ((input && p.mode == ProgressParserTokenTypes.KW_INPUT) ||
                 (!input && p.mode == ProgressParserTokenTypes.KW_OUTPUT) ||
                 p.mode == ProgressParserTokenTypes.KW_IN_OUT)
             {
                res.add(p);
             }
          }

          return res;
       }

       /**
        * Get the string representation of this operation.
        */
       @Override
       public String toString()
       {
          return parent == null ? file : (parent.toString() + ":" + name);
       }

       /**
        * Get the {@link #persistent} flag for this operation.
        *
        * @return   See above.
        */
       public boolean isPersistent()
       {
          return persistent;
       }

       /**
        * Get the operation name.
        *
        * @return   The {@link #operation}.
        */
       public String getOperation()
       {
          return operation;
       }

       /**
        * Get the WSDL binding for this operation.  If this is a {@link #persistent} program, the
        * {@link #parentService} is returned.  Otherwise, {@link #service} is used as the binding name.
        *
        * @return   See above.
        */
       public String getBinding()
       {
          return persistent ? parentService : service;
       }

       /**
        * Get the WSDL namespace for this operation, from the <code>DeploymentWizard/WebServiceNamespace</code>
        * configuration in the .wsm file.
        *
        * @return   See above.
        */
       public String getNamespace()
       {
          return soap.getConfig("DeploymentWizard/WebServiceNamespace");
       }

       /**
        * Given a ProcDetails node, read the parameters and populate the {@link #paramCfgs} and {@link #paramEls}
        * maps.
        *
        * @param    procDetails
        *           The ProcDetails XML element.
        * @param    parent
        *           The name of the parent node.
        */
       void createParameterMap(NodeList procDetails, String parent)
       {
          if (procDetails == null || procDetails.getLength() == 0)
          {
             return;
          }

          Element procDetail = (Element) procDetails.item(0);
          NodeList params = procDetail.getElementsByTagName("Parameter");
          for (int i = 0; i < params.getLength(); i++)
          {
             Element elParam = (Element) params.item(i);

             Map<String, String> cfgs = new HashMap<>();
             ServiceSupport.readRecursively("Parameter", elParam, cfgs);

             NamedNodeMap attrs = elParam.getAttributes();
             for (int j = 0; j < attrs.getLength(); j++)
             {
                Node attr = attrs.item(j);
                cfgs.put("Parameter#" + attr.getNodeName(), attr.getNodeValue());
             }

             paramCfgs.put(cfgs.get("Parameter/OrigName"), cfgs);
             paramEls.put(cfgs.get("Parameter/OrigName"), elParam);
          }

          Map<String, String> cfgs = new HashMap<>();
          ServiceSupport.readRecursively(parent, procDetail, cfgs);
          useRetVal = cfgs.containsKey(parent + "/ReturnValue#ordinal");
       }
    }