SoapWsdl.java
/*
** Module : SoapWsdl.java
** Abstract : Helper class to generate a WSDL document from a .wsm file and its exported operations.
**
** Copyright (c) 2020-2021, 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 and BEFORE-TABLE support.
** CA 20210911 The targetNamespace was not properly set from the DeploymentWizard/WebServiceNamespace.
** 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).
*/
/*
** 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.net.*;
import java.util.*;
import java.util.concurrent.atomic.*;
import javax.xml.parsers.*;
import org.w3c.dom.*;
import com.goldencode.p2j.*;
import com.goldencode.p2j.persist.annotation.*;
import com.goldencode.p2j.uast.*;
import com.goldencode.p2j.util.*;
import com.goldencode.util.*;
import static com.goldencode.p2j.uast.ProgressParserTokenTypes.*;
/**
* Generate the WSDL file for all services exported by a .wsm file.
*/
class SoapWsdl
{
/** The namespace for the SOAP elements. */
private static final String NS_SOAP = "http://schemas.xmlsoap.org/wsdl/soap/";
/** The namespace for the WSDL elements. */
private static final String NS_WSDL = "http://schemas.xmlsoap.org/wsdl/";
/** The namespace for the XMLNS elements. */
private static final String NS_XMLNS = "http://www.w3.org/2000/xmlns/";
/** The namespace for the PRODATA elements. */
private static final String NS_PRODATA = "urn:schemas-progress-com:xml-prodata:0001";
/** The WSDL exported services. */
private final Map<String, ServiceConfig> services = new HashMap<>();
/** The WSDL document. */
private final Document dom;
/** The WSDL root element. */
private final Element root;
/** The element where to append the schema elements defining all the types in a namespace. */
private final Element elTypes;
/** The default namespace for the WSDL. */
private String targetNamespace = "urn:tempuri-org";
/** The target endpoint (only the path, without protocol, host and port). */
private String endpoint = "";
/** The default actioan name. */
private String soapAction = "";
/** Flag to append the object name to the {@link #soapAction}. */
private boolean appendObjNameToSoapAction = false;
/** The suffix for the port-type binding operation. */
private String portTypeBindingSuffix = "Obj";
/** The suffix for the service. */
private String serviceSuffix = "Service";
/** The default service. */
private String defaultService;
/** Map of datasets defined in this WSDL. */
private Map<String, Element> datasets = new HashMap<>();
/** Set of added table parameters. */
private Set<String> tableParams = new HashSet<>();
/** The help string to be used as service documentation. */
private String helpString;
/**
* Create a new instance.
*/
public SoapWsdl()
{
try
{
dom = XmlHelper.newDocument();
}
catch (ParserConfigurationException e)
{
throw new RuntimeException(e);
}
root = dom.createElementNS(NS_WSDL, "wsdl:definitions");
dom.appendChild(root);
// create the 'types' container
elTypes = dom.createElementNS(NS_WSDL, "wsdl:types");
// create the soap-fault type
Element elSchema = dom.createElement("schema");
elSchema.setAttribute("elementFormDefault", "unqualified");
elSchema.setAttribute("targetNamespace", "urn:soap-fault:details");
elSchema.setAttribute("xmlns", "http://www.w3.org/2001/XMLSchema");
List<SoapOperationParameter> faultParameters = new ArrayList<>();
faultParameters.add(new SoapOperationParameter("errorMessage", VAR_CHAR, KW_OUTPUT, false));
faultParameters.add(new SoapOperationParameter("requestID", VAR_CHAR, KW_OUTPUT, false));
Element elTypeFault = createComplexType(null, "FaultDetail", faultParameters, true);
elSchema.appendChild(elTypeFault);
elTypes.appendChild(elSchema);
}
/**
* Set the service documentation.
*
* @param helpString
* The help string.
*/
public void setHelpString(String helpString)
{
this.helpString = helpString;
}
/**
* Set the endpoint for this WSDL.
*
* @param endpoint
* The configured endpoint from the .wsm file.
*/
public void setEndpoint(String endpoint)
{
try
{
URI uri = new URI(endpoint);
endpoint = uri.getPath();
this.endpoint = endpoint;
}
catch (URISyntaxException e)
{
throw new RuntimeException("Could not resolve endpoint path: " + endpoint, e);
}
}
/**
* Set the {@link #appendObjNameToSoapAction} flag.
*
* @param appendObjNameToSoapAction
* The new {@link #appendObjNameToSoapAction} value.
*/
public void setAppendObjNameToSoapAction(boolean appendObjNameToSoapAction)
{
this.appendObjNameToSoapAction = appendObjNameToSoapAction;
}
/**
* Set the port-type binding suffix. It will override the default 'Obj' suffix only if the new value is
* not-null and not empty.
*
* @param portTypeBindingSuffix
* The new {@link #portTypeBindingSuffix} value.
*/
public void setPortTypeBindingSuffix(String portTypeBindingSuffix)
{
if (portTypeBindingSuffix == null)
{
return;
}
this.portTypeBindingSuffix = portTypeBindingSuffix;
}
/**
* Set the service suffix. It will override the default 'Service' suffix only if the new value is not
* null and not empty.
*
* @param serviceSuffix
* The new service suffix.
*/
public void setServiceSuffix(String serviceSuffix)
{
if (serviceSuffix == null)
{
return;
}
this.serviceSuffix = serviceSuffix;
}
/**
* Set the soap action.
*
* @param soapAction
* The SOAP action.
*/
public void setSoapAction(String soapAction)
{
if (soapAction == null)
{
return;
}
this.soapAction = soapAction;
}
/**
* Add a WSDL service.
*
* @param service
* The service name.
* @param parentService
* The parent service name.
*/
public void addService(String service, String parentService)
{
if (services.containsKey(service))
{
return;
}
boolean first = services.isEmpty();
if (first)
{
root.setAttribute("name", service);
defaultService = service;
}
ServiceConfig srv = new ServiceConfig(service, services.size() + 2, parentService);
services.put(service, srv);
if (first)
{
Element el = dom.createElementNS(NS_WSDL, "wsdl:message");
el.setAttribute("name", "FaultDetailMessage");
Element elPart = dom.createElementNS(NS_WSDL, "wsdl:part");
elPart.setAttribute("name", "FaultDetail");
elPart.setAttribute("element", "S1:FaultDetail");
el.appendChild(elPart);
srv.messages.add(el);
}
}
/**
* Add a SOAP operation to a service.
*
* @param operation
* The SOAP operation configuration.
*/
public void addOperation(SoapOperation operation)
{
// TODO: createAO - is it needed? If not, document why.
if (operation.parent == null && operation.persistent)
{
// add the 'CreatePO_%s'
ServiceConfig srv = services.get(operation.parentService);
String typeName = "CreatePO_" + operation.operation;
List<SoapOperationParameter> inputParms = new ArrayList<>();
srv.elSchema.appendChild(createComplexType(operation, typeName, inputParms, true));
List<SoapOperationParameter> outputParms = new ArrayList<>();
outputParms.add(new SoapOperationParameter("result", operation.retValType, KW_OUTPUT, true));
srv.elSchema.appendChild(createComplexType(operation, typeName + "Response", outputParms, true));
// add messages
srv.messages.add(createMessage(srv, operation.operation, typeName, null));
srv.messages.add(createMessage(srv, operation.operation + "Response", typeName + "Response", null));
// add operation to port
srv.elPortType.appendChild(createPortTypeOperation(srv, typeName, operation.operation));
// add operation to binding
srv.elBinding.appendChild(createBindingOperation(srv, operation, typeName, false));
////////////////////////////////////////////////////////////////////////////////////////
// create the 'Release_%s' in the operation's service
srv = services.get(operation.service);
typeName = "Release_" + operation.operation;
List<SoapOperationParameter> idParms = new ArrayList<>();
idParms.add(new SoapOperationParameter("UUID", VAR_CHAR, KW_OUTPUT, false));
srv.elSchema.appendChild(createComplexType(operation, operation.operation + "ID", idParms, true));
srv.elSchema.appendChild(createComplexType(operation, typeName, null, true));
srv.elSchema.appendChild(createComplexType(operation, typeName + "Response", null, true));
// add messages
srv.messages.add(createMessage(srv, operation.operation + "ID", operation.operation + "ID", operation.operation + "ID"));
srv.messages.add(createMessage(srv, operation.operation + "Release", typeName, null));
srv.messages.add(createMessage(srv, operation.operation + "ReleaseResponse", typeName + "Response", null));
// add operation to port
srv.elPortType.appendChild(createPortTypeOperation(srv, typeName, operation.operation + "Release"));
// add operation to binding
srv.elBinding.appendChild(createBindingOperation(srv, operation, typeName, true));
return;
}
ServiceConfig srv = services.get(operation.service);
srv.elSchema.appendChild(createComplexType(operation,
operation.operation,
operation.getParameters(true),
true));
List<SoapOperationParameter> outputParms = operation.getParameters(false);
if (operation.useRetVal)
{
outputParms.add(0, new SoapOperationParameter("result", operation.retValType, KW_OUTPUT, true));
}
srv.elSchema.appendChild(createComplexType(operation,
operation.operation + "Response",
outputParms,
true));
// add messages
srv.messages.add(createMessage(srv, operation.operation, null, null));
srv.messages.add(createMessage(srv, operation.operation + "Response", null, null));
// add operation to port
srv.elPortType.appendChild(createPortTypeOperation(srv, operation.operation, null));
// add operation to binding
Boolean hID = (operation.parent == null ? null : operation.parent.persistent);
srv.elBinding.appendChild(createBindingOperation(srv, operation, operation.operation, hID));
}
/**
* Generate the WSDL document. This will iterate over all the {@link #services} and create the WSDL
* message, port-type, binding and service nodes.
*
* @return The WSDL document.
*/
public Document generate()
{
for (ServiceConfig service : services.values())
{
root.setAttributeNS(NS_XMLNS, "xmlns:" + service.ns, targetNamespace + ":" + service.name);
}
// append all message nodes
for (ServiceConfig service : services.values())
{
for (Element el : service.messages)
{
root.appendChild(el);
}
}
// append all portType nodes
for (ServiceConfig service : services.values())
{
root.appendChild(service.elPortType);
}
// append all binding nodes
for (ServiceConfig service : services.values())
{
root.appendChild(service.elBinding);
}
// append all service nodes
Element elService = dom.createElementNS(NS_WSDL, "wsdl:service");
elService.setAttribute("name", root.getAttribute("name") + serviceSuffix);
root.appendChild(elService);
for (ServiceConfig service : services.values())
{
elService.appendChild(service.elServicePort);
}
return dom;
}
/**
* Initialize this WSDL.
*
* @param author
* The author name.
* @param targetNS
* The target namespace.
*/
void initialize(String author, String targetNS)
{
if (targetNS != null)
{
targetNamespace = targetNS;
}
root.setAttribute("targetNamespace", targetNamespace);
root.setAttributeNS(NS_XMLNS, "xmlns:S1", "urn:soap-fault:details");
root.setAttributeNS(NS_XMLNS, "xmlns:wsdl", "http://schemas.xmlsoap.org/wsdl/");
root.setAttributeNS(NS_XMLNS, "xmlns:tns", targetNamespace);
root.setAttributeNS(NS_XMLNS, "xmlns:xsd", "http://www.w3.org/2001/XMLSchema");
root.setAttributeNS(NS_XMLNS, "xmlns:soap", "http://schemas.xmlsoap.org/wsdl/soap/");
root.setAttributeNS(NS_XMLNS, "xmlns:soapenc", "http://schemas.xmlsoap.org/soap/encoding/");
root.setAttributeNS(NS_XMLNS, "xmlns:prodata", "urn:schemas-progress-com:xml-prodata:0001");
Element elDoc = dom.createElementNS(NS_WSDL, "wsdl:documentation");
String serviceDoc = "EncodingType=DOC_LITERAL, FWD=" + Version.getFWDVersion().replace(' ', '_');
if (author != null && !author.isEmpty())
{
serviceDoc = "Author=" + author + ", " + serviceDoc;
}
elDoc.appendChild(dom.createTextNode(serviceDoc));
root.appendChild(elDoc);
root.appendChild(elTypes);
}
/**
* Create an operation associated with a WSDL binding element.
*
* @param srv
* The service configuration.
* @param operation
* The SOAP operation.
* @param name
* The binding operation name.
* @param headerID
* Flag indicating if the input message contains a header ID.
*
* @return The created binding operation element.
*/
private Element createBindingOperation(ServiceConfig srv,
SoapOperation operation,
String name,
Boolean headerID)
{
Element elHeader = null;
if (headerID != null)
{
String hid = operation.parent == null ? operation.operation : operation.parent.operation;
hid = hid + "ID";
elHeader = dom.createElementNS(NS_SOAP, "soap:header");
elHeader.setAttribute("message", "tns:" + hid);
elHeader.setAttribute("part", hid);
elHeader.setAttribute("use", "literal");
if (headerID)
{
// only INPUT requires it
elHeader.setAttributeNS(NS_WSDL, "wsdl:required", "true");
}
}
Element el = dom.createElementNS(NS_WSDL, "wsdl:operation");
el.setAttribute("name", name);
Element elSoap = dom.createElementNS(NS_SOAP, "soap:operation");
String action = soapAction;
if (appendObjNameToSoapAction)
{
action = action + "/" + srv.name;
}
elSoap.setAttribute("soapAction", action);
elSoap.setAttribute("style", "document");
el.appendChild(elSoap);
Element elInput = dom.createElementNS(NS_WSDL, "wsdl:input");
if (headerID != null && headerID)
{
elInput.appendChild(elHeader);
}
Element elInputBody = dom.createElementNS(NS_SOAP, "soap:body");
elInputBody.setAttribute("use", "literal");
elInput.appendChild(elInputBody);
el.appendChild(elInput);
Element elOutput = dom.createElementNS(NS_WSDL, "wsdl:output");
if (headerID != null && !headerID)
{
elOutput.appendChild(elHeader);
}
Element elOutputBody = dom.createElementNS(NS_SOAP, "soap:body");
elOutputBody.setAttribute("use", "literal");
elOutput.appendChild(elOutputBody);
el.appendChild(elOutput);
Element elFault = dom.createElementNS(NS_WSDL, "wsdl:fault");
elFault.setAttribute("name", srv.name + "Fault");
Element elSoapFault = dom.createElementNS(NS_SOAP, "soap:fault");
elSoapFault.setAttribute("name", srv.name + "Fault");
elSoapFault.setAttribute("use", "literal");
elFault.appendChild(elSoapFault);
el.appendChild(elFault);
return el;
}
/**
* Create an operation associated with the WSDL port-type.
*
* @param srv
* The service configuration.
* @param name
* The operation name.
* @param typeName
* The operation's type for the input, output or fault messages.
*
* @return The port-type operation.
*/
private Element createPortTypeOperation(ServiceConfig srv, String name, String typeName)
{
String tnsName = (typeName == null ? srv.name + "_" + name : typeName);
Element el = dom.createElementNS(NS_WSDL, "wsdl:operation");
el.setAttribute("name", name);
Element elInput = dom.createElementNS(NS_WSDL, "wsdl:input");
elInput.setAttribute("message", "tns:" + tnsName);
el.appendChild(elInput);
Element elOutput = dom.createElementNS(NS_WSDL, "wsdl:output");
elOutput.setAttribute("message", "tns:" + tnsName + "Response");
el.appendChild(elOutput);
Element elFault = dom.createElementNS(NS_WSDL, "wsdl:fault");
elFault.setAttribute("name", srv.name + "Fault");
elFault.setAttribute("message", "tns:FaultDetailMessage");
el.appendChild(elFault);
return el;
}
/**
* Create a message associated with a WSDL port-type operation.
*
* @param srv
* The service configuration.
* @param name
* The message name.
* @param element
* The overriding name for the 'element' attribute at the message.
* @param partName
* The 'name' attribute value. Defaults to 'parameters'.
*
* @return The XML element for this message.
*/
private Element createMessage(ServiceConfig srv, String name, String element, String partName)
{
Element el = dom.createElementNS(NS_WSDL, "wsdl:message");
el.setAttribute("name", element == null ? srv.name + "_" + name : name);
Element elPart = dom.createElementNS(NS_WSDL, "wsdl:part");
elPart.setAttribute("name", partName == null ? "parameters" : partName);
elPart.setAttribute("element", srv.ns + ":" + (element == null ? name : element));
el.appendChild(elPart);
return el;
}
/**
* Create a table parameter for a SOAP operation.
*
* @param operation
* The SOAP operation.
* @param par
* The table parameter.
*
* @return The XML element for this parameter.
*/
private Element createTableParam(SoapOperation operation, SoapOperationParameter par)
{
ServiceConfig srv = services.get(operation.service);
Element elType = dom.createElement("complexType");
elType.setAttribute("name", operation.operation + "_" + par.name + "Param");
Element elSeq = dom.createElement("sequence");
Element el = dom.createElement("element");
el.setAttribute("maxOccurs", "unbounded");
el.setAttribute("minOccurs", "0");
el.setAttribute("name", par.name + "Row");
el.setAttribute("type", srv.ns + ":" + operation.operation + "_" + par.name + "Row");
elSeq.appendChild(el);
elType.appendChild(elSeq);
return elType;
}
/**
* Create the before-image type.
*
* @param service
* The service to which this type belongs.
* @param dsname
* The dataset name.
*
* @return The XML element for this before-image type.
*/
private Element createDataSetBeforeImage(ServiceConfig service, String dsname)
{
Element elType = dom.createElement("complexType");
elType.setAttribute("name", dsname + "Changes");
elType.setAttribute("prodata:datasetName", dsname);
elType.setAttribute("prodata:isDsChanges", "true");
elType.setAttribute("prodata:namespace", targetNamespace + ":" + service.name);
Element elSeq = dom.createElement("sequence");
Element elAny = dom.createElement("any");
elSeq.appendChild(elAny);
elType.appendChild(elSeq);
return elType;
}
/**
* Check if there already exists a dataset with this name. If it exists, return <code>null</code> so it
* will not be added again to the schema. Otherwise, create an unique name for it, in the {@link #datasets}
* map.
*
* @param el
* The dataset schema element.
*
* @return The new name or <code>null</code> if it already exists.
*/
private String checkDataSetName(Element el)
{
String dsname = el.getAttribute("name");
Element dsEl = datasets.get(dsname);
if (dsEl != null)
{
if (identicalElements(el, dsEl))
{
return null;
}
int idx = 2;
while (datasets.containsKey(dsname + idx))
{
idx = idx + 1;
}
dsname = dsname + idx;
}
datasets.put(dsname, el);
return dsname;
}
/**
* Check if these two XML element nodes are syntactically identical (attributes, child nodes).
*
* @param el1
* The first element.
* @param el2
* The second element.
*
* @return <code>true</code> if the elements are identical.
*/
private boolean identicalElements(Element el1, Element el2)
{
Map<String, String> attr1 = readAttributes(el1);
Map<String, String> attr2 = readAttributes(el1);
if (attr1.size() != attr2.size())
{
return false;
}
Set<String> keys = new HashSet<>();
keys.addAll(attr1.keySet());
keys.addAll(attr2.keySet());
if (keys.size() != attr1.size())
{
return false;
}
for (String key : keys)
{
String v1 = attr1.get(key);
String v2 = attr2.get(key);
if (!v1.equals(v2))
{
return false;
}
}
NodeList nl1 = el1.getChildNodes();
NodeList nl2 = el2.getChildNodes();
if (nl1.getLength() != nl2.getLength())
{
return false;
}
for (int i = 0; i < nl1.getLength(); i++)
{
Node n1 = nl1.item(i);
Node n2 = nl2.item(i);
if (n1.getNodeType() == Node.ELEMENT_NODE && n2.getNodeType() == Node.ELEMENT_NODE)
{
if (!identicalElements((Element) n1, (Element) n2))
{
return false;
}
}
else if (n1.getNodeType() != n2.getNodeType())
{
return false;
}
}
return true;
}
/**
* Read all attributes from the specified node.
*
* @param el
* The XML node.
*
* @return A map of attribute names and their values.
*/
private Map<String, String> readAttributes(Element el)
{
Map<String, String> res = new HashMap<>();
NamedNodeMap nnm = el.getAttributes();
for (int i = 0; i < nnm.getLength(); i++)
{
Node n = nnm.item(i);
if (n.getNodeType() == Node.ATTRIBUTE_NODE)
{
res.put(n.getNodeName(), n.getNodeValue());
}
}
return res;
}
/**
* Create a table schema, considering the dataset relations (nested, etc).
*
* @param operation
* The SOAP operation.
* @param dataset
* The parent dataset.
* @param tparam
* The table reference.
*
* @return The root element for this table.
*/
private Element createTableParam(SoapOperation operation,
SoapOperationParameter dataset,
SoapOperationParameter tparam)
{
Element elTable = createComplexType(operation, tparam.name, tparam.tableFields.values(), true);
if (tparam.nodeName != null)
{
elTable.setAttribute("name", tparam.nodeName);
elTable.setAttribute("prodata:tableName", tparam.name);
}
if (tparam.beforeTable != null)
{
elTable.setAttribute("prodata:beforeTable", "BI" + tparam.name);
}
elTable.setAttribute("maxOccurs", "unbounded");
elTable.setAttribute("minOccurs", "0");
Element elSeq = (Element) elTable.getElementsByTagName("sequence").item(0);
// append recursively all child tables...
List<SoapOperationParameter> childTables = dataset.getChildTables(tparam);
for (SoapOperationParameter child : childTables)
{
Element elChild = createTableParam(operation, dataset, child);
elSeq.appendChild(elChild);
}
return elTable;
}
/**
* Create a dataset parameter for a SOAP operation.
*
* @param operation
* The SOAP operation.
* @param par
* The dataset parameter.
*
* @return The XML element for this parameter.
*/
private Element createDataSetParam(SoapOperation operation, SoapOperationParameter par)
{
Element el = dom.createElement("element");
String serviceNS = services.get(operation.service).ns;
Element elAppinfo = null;
// append the dataset-relation(s)
// 1. to annotation/appinfo - non-PK based relations
// 2. TODO: as 'keyref' - PK based relations are emitted as children of root 'el' node
if (par.relations != null)
{
for (DataSetRelation relation : par.relations)
{
// TODO: skip 'keyref' relations
if (elAppinfo == null)
{
Element elAnno = dom.createElement("annotation");
el.appendChild(elAnno);
elAppinfo = dom.createElement("appinfo");
elAnno.appendChild(elAppinfo);
}
Element elRel = dom.createElementNS(NS_PRODATA, "prodata:relation");
elAppinfo.appendChild(elRel);
elRel.setAttribute("name", relation.name);
elRel.setAttributeNS(NS_PRODATA, "prodata:child", relation.child);
if (relation.nested)
{
elRel.setAttributeNS(NS_PRODATA, "prodata:nested", "true");
}
elRel.setAttributeNS(NS_PRODATA, "prodata:parent", relation.parent);
elRel.setAttributeNS(NS_PRODATA, "prodata:relationFields", relation.pairs);
}
}
List<Index> uniqueIndexes = new ArrayList<>();
for (SoapOperationParameter tparam : par.datasetTables)
{
if (tparam.tableIndexes == null)
{
continue;
}
for (Index index : tparam.tableIndexes)
{
if (index.primary() && !index.unique())
{
if (elAppinfo == null)
{
Element elAnno = dom.createElement("annotation");
el.appendChild(elAnno);
elAppinfo = dom.createElement("appinfo");
elAnno.appendChild(elAppinfo);
}
Element elIndex = dom.createElementNS(NS_PRODATA, "prodata:index");
elAppinfo.appendChild(elIndex);
// these names are NOT made unique
elIndex.setAttribute("name", index.legacy());
elIndex.setAttributeNS(NS_PRODATA, "prodata:primaryIndex", "true");
for (IndexComponent comp : index.components())
{
Element elComp = dom.createElementNS(NS_PRODATA, "prodata:field");
elComp.setAttribute("name", comp.legacy());
elIndex.appendChild(elComp);
}
Element elTable = dom.createElementNS(NS_PRODATA, "prodata:table");
elTable.setAttribute("name", tparam.name);
elIndex.appendChild(elTable);
}
else
{
uniqueIndexes.add(index);
}
}
}
if (par.nodeName != null)
{
el.setAttribute("name", par.nodeName);
el.setAttribute("prodata:datasetName", par.name);
}
else
{
el.setAttribute("name", par.name);
}
Element elType = dom.createElement("complexType");
el.appendChild(elType);
Element elSeq = dom.createElement("sequence");
for (SoapOperationParameter tparam : par.datasetTables)
{
// if the table is NESTED or PARENT-ID-RELATION, then skip it
if (par.isNestedTable(tparam))
{
continue;
}
Element elTable = createTableParam(operation, par, tparam);
elSeq.appendChild(elTable);
}
elType.appendChild(elSeq);
Map<String, AtomicInteger> counters = new HashMap<>();
for (Index index : uniqueIndexes)
{
Element elUnique = dom.createElement("unique");
// these 'unique' tags must have an unique name
String indexName = index.legacy();
AtomicInteger counter = counters.computeIfAbsent(indexName, (n) -> new AtomicInteger(0));
int next = counter.incrementAndGet();
if (next != 1)
{
indexName += "_" + next;
}
elUnique.setAttribute("name", indexName);
if (index.primary())
{
elUnique.setAttributeNS(NS_PRODATA, "prodata:primaryIndex", "true");
}
el.appendChild(elUnique);
Element elSelector = dom.createElement("selector");
elSelector.setAttribute("xpath", ".//" + serviceNS + ":" + index.legacy());
elUnique.appendChild(elSelector);
for (IndexComponent comp : index.components())
{
Element elField = dom.createElement("field");
elField.setAttribute("xpath", serviceNS + ":" + comp.legacy());
elUnique.appendChild(elField);
}
}
return el;
}
/**
* Create a complex type to declare the operation's parameters.
*
* @param operation
* The SOAP operation.
* @param name
* The type's name.
* @param parameters
* The operation's parameters.
* @param asElement
* Flag indicating if an <code>element</code> node should be used as parent.
*
* @return The XML element for this type.
*/
private Element createComplexType(SoapOperation operation,
String name,
Collection<SoapOperationParameter> parameters,
boolean asElement)
{
Element parent = null;
if (asElement)
{
Element el = dom.createElement("element");
el.setAttribute("name", name);
parent = el;
}
Element elType = dom.createElement("complexType");
if (parent != null)
{
parent.appendChild(elType);
}
else
{
elType.setAttribute("name", name);
parent = elType;
}
Element elSeq = dom.createElement("sequence");
elType.appendChild(elSeq);
if (parameters != null)
{
ServiceConfig srv = (operation == null ? null : services.get(operation.service));
for (SoapOperationParameter par : parameters)
{
String type = null;
String proType = null;
if (par.type == KW_TAB_HAND)
{
type = srv.ns + ":TableHandleParam";
if (!srv.hasTableHandleParam)
{
Element elComplexType = dom.createElement("complexType");
elComplexType.setAttribute("name", "TableHandleParam");
srv.elSchema.appendChild(elComplexType);
Element elSeq2 = dom.createElement("sequence");
elComplexType.appendChild(elSeq2);
Element elAny = dom.createElement("any");
elAny.setAttribute("namespace", "##local");
elSeq2.appendChild(elAny);
srv.hasTableHandleParam = true;
}
}
else if (par.type == KW_DSET_HND)
{
type = srv.ns + ":DataSetHandleParam";
if (!srv.hasDataSetHandleParam)
{
Element elComplexType = dom.createElement("complexType");
elComplexType.setAttribute("name", "DataSetHandleParam");
srv.elSchema.appendChild(elComplexType);
Element elAnno = dom.createElement("annotation");
elComplexType.appendChild(elAnno);
Element elDoc = dom.createElement("documentation");
elDoc.setTextContent("This is a schema definition for a dataset-handle parameter. " +
"The first element must be the schema definition for this dataset, " +
"while the second element will be the serialized dataset.");
elAnno.appendChild(elDoc);
Element elSeq2 = dom.createElement("sequence");
elComplexType.appendChild(elSeq2);
Element elAny = dom.createElement("any");
elAny.setAttribute("maxOccurs", "2");
elAny.setAttribute("minOccurs", "2");
elSeq2.appendChild(elAny);
srv.hasDataSetHandleParam = true;
}
}
else if (par.type == KW_TABLE)
{
// this is a table
String tname = operation.operation + "_" + par.name + "Param";
String rname = operation.operation + "_" + par.name + "Row";
type = srv.ns + ":" + tname;
if (tableParams.add(tname))
{
srv.elSchema.appendChild(createComplexType(null, rname, par.tableFields.values(), false));
srv.elSchema.appendChild(createTableParam(operation, par));
}
}
else if (par.type == KW_DATASET)
{
Element el = createDataSetParam(operation, par);
String dsname = checkDataSetName(el);
if (dsname != null)
{
// a new dataset, register it with the schema.
if (par.useBeforeImage)
{
Element elb4 = createDataSetBeforeImage(srv, dsname);
elb4.setAttribute("name", dsname + "Changes");
srv.elSchema.appendChild(elb4);
}
srv.elSchema.appendChild(el);
}
else
{
dsname = el.getAttribute("name");
}
// this is a dataset
type = srv.ns + ":" + dsname;
Element elPar = dom.createElement("element");
if (par.useBeforeImage)
{
elPar.setAttribute("name", dsname);
elPar.setAttribute("type", type + "Changes");
}
else
{
elPar.setAttribute("ref", type);
}
elSeq.appendChild(elPar);
continue;
}
else
{
switch ((int) par.type)
{
case FIELD_BLOB:
type = "base64Binary";
proType = "blob";
break;
case FIELD_CLOB:
proType = "clob";
case FUNC_CHAR:
case FUNC_LONGCHAR:
case FIELD_CHAR:
case VAR_CHAR:
case VAR_LONGCHAR:
type = "string";
break;
case FUNC_DATE:
case FIELD_DATE:
case VAR_DATE:
type = "date";
break;
case FIELD_DATETIME:
proType = "dateTime";
case FUNC_DATETIME:
case FUNC_DATETIME_TZ:
case FIELD_DATETIME_TZ:
case VAR_DATETIME:
case VAR_DATETIME_TZ:
type = "dateTime";
break;
case FUNC_INT:
case FIELD_INT:
case VAR_INT:
type = "int"; // signed 32-bit
break;
case FUNC_INT64:
case FUNC_RECID:
case FUNC_HANDLE:
case FIELD_INT64:
case FIELD_RECID:
case FIELD_HANDLE:
case VAR_INT64:
case VAR_RECID:
case VAR_HANDLE:
type = "long";
break;
case FUNC_DEC:
case FIELD_DEC:
case VAR_DEC:
type = "decimal";
break;
case FIELD_LOGICAL:
case FUNC_LOGICAL:
case VAR_LOGICAL:
type = "boolean";
break;
case FUNC_MEMPTR:
case FUNC_ROWID:
case FUNC_RAW:
case FIELD_ROWID:
case FIELD_RAW:
case VAR_MEMPTR:
case VAR_ROWID:
case VAR_RAW:
type = "base64Binary";
break;
}
if (type == null)
{
throw new NullPointerException("Unknown XSD type: " +
ProgressParser.lookupTokenName((int) par.type));
}
type = "xsd:" + type;
}
Element elPar = dom.createElement("element");
if (par.nodeName != null )
{
// this is non-null only for table fields
elPar.setAttribute("name", par.nodeName);
elPar.setAttribute("prodata:fieldName", par.name);
}
else
{
elPar.setAttribute("name", par.name);
}
elPar.setAttribute("type", type);
if (par.nillable)
{
elPar.setAttribute("nillable", "true");
}
if (proType != null)
{
elPar.setAttributeNS(NS_PRODATA, "prodata:dataType", "prodata:" + proType);
}
if (par.extent != SourceNameMapper.NO_EXTENT)
{
if (par.extent == -1)
{
elPar.setAttribute("maxOccurs", "unbounded");
elPar.setAttribute("minOccurs", "0");
}
else
{
elPar.setAttribute("maxOccurs", "" + par.extent);
elPar.setAttribute("minOccurs", "" + par.extent);
}
}
elSeq.appendChild(elPar);
}
}
return parent;
}
/**
* The configuration for the exported WSDL service.
*/
private class ServiceConfig
{
/** The parent service name. */
private final String parentService;
/** The service name. */
private final String name;
/** The service namespace. */
private final String ns;
/** The list of declared messages. */
private List<Element> messages = new ArrayList<>();
/** The root binding element for this service. */
private final Element elBinding;
/** The root port-type element for this service. */
private final Element elPortType;
/** The port elemnet for this service. */
private final Element elServicePort;
/** The schema element where to attach the declared service types. */
private final Element elSchema;
/** Flag indicating if a dataset-handle schema element was generated. */
private boolean hasDataSetHandleParam = false;
/** Flag indicating if a table-handle schema element was generated. */
private boolean hasTableHandleParam = false;
/**
* Create a new service.
*
* @param service
* The service name.
* @param idx
* The service index, used to compute the namespace.
* @param parentService
* The parent service name.
*/
public ServiceConfig(String service, int idx, String parentService)
{
this.name = service;
this.parentService = parentService;
this.ns = "S" + idx;
// create schema node for the types
elSchema = dom.createElement("schema");
elSchema.setAttribute("elementFormDefault", "qualified");
elSchema.setAttribute("targetNamespace", targetNamespace + ":" + name);
elSchema.setAttribute("xmlns", "http://www.w3.org/2001/XMLSchema");
elTypes.appendChild(elSchema);
// create the portType node
elPortType = dom.createElementNS(NS_WSDL, "wsdl:portType");
elPortType.setAttribute("name", name + portTypeBindingSuffix);
// create the binding node
elBinding = dom.createElementNS(NS_WSDL, "wsdl:binding");
elBinding.setAttribute("name", name + portTypeBindingSuffix);
elBinding.setAttribute("type", "tns:" + name + portTypeBindingSuffix);
Element elSoapBinding = dom.createElementNS(NS_SOAP, "soap:binding");
elSoapBinding.setAttribute("style", "document");
elSoapBinding.setAttribute("transport", "http://schemas.xmlsoap.org/soap/http");
elBinding.appendChild(elSoapBinding);
// create port node
elServicePort = dom.createElementNS(NS_WSDL, "wsdl:port");
elServicePort.setAttribute("name", name + portTypeBindingSuffix);
elServicePort.setAttribute("binding", "tns:" + name + portTypeBindingSuffix);
Element elDoc = dom.createElementNS(NS_WSDL, "wsdl:documentation");
elDoc.setTextContent(helpString);
elServicePort.appendChild(elDoc);
Element elAddress = dom.createElementNS(NS_SOAP, "soap:address");
elAddress.setAttribute("location", endpoint);
elServicePort.appendChild(elAddress);
}
}
}