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