SoapResponseArguments.java
/*
** Module : SoapResponseArguments.java
** Abstract : APIs to manage the response arguments in the HTTP servlet response, for an executed SOAP request.
**
** Copyright (c) 2020-2025, 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 Added XSD namespace.
** CA 20210908 Do not serialize using Axis2 OMFormat, as that will create a 'multipart/related' message,
** which is not how OE does it. If this is added back, the harness needs to be added support
** for this 'multiplart/related' mime type.
** CA 20211112 Refactored to allow transfer of the DATASET or TABLE via XML. Added support for
** DATASET-HANDLE and TABLE-HANDLE.
** CA 20211214 For State-free appservers, the agents can be bound to remote persistent procedures; this
** binding must be made using the agent's ID, as a client can invoke multiple remote
** persistent procedures, and using the procedure's ID for this binding can lead to
** collisions, as the pair (connection ID, procedure ID) is not guaranteed to be unique for
** a connection.
** CA 20211227 null or empty XML for dataset or table must be interpreted as unknown response.
** CA 20220206 Add the empty default namespace to the schema and the dataset element, for dataset handle.
** 003 TJD 20240220 Removed compiler warnings related with libraries updates
** 004 GBB 20240610 Error message to be taken from reason msg, if returnValue not present.
** 005 RNC 20241115 Modified flushArguments() to use a custom escaper for XML output.
** 006 RNC 20241128 Fixed error message in writeError().
** 007 RAA 20241217 Added XML declaration in flushArguments().
** 008 AL2 20250325 Let the SOAP response arguments hook onto a reset event in order to drop its
** serializer and avoid memory leaks.
** 009 GBB 20250403 Fixing param names and javadoc.
*/
/*
** 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.wsdl.Part;
import javax.wsdl.extensions.ExtensibilityElement;
import javax.wsdl.extensions.soap.SOAPHeader;
import javax.xml.bind.DatatypeConverter;
import javax.xml.namespace.*;
import javax.xml.stream.*;
import com.goldencode.util.*;
import org.apache.axiom.om.*;
import org.apache.axiom.om.util.*;
import org.apache.axiom.soap.*;
import org.apache.axiom.soap.SOAPFactory;
import org.apache.axiom.soap.SOAPFault;
import org.codehaus.stax2.XMLOutputFactory2;
import com.goldencode.p2j.oo.lang.*;
import com.goldencode.p2j.persist.*;
import com.goldencode.p2j.rest.*;
import com.goldencode.p2j.soap.WsdlConfig.*;
import com.goldencode.p2j.util.*;
import com.goldencode.p2j.xml.CustomXMLEscapes;
/**
* Helper class to serialize a SOAP response.
*/
public class SoapResponseArguments
extends ResponseArguments
{
/** The envelope holding the response. */
private final SOAPEnvelope response;
/** The root element defining the response, in the SOAP body. */
private final OMElement soapResponse;
/** The XSI namespace. */
private final OMNamespace nsXSI;
/** The XSD namespace. */
private final OMNamespace nsXSD;
/** The SOAP output parameters' schema, by their name. */
private final Map<String, BaseSchemaType> soapParams;
/** SOAP factory to create new elements. */
private final SOAPFactory soapFactory;
/** The output defined in this operation's WSDL. */
private final Output output;
/** The WSDL configuration. */
private final WsdlConfig wsdlConfig;
/** The request ID for the SOAP request to which these arguments belong. */
private String requestID = null;
/** Flag indicating that we are not writing a table field. */
private boolean isField = false;
/**
* Initialize this instance, to write output parameters for the SOAP operation.
*
* @param soapOperation
* The SOAP operation as defined in the WSDL.
* @param wsdlConfig
* The WSDL configuration.
* @param multiSession
* Flag indicating that the remote appserver is in a multi-session agent mode.
*/
public SoapResponseArguments(Operation soapOperation, WsdlConfig wsdlConfig, boolean multiSession)
{
soapFactory = OMAbstractFactory.getSOAP11Factory();
OMNamespace soapNS = soapFactory.createOMNamespace("http://schemas.xmlsoap.org/soap/envelope/",
"SOAP-ENV");
response = soapFactory.createSOAPEnvelope(soapNS);
if (multiSession)
{
soapFactory.createSOAPHeader(response);
}
soapFactory.createSOAPBody(response);
nsXSI = response.declareNamespace("http://www.w3.org/2001/XMLSchema-instance", "xsi");
nsXSD = response.declareNamespace("http://www.w3.org/2001/XMLSchema", "xsd");
response.declareNamespace(nsXSD);
if (soapOperation != null)
{
output = soapOperation.getOutput();
if (output != null && output.getMessage() != null)
{
Part part = output.getMessage().getPart("parameters");
QName partqn = part.getElementName();
soapResponse = soapFactory.createOMElement(new QName(null, partqn.getLocalPart()), response.getBody());
soapResponse.addAttribute("xmlns", partqn.getNamespaceURI(), null);
}
else
{
soapResponse = null;
}
soapParams = wsdlConfig.resolveParameters(soapOperation);
}
else
{
soapParams = null;
soapResponse = null;
output = null;
}
this.wsdlConfig = wsdlConfig;
}
/**
* Serialize the error, to be included in the response.
*
* @param err
* The request argument error.
* @param multiSession
* Flag indicating that the remote appserver is in multi-session agent mode.
*/
public void writeError(RequestArgumentError err, boolean multiSession)
{
if (multiSession)
{
writeError("Exception unmarshalling a parameter value",
"An error was detected in the Web Service request. (10894)");
}
else
{
String header = err.getBodyError() ? "Error in SOAP body: " : "Error in SOAP parameter: ";
writeError(header + err.getMessage(),
"An error was detected in the Web Service request. (10894)");
}
}
/**
* Serialize the error, to be included in the response.
*
* @param err
* The error.
*
* @return The serialized version of the error.
*/
@Override
public String writeError(LegacyError err)
{
String msg = "";
String reason = "";
if (err instanceof AppError && ((AppError) err).isFromReturn())
{
reason = "An error was detected while executing the Web Service request. (10893)";
msg = ((AppError) err).getReturnValue().toStringMessage();
}
else
{
reason = "An error was detected in the Web Service request. (10894)";
int numErrors = err.getNumMessages().intValue();
integer idx = new integer();
for (int i = 1; i <= numErrors; i++)
{
idx.assign(i);
msg = err.getMessage(idx).toStringMessage();
msg = "Error in SOAP parameter: " + msg;
break;
}
}
writeError(msg, reason, true);
return ""; // we are writing to envelope
}
/**
* Serialize the error, to be included in the response.
*
* @param msg
* The message to be used as the SOAP fault code ('faultString').
* @param reason
* The message to be used as the SOAP fault detail.
*/
public void writeError(String msg, String reason)
{
writeError(msg, reason, false);
}
/**
* Serialize the error, to be included in the response.
*
* @param returnValue
* The message to be used as the SOAP fault code ('faultString').
* @param reason
* The message to be used as the SOAP fault detail.
* @param server
* Flag indicating if this is a server error.
*/
public void writeError(String returnValue, String reason, boolean server)
{
if (soapResponse != null)
{
soapResponse.detach();
}
SOAPFault fault = soapFactory.createSOAPFault(response.getBody());
SOAPFaultCode code = soapFactory.createSOAPFaultCode(fault);
code.setText(response.getBody().getQName().getPrefix() + (server ? ":Server" : ":Client"));
SOAPFaultReason elReason = soapFactory.createSOAPFaultReason(fault);
elReason.setText(reason);
SOAPFaultDetail detail = soapFactory.createSOAPFaultDetail(fault);
QName qn = new QName("urn:soap-fault:details", "FaultDetail", "ns1");
OMElement faultDetail = soapFactory.createOMElement(qn, detail);
qn = new QName(null, "errorMessage");
OMElement elMsg = soapFactory.createOMElement(qn, faultDetail);
elMsg.setText(StringHelper.hasContent(returnValue) ? returnValue : reason);
if (requestID != null)
{
qn = new QName(null, "requestID");
OMElement elReqID = soapFactory.createOMElement(qn, faultDetail);
elReqID.setText(requestID);
}
}
/**
* Write the persistent procedure ID to the SOAP header, as a response for a persistent procedure
* request.
*
* @param connectionID
* The connection ID used by this request.
* @param resourceID
* The persistent procedure ID.
* @param agentId
* The agent ID where this remote procedure was created.
* @param binding
* The operation definition in the WSDL binding, for the response.
*/
public void writeProcedureID(String connectionID, String resourceID, int agentId, BindingOperation binding)
{
BindingOutput output = binding.getBindingOutput();
Iterator<ExtensibilityElement> iter = output.getExtensibilityElements().iterator();
SOAPHeader outputHeader = null;
while (iter.hasNext())
{
ExtensibilityElement exEl = iter.next();
if (exEl instanceof SOAPHeader)
{
outputHeader = (SOAPHeader) exEl;
break;
}
}
if (outputHeader != null)
{
if (response.getHeader() == null)
{
soapFactory.createSOAPHeader(response);
}
String outputPart = outputHeader.getPart();
QName outputMsg = outputHeader.getMessage();
Message msg = wsdlConfig.getDefinition().getMessage(outputMsg);
WSDLSchema msgSchema = wsdlConfig.resolveSchema(msg, outputMsg.getLocalPart());
OMElement partEl = soapFactory.createOMElement(new QName(null, outputPart), response.getHeader());
partEl.addAttribute("xmlns", msgSchema.getTargetNamespace(), null);
OMElement uuidEl = soapFactory.createOMElement(new QName(null, "UUID"), partEl);
uuidEl.setText(connectionID + "#" + resourceID + "#" + agentId);
}
}
/**
* Get the string representation of this instance.
*
* @param bdt
* The instance.
*
* @return The string representation, using {@link DatatypeConverter}.
*/
protected String toString(BaseDataType bdt)
{
switch (bdt.getClass().getSimpleName())
{
case "decimal":
BigDecimal bd = new BigDecimal(((decimal) bdt).toStringExport());
if (isField && bd.scale() == 0)
{
bd = bd.setScale(1);
}
return DatatypeConverter.printDecimal(bd);
case "rowid":
long val = ((rowid) bdt).getValue();
byte[] bytes = Utils.longToBytesLE(val);
int used = bytes.length;
for (int i = bytes.length - 1; i >= 0 && bytes[i] == 0; i--, used--)
{
// calculates the number of used bytes
}
byte[] data = new byte[used];
System.arraycopy(bytes, 0, data, 0, used);
return DatatypeConverter.printBase64Binary(data);
default:
return super.toString(bdt);
}
}
/**
* Determine if unknown values must be ignored.
*
* @return Always <code>false</code>.
*/
@Override
protected boolean ignoreUnknown()
{
return false;
}
/**
* Serialize this argument's values, considering the target format.
*
* @param stream
* The response stream.
* @param target
* The argument's encoded target.
* @param val
* The argument's value.
* @param response
* The HTTP response.
*/
@Override
protected void writeExtentArgument(OutputStream stream,
String target,
Object val,
HttpServletResponse response)
throws IOException
{
int length = Array.getLength(val);
for (int i = 0; i < length; i++)
{
Object extval = Array.get(val, i);
writeArgument(stream, target, extval, response);
}
}
/**
* Serialize this argument's value, considering the target format.
*
* @param stream
* The response stream.
* @param target
* The argument's encoded target.
* @param sval
* The argument's string-converted value.
* @param val
* The argument's value.
* @param response
* The HTTP response.
*/
@Override
protected void writeArgumentInt(OutputStream stream,
String target,
String sval,
Object val,
HttpServletResponse response)
throws IOException
{
if (val instanceof OMElement)
{
// was added explicitly by someone else
return;
}
if (!target.startsWith("SOAP:"))
{
throw new RuntimeException("Invalid SOAP target: " + target);
}
target = target.substring("SOAP:".length());
QName parqn = new QName(null, target);
OMElement argEl = soapFactory.createOMElement(parqn, soapResponse);
if (val == null || ((val instanceof BaseDataType) && ((BaseDataType) val).isUnknown()))
{
argEl.addAttribute("nil", "true", nsXSI);
}
else
{
argEl.setText(sval);
}
}
/**
* Flush the arguments to the response stream (i.e. HTTP body).
*
* @param stream
* The stream to write the parameters.
* @param response
* The servlet response.
*/
@Override
protected void flushArguments(OutputStream stream, HttpServletResponse response)
throws IOException
{
// CA: OMOutputFormat will force a 'multipart/related' content type which is not what OE does.
// If this is added back, the harness needs to be added support for this 'multiplart/related' mime type.
// OMOutputFormat format = new OMOutputFormat();
// format.setDoOptimize(true);
// format.setCharSetEncoding("UTF-8");
// response.setContentType(format.getContentType());
// response.setCharacterEncoding(format.getCharSetEncoding());
XMLOutputFactory factory = XMLOutputFactory2.newFactory();
factory.setProperty(XMLOutputFactory2.P_TEXT_ESCAPER, new CustomXMLEscapes());
XMLStreamWriter xmlStreamWriter;
try
{
xmlStreamWriter = factory.createXMLStreamWriter(stream);
xmlStreamWriter.writeStartDocument("UTF-8", "1.0"); // write the XML declaration
this.response.serialize(xmlStreamWriter, false);
xmlStreamWriter.flush();
xmlStreamWriter.close();
}
catch (XMLStreamException e)
{
throw new IOException(e);
}
}
/**
* Serialize the specified table.
*
* @param target
* The target OUTPUT parameter.
* @param val
* The table result set, as received from the remote side.
*
* @return The serialized version of this table.
*/
@Override
protected Object writeTable(String target, TableWrapper val)
{
String tableXml = val.getXmlTable();
if (tableXml == null || tableXml.trim().isEmpty())
{
return ""; // TODO: what to write?
}
try
{
OMElement el = AXIOMUtil.stringToOM(tableXml);
SoapHandler.removeNamespaces(el);
if (val.isTableHandle())
{
// if TABLE-HANDLE, add the table to the parameter name
String targetName = target.substring(target.indexOf(':') + 1);
OMElement elRoot = AXIOMUtil.stringToOM("<" + targetName + "/>");
OMElement dsNode = AXIOMUtil.stringToOM("<DataSet/>");
dsNode.addAttribute("xmlns", "", null);
elRoot.addChild(dsNode);
// get the schema node
OMElement elSchema = null;
Iterator<OMElement> iter = el.getChildElements();
while (iter.hasNext())
{
OMElement elChild = iter.next();
if (elChild.getNamespaceURI().equals("http://www.w3.org/2001/XMLSchema"))
{
elSchema = elChild;
break;
}
}
String tableName = el.getLocalName();
if (elSchema != null)
{
elSchema.detach();
dsNode.addChild(elSchema);
// change the 'table' to 'Data' and 'tableRow' to 'Item'
OMElement el2 = (OMElement) elSchema.getChildElements().next();
el2.addAttribute("name", "Data", null);
el2 = (OMElement) el2.getChildElements().next();
el2 = (OMElement) el2.getChildElements().next();
el2 = (OMElement) el2.getChildElements().next();
el2.addAttribute("name", "Item", null);
}
// rename the "table" to "Data" and "tableRow" to "Item"
el.setLocalName("Data");
el.addAttribute(soapResponse.getAttribute(new QName("xmlns")));
iter = el.getChildrenWithLocalName(tableName + "Row");
while (iter.hasNext())
{
OMElement row = iter.next();
row.setLocalName("Item");
}
dsNode.addChild(el);
el = elRoot;
}
soapResponse.addChild(el);
return el;
}
catch (XMLStreamException e)
{
throw new RuntimeException(e);
}
}
/**
* Serialize the specified dataset.
*
* @param target
* The target OUTPUT parameter.
* @param val
* The dataset, as received from the remote side.
*
* @return The serialized version of this dataset.
*/
@Override
protected Object writeDataSet(String target, DatasetWrapper val)
{
String dsXml = val.getXmlDataset();
if (dsXml == null || dsXml.trim().isEmpty())
{
return "";
}
try
{
OMElement el = AXIOMUtil.stringToOM(dsXml);
SoapHandler.removeNamespaces(el);
if (val.isDatasetHandle())
{
// if DATASET-HANDLE, add the tables to <dsname> and as root the parameter name
String targetName = target.substring(target.indexOf(':') + 1);
OMElement elRoot = AXIOMUtil.stringToOM("<" + targetName + "/>");
// get the schema node
OMElement elSchema = null;
Iterator<OMElement> iter = el.getChildElements();
while (iter.hasNext())
{
OMElement elChild = iter.next();
if (elChild.getNamespaceURI().equals("http://www.w3.org/2001/XMLSchema"))
{
elSchema = elChild;
break;
}
}
if (elSchema != null)
{
elSchema.detach();
elRoot.addChild(elSchema);
elSchema.addAttribute("xmlns", "", null);
}
el.addAttribute("xmlns", "", null);
elRoot.addChild(el);
el = elRoot;
}
soapResponse.addChild(el);
return el;
}
catch (XMLStreamException e)
{
throw new RuntimeException(e);
}
}
/**
* Serialize the specified dataset.
*
* @param target
* The target OUTPUT parameter.
* @param val
* The dataset, as received from the remote side.
*
* @return The serialized version of this dataset.
*/
@Override
protected Object writeDataSet(String target, DataSetContainer val)
{
throw new RuntimeException("DataSetContainer can not be used for SOAP!");
}
/**
* Set the request ID for the executed SOAP service, to which this response belongs.
*
* @param requestID
* The request ID.
*/
void setRequestId(String requestID)
{
this.requestID = requestID;
}
/**
* Reset the state of the response arguments. For SOAP requests, arguments
* are bound to responses, so the serializer should be cleared to avoid leaking
* them to the thread scope.
*/
@Override
protected void reset()
{
this.serializer.set(null);
}
}