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