SoapRequestParser.java
/*
** Module : SoapRequestParser.java
** Abstract : APIs to parse the SOAP request arguments.
**
** Copyright (c) 2020-2021, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------------Description----------------------------------------
** 001 CA 20200518 First version.
** CA 20200528 Fixes for extent parameters, BEFORE-TABLE and error management.
** 002 CA 20210304 Fixed date, datetime and datetimetz literal parsing.
** CA 20211013 DATASET parameters have the table table indexes, too, at the WSDL schema.
** CA 20211112 Refactored to allow transfer of the DATASET or TABLE via XML. Added support for
** DATASET-HANDLE and TABLE-HANDLE.
** 003 RNC 20241128 Implemented logic to recognize 'nil' attribute.
*/
/*
** 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.soap;
import java.io.*;
import java.lang.reflect.*;
import java.math.*;
import java.util.*;
import javax.servlet.http.*;
import javax.wsdl.*;
import javax.xml.bind.*;
import javax.xml.namespace.*;
import javax.xml.stream.*;
import org.apache.axiom.om.*;
import org.apache.axiom.om.util.*;
import org.apache.axis2.databinding.utils.*;
import com.goldencode.p2j.directory.Base64;
import com.goldencode.p2j.persist.*;
import com.goldencode.p2j.rest.*;
import com.goldencode.p2j.soap.WsdlConfig.*;
import com.goldencode.p2j.util.*;
/**
* Parse the service input arguments from the SOAP envelope (body or header).
*/
public class SoapRequestParser
extends ServiceArgumentsParser
{
/** The SOAP body element which encodes the request. */
private final OMElement soapRequest;
/** The map of service input parameters, (FWD-style) as defined at the WSDL operation. */
private final Map<String, LegacyServiceParameter> params = new HashMap<>();
/** The schema definition for the service input parameters, as defined at the schema. */
private final Map<String, BaseSchemaType> soapParams;
/** The list of service input parameters (FWD-style) as defined at the WSDL operation. */
private final LegacyServiceParameter[] serviceParameters;
/**
* Initialize this instance, to parse the input parameters for the SOAP operation.
*
* @param params
* The output parameters for the SOAP operation.
* @param soapRequest
* The SOAP request body.
* @param soapOperation
* The SOAP operation as defined in the WSDL.
* @param wsdlConfig
* The WSDL configuration.
*/
public SoapRequestParser(LegacyServiceParameter[] params,
OMElement soapRequest,
Operation soapOperation,
WsdlConfig wsdlConfig)
{
this.soapRequest = soapRequest;
this.serviceParameters = params;
for (LegacyServiceParameter lsp : params)
{
this.params.put(lsp.name().toLowerCase(), lsp);
}
soapParams = wsdlConfig.resolveParameters(soapOperation);
}
/**
* Parse an extent argument.
*
* @param body
* The HTTP body.
* @param request
* The request payload.
* @param idx
* The argument's index.
* @param source
* The arguments's source.
* @param type
* The arguments's type.
* @param extent
* The arguments's length.
*
* @return An array with the argument's values.
*/
@Override
protected Object[] parseExtentArgument(String body,
HttpServletRequest request,
int idx,
String source,
String type,
int extent)
throws RequestArgumentError
{
LegacyServiceParameter lsp = serviceParameters[idx];
BaseSchemaType bst = soapParams.get(lsp.name());
Class<?> cls = BaseDataType.fromTypeName(type);
List<BaseDataType> vals = new ArrayList<>();
Iterator<OMElement> iter = getSoapParameters(source);
while (iter.hasNext())
{
OMElement el = iter.next();
BaseDataType bdt = BaseDataType.generateUnknown(cls);
bdt.assign(createArgument(bst.xsdType, cls, el.getText()));
vals.add(bdt);
}
int length = extent == SourceNameMapper.DYNAMIC_EXTENT ? vals.size() : extent;
if (extent != SourceNameMapper.DYNAMIC_EXTENT && length != vals.size() && !vals.isEmpty())
{
// must match
throw new RequestArgumentError("extent not match");
}
Object arg = Array.newInstance(cls, length);
for (int i = 0; i < vals.size(); i++)
{
Array.set(arg, i, vals.get(i));
}
for (int i = vals.size(); i < length; i++)
{
Array.set(arg, i, BaseDataType.generateUnknown(cls));
}
return (Object[]) arg;
}
/**
* Assign the given argument to the specified value.
* <p>
* This will assign the <code>bdt</code> instance, depending on the argument's configured XSD type.
*
* @param idx
* The argument's index.
* @param bdt
* The argument's {@link BaseDataType} instance (may be <code>null</code>).
* @param sval
* The string representation of this argument.
*/
@Override
protected void assignArgument(int idx, BaseDataType bdt, String sval)
throws RequestArgumentError
{
LegacyServiceParameter lsp = serviceParameters[idx];
BaseSchemaType bst = soapParams.get(lsp.name());
QName nilQName = new QName("http://www.w3.org/2001/XMLSchema-instance", "nil");
String nilValue = getSoapParameter(lsp.source()).getAttributeValue(nilQName);
if (nilValue == null || nilValue.equalsIgnoreCase("false"))
{
bdt.assign(createArgument(bst.xsdType, bdt.getClass(), sval));
}
else if (nilValue.equalsIgnoreCase("true"))
{
bdt.setUnknown();
}
else
{
throw new RequestArgumentError("Invalid boolean value: " + nilValue + " (10915)", true);
}
}
/**
* Parse the argument, by interpreting the request.
*
* @param body
* The request body.
* @param source
* The parameter's encoded source.
* @param request
* The request payload.
*
* @return The resolved argument.
*/
@Override
protected String parseArgumentInt(String body, String source, HttpServletRequest request)
throws IOException
{
OMElement parEl = getSoapParameter(source);
LegacyServiceParameter lsp = getLegacyParameter(source);
switch (lsp.type().toUpperCase())
{
case "DATASET":
case "DATASET-HANDLE":
case "TABLE":
case "TABLE-HANDLE":
return source;
default:
return parEl == null ? null : parEl.getText();
}
}
/**
* Load the specified table.
*
* @param content
* The table definition.
*
* @return A {@link TableWrapper} instance to be passed as argument to the remote call.
*/
@Override
protected TableWrapper loadTable(String content)
throws IOException,
RequestArgumentError
{
OMElement parEl = getSoapParameter(content);
LegacyServiceParameter lsp = getLegacyParameter(content);
if (parEl == null)
{
// the table is not specified, create an 'empty' node
try
{
parEl = AXIOMUtil.stringToOM("<" + lsp.name() + "/>");
}
catch (XMLStreamException e)
{
throw new RuntimeException(e);
}
}
else if ("TABLE-HANDLE".equalsIgnoreCase(lsp.type()))
{
// in this case, the XML looks like this:
// root element: the TABLE-HANDLE parameter name
// > first child: -> TempTable node
// > first child: the schema
// > second child: the table with its rows (the schema node must be moved here)
// we need to move the schema node to the proper child and use that XML
OMElement schemaEl = null;
OMElement tableEl = null;
Iterator<OMElement> iter1 = parEl.getChildrenWithLocalName("TempTable");
if (iter1.hasNext())
{
Iterator<OMElement> iter = iter1.next().getChildElements();
while (iter.hasNext())
{
OMElement child = iter.next();
if (child.getNamespace() != null &&
child.getNamespace().getNamespaceURI().equals("http://www.w3.org/2001/XMLSchema"))
{
schemaEl = child;
}
else
{
if (tableEl != null)
{
tableEl = null;
break;
}
tableEl = child;
}
}
}
if (schemaEl != null && tableEl != null)
{
schemaEl.detach();
Iterator<OMElement> iter = tableEl.getChildElements();
List<OMElement> rows = new ArrayList<>();
while (iter.hasNext())
{
OMElement row = iter.next();
rows.add(row);
}
for (OMElement row : rows)
{
row.detach();
}
// schema must be first!
tableEl.addChild(schemaEl);
for (OMElement row : rows)
{
tableEl.addChild(row);
}
parEl = tableEl;
}
else
{
// TODO: error ?
}
}
SoapHandler.removeNamespaces(parEl);
String xmlTable = parEl.toString();
TableWrapper wrapper = new TableWrapper(lsp.input(), lsp.output(), false);
wrapper.setXmlTable(xmlTable);
wrapper.setAsXml(true);
return wrapper;
}
/**
* Create a new {@link DatasetWrapper}, so that the transfer is made via XML.
*
* @param name
* The dataset name.
* @param input
* The INPUT mode.
* @param output
* The OUTPUT mode.
* @param asHandle
* Flag indicating if this is a DATASET-HANDLE parameter.
*
* @return A new {@link DatasetWrapper} for XML transfer.
*/
@Override
protected DatasetWrapper createDataset(String name, boolean input, boolean output, boolean asHandle)
{
DataSetSchemaType dst = name == null || name.isEmpty() ? null : (DataSetSchemaType) soapParams.get(name);
DatasetWrapper wrapper = new DatasetWrapper(input, output, asHandle);
wrapper.setAsXml(true);
wrapper.setUseBeforeImage(dst != null && dst.useBeforeImage);
return wrapper;
}
/**
* Create a new {@link TableWrapper}, so that the transfer is made via XML.
*
* @param name
* The table name.
* @param input
* The INPUT mode.
* @param output
* The OUTPUT mode.
* @param asHandle
* Flag indicating if this is a DATASET-HANDLE parameter.
*
* @return A new {@link TableWrapper} for XML transfer.
*/
@Override
protected TableWrapper createTable(String name, boolean input, boolean output, boolean asHandle)
{
TableSchemaType tst = name == null || name.isEmpty() ? null : (TableSchemaType) soapParams.get(name);
TableWrapper wrapper = new TableWrapper(input, output, asHandle);
wrapper.setAsXml(true);
return wrapper;
}
/**
* Load the specified dataset.
*
* @param content
* The dataset definition.
*
* @return A {@link DatasetWrapper} instance to be passed as argument to the remote call.
*/
@Override
protected DatasetWrapper loadDataSet(String content)
throws IOException,
RequestArgumentError
{
OMElement parEl = getSoapParameter(content);
LegacyServiceParameter lsp = getLegacyParameter(content);
if (parEl == null)
{
// the dataset is not specified, create an 'empty' node
try
{
parEl = AXIOMUtil.stringToOM("<" + lsp.name() + "/>");
}
catch (XMLStreamException e)
{
throw new RuntimeException(e);
}
}
else if ("DATASET-HANDLE".equalsIgnoreCase(lsp.type()))
{
// in this case, the XML looks like this:
// root element: the DATASET-HANDLE parameter name
// first child: the schema
// second child: the dataset
// we need to move the schema node to the second child and use that XML
OMElement schemaEl = null;
OMElement dsEl = null;
Iterator<OMElement> iter = parEl.getChildElements();
while (iter.hasNext())
{
OMElement child = iter.next();
if (child.getNamespace() != null &&
child.getNamespace().getNamespaceURI().equals("http://www.w3.org/2001/XMLSchema"))
{
schemaEl = child;
}
else
{
if (dsEl != null)
{
dsEl = null;
break;
}
dsEl = child;
}
}
if (schemaEl != null && dsEl != null)
{
schemaEl.detach();
iter = dsEl.getChildElements();
List<OMElement> rows = new ArrayList<>();
while (iter.hasNext())
{
OMElement row = iter.next();
rows.add(row);
}
for (OMElement row : rows)
{
row.detach();
}
// schema must be first!
dsEl.addChild(schemaEl);
for (OMElement row : rows)
{
dsEl.addChild(row);
}
parEl = dsEl;
}
else
{
// TODO: error ?
}
}
SoapHandler.removeNamespaces(parEl);
String xmlDs = parEl.toString();
DatasetWrapper wrapper = new DatasetWrapper(lsp.input(), lsp.output(), false);
wrapper.setXmlDataset(xmlDs);
wrapper.setAsXml(true);
return wrapper;
}
/**
* Raises an error so that the request stops with a 5xx internal server error.
*
* @param bdt
* The BDT for which the value could not be parsed.
* @param val
* The attempted value to assign.
*/
@Override
protected void notAValue(BaseDataType bdt, String val)
throws RequestArgumentError
{
if (val == null)
{
val = "?";
}
String type = bdt.getTypeName().toLowerCase();
boolean quote = true;
switch (bdt.getClass().getSimpleName())
{
case "integer":
type = "integer";
break;
case "recid":
case "int64":
type = "long";
break;
case "decimal":
type = "decimal";
break;
case "logical":
type = "boolean";
break;
case "date":
type = "date";
break;
case "datetimetz":
quote = false;
if (val.isEmpty())
{
throw new RequestArgumentError("String index out of range: 0 (10914)");
}
type = "dateTime";
break;
case "datetime":
if (val.isEmpty())
{
throw new RequestArgumentError("String index out of range: 0 (10914)");
}
if (val.equals("?"))
{
val = "";
}
type = "dateTime";
break;
}
if (quote)
{
val = "''''" + val + "''''";
}
throw new RequestArgumentError("Invalid " + type + " value " + val + " sent to deserializer (10914)");
}
/**
* Get the service parameter with the given name.
*
* @param source
* The parameter's name.
*
* @return The {@link LegacyServiceParameter} from the {@link #params} map.
*/
private LegacyServiceParameter getLegacyParameter(String source)
{
if (!source.startsWith("SOAP:"))
{
throw new RuntimeException("Invalid SOAP source: " + source);
}
source = source.substring("SOAP:".length());
source = source.toLowerCase();
return params.get(source);
}
/**
* Get the SOAP element from the envelope's body, for the specified parameter.
*
* @param source
* The parameter name.
*
* @return The SOAP element for this parameter, from the envelope's body.
*/
private OMElement getSoapParameter(String source)
{
if (!source.startsWith("SOAP:"))
{
throw new RuntimeException("Invalid SOAP source: " + source);
}
source = source.substring("SOAP:".length());
// source = source.toLowerCase();
BaseSchemaType bst = soapParams.get(source);
String xmlName = bst.xmlName;
QName parqn = new QName(soapRequest.getQName().getNamespaceURI(), xmlName);
OMElement parEl = soapRequest.getFirstChildWithName(parqn);
return parEl == null ? soapRequest.getFirstChildWithName(new QName(xmlName)) : parEl;
}
/**
* Get the SOAP elements from the envelope's body, for the specified parameter.
*
* @param source
* The parameter name.
*
* @return The SOAP elements for this parameter, from the envelope's body.
*/
private Iterator<OMElement> getSoapParameters(String source)
{
if (!source.startsWith("SOAP:"))
{
throw new RuntimeException("Invalid SOAP source: " + source);
}
source = source.substring("SOAP:".length());
// source = source.toLowerCase();
QName parqn = new QName(soapRequest.getQName().getNamespaceURI(), source);
return soapRequest.getChildrenWithName(parqn);
}
/**
* Create a new {@link BaseDataType} instance and restore its value from the given string,
* based on the XSD type.
*
* @param xsdType
* The parameter's XSD type.
* @param type
* The BDT sub-class.
* @param sval
* The string representation of this argument.
*
* @return The new {@link BaseDataType} or {@link MemoryBuffer} instance for this argument.
*/
private Object createArgument(String xsdType, Class<?> type, String sval)
throws RequestArgumentError
{
BaseDataType bdt = BaseDataType.generateUnknown(type);
if (type == handle.class)
{
notAValue(bdt, sval);
return bdt;
}
if (sval == null)
{
return bdt;
}
switch (xsdType)
{
case "xsd:string":
bdt.assign(DatatypeConverter.parseString(sval));
break;
case "xsd:int":
bdt.assign(new BigInteger(Long.toString(DatatypeConverter.parseLong(sval))));
break;
case "xsd:long":
bdt.assign(new BigInteger(Long.toString(DatatypeConverter.parseLong(sval))));
break;
case "xsd:decimal":
if (sval.isEmpty())
{
notAValue(bdt, "");
}
bdt.assign(DatatypeConverter.parseDecimal(sval));
break;
case "xsd:dateTime":
case "xsd:date":
if (sval.isEmpty())
{
notAValue(bdt, "");
}
else if (sval.equals("?"))
{
notAValue(bdt, "?");
}
if (bdt.getClass() == date.class)
{
// we need only the date part
if (sval.indexOf("T") > 0)
{
sval = sval.substring(0, sval.indexOf("T"));
}
}
else if (bdt.getClass() == datetime.class)
{
// TODO: we need only the datetime part
}
date d = null;
try
{
d = date.parseIso8601(sval);
if (d == null)
{
notAValue(bdt, sval);
}
}
catch (Throwable t)
{
notAValue(bdt, sval);
}
if (bdt instanceof datetimetz)
{
if (!(d instanceof datetimetz))
{
notAValue(bdt, sval);
}
((datetimetz) bdt).assign(d);
}
else if (bdt instanceof datetime)
{
if (!(d instanceof datetime))
{
notAValue(bdt, sval);
}
if (d instanceof datetimetz)
{
Calendar c = ((datetimetz) d).calendarValue();
((datetime) bdt).assign(new datetime(c.getTime(), c.getTimeZone()));
}
else
{
((datetime) bdt).assign(d);
}
}
else
{
if (d instanceof datetimetz)
{
Calendar c = ((datetimetz) d).calendarValue();
((date) bdt).assign(new date(c.getTime(), c.getTimeZone()));
}
else
{
((date) bdt).assign(d);
}
}
break;
case "xsd:boolean":
if (sval.isEmpty())
{
notAValue(bdt, "");
}
bdt.assign(ConverterUtil.convertToBoolean(sval));
break;
case "xsd:base64Binary":
byte[] bytes = Base64.safeBase64ToByteArray(sval);
if (bdt instanceof memptr || bdt instanceof blob)
{
MemoryBuffer mb = new MemoryBuffer(bytes);
return mb;
}
if (bdt instanceof rowid)
{
if (bytes.length > 0)
{
long val = bytes[0] & 0xffL;
for (int i = 1; i < Math.min(bytes.length, 8); i++)
{
val += (bytes[i] & 0xffL) << (8 * i);
}
((rowid) bdt).assign(new rowid(val));
}
}
else
{
((BinaryData) bdt).assign(bytes);
}
break;
}
return bdt;
}
}