WebServiceImpl.java
/*
** Module : WebServiceImpl.java
** Abstract : Implementation of APIs specific to a client-side access to Web Services.
**
** Copyright (c) 2016-2024, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------Description---------------------------------
** 001 OM 20160323 Created initial version.
** 002 EVL 20180207 Fix for invalid char in comment issue.
** 003 OM 20180528 Replace backslash characters with slash to make the URL rfc1738 compliant.
** 004 SBI 20181224 Fixed validateParameters.
** 005 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
** 006 HC 20230517 Implemented server-side web service calling.
** 007 OM 20240510 Used constants instead of hardcoded literals.
** 008 TJD 20240208 Java 17 dependencies updates
** 009 GBB 20240610 Namespace for request body to be resolved from schema instead of wsdl definition.
** SOAP fault to be handled server-side.
** 010 CA 20240620 The context-local 'nohostverify' must be set and unset only when the SOAP request is executing,
** when in SSL mode.
** 011 GBB 20240826 Moving to osresource package. Rename 'single' to 'local'.
*/
/*
** 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.util.osresource;
import com.goldencode.p2j.net.*;
import com.goldencode.p2j.security.*;
import com.goldencode.p2j.util.*;
import com.goldencode.p2j.util.ErrorManager;
import com.goldencode.p2j.util.logging.*;
import org.apache.axiom.om.*;
import org.apache.axiom.om.util.*;
import org.apache.axiom.soap.*;
import org.apache.axiom.soap.SOAPHeader;
import org.apache.axis2.*;
import org.apache.axis2.addressing.*;
import org.apache.axis2.client.*;
import org.apache.axis2.context.*;
import org.apache.axis2.kernel.http.*;
import org.apache.axis2.transport.http.impl.httpclient4.*;
import org.apache.axis2.wsdl.*;
import org.apache.ws.commons.schema.*;
import javax.wsdl.*;
import javax.wsdl.Binding;
import javax.wsdl.Message;
import javax.wsdl.extensions.*;
import javax.wsdl.extensions.schema.*;
import javax.wsdl.extensions.soap.*;
import javax.wsdl.extensions.soap12.*;
import javax.wsdl.factory.*;
import javax.wsdl.xml.*;
import javax.xml.namespace.*;
import javax.xml.stream.*;
import java.util.*;
import java.util.logging.*;
/**
* Client-side implementation of WebService. P2J delegates all call here so that the 'remote'
* procedure is called form P2J client-side and benefit from true client network environment
* (firewall setting, host of request from POV of the machine that hosts the WebService.)
*/
class WebServiceImpl
implements WebService,
SessionListener
{
/** Logger */
private static final CentralLogger LOG = CentralLogger.get(WebServiceImpl.class.getName());
/** The context-local data of this class. */
private static ContextLocal<WorkArea> local =
new ContextLocal<WorkArea>()
{
protected WorkArea initialValue()
{
return new WorkArea();
}
};
/** Token used to authenticate with the dispatcher when registering APIs. */
private static Object modToken = null;
/** The single instance available of this class. */
private static final ContextLocal<WebServiceImpl> instance = new ContextLocal<>();
/** A SOAP factory to create message data. */
private static final org.apache.axiom.soap.SOAPFactory soapFactory =
OMAbstractFactory.getSOAP11Factory();
/**
* Private c'tor, to not allow explicit usage of this class. Its intended goal is to be
* exported as a network server.
*
* @param local
* <code>true</code> to register purely on a local basis (no remote/network access to these
* methods).
*/
private WebServiceImpl(boolean local)
{
synchronized (WebServiceImpl.class)
{
modToken = RemoteObject.registerServer(WebService.class, this, modToken, local);
}
}
/**
* Retrieve the target instance.
*
* @param local
* <code>true</code> to register purely on a local basis (no remote/network access to these
* methods).
*/
static synchronized WebServiceImpl getInstance(boolean local)
{
WebServiceImpl ws = instance.get();
if (ws == null)
{
ws = new WebServiceImpl(local);
SessionManager.get().getSession().addSessionListener(ws);
instance.set(ws);
}
return ws;
}
/**
* This callback will be called after the security context has been set up.
*
* @param session
* The session that is starting.
*/
@Override
public void initialize(Session session)
{
// no-ops
}
/**
* This method is called when the session is ending. We close and cleanup all resources
* allocated in the current context.
*
* @param session
* The session that is ending.
*/
@Override
public void terminate(Session session)
{
WorkArea wa = local.get();
synchronized (wa)
{
Map<Integer, WebServiceData> copy = new HashMap<>(wa.webServices);
for (WebServiceData webService : copy.values())
{
disconnect(webService.getId());
}
}
}
/**
* Validate parameters and establish a new connection using the given options.
* <p>
* The Web Services are not real connection and no actual connection remains open. Instead, the
* following actions are taken:
* <ol>
* <li>use network protocols to acquire the requested WSDL document
* <li>parses the document and verifies the passed in parameters against the configurations
* from WSDl document
* <li>in case of errors {@link ErrorManager} is used to report them
* <li>a new {@link WebServiceData} structure is created and populated with validated values
* and stored in context local data structures. At this moment object is populated
* only with <i>final</i> information needed for further service invocation. The
* object is assigned to a unique identifier that will be used from this point forward
* for accessing this connection. This {@code id} is returned in case of success.
* </ol>
* The returned id will be used in further references to this connection until the connection
* is disconnected.
* <p>
* The {@code #CONNECT_OPTION_MAX_CONNECTIONS}, {@code #CONNECT_OPTION_CONNECTION_LIFETIME},
* {@code #CONNECT_OPTION_NO_SESSION_REUSE} and {@code #CONNECT_OPTION_NO_HOST_VERIFY} options
* are not supported.
*
* @param options
* A collection of all parameters used for this connection.
*
* @return A positive (possible 0) integer representing the id of this connection to
* web-service server and a negative value representing an error code if any error
* occurred during validation and initialization of this connection. In the latter
* case the error was already reported. Check the {@link ErrorManager} for details
* about any errors.
*/
@Override
@SuppressWarnings("unchecked")
public int connect(WebServiceConnectOptions options)
{
// step 1. check if the WSDL document is available and can be parsed properly. Use the
// specified [WSDLUserid [WSDLPassword]]
Definition wsdlDef = null;
String url = options.getWsdlDocument().replace('\\', '/');
try
{
// TODO: use the WSDL userId and password
WSDLReader reader = WSDLFactory.newInstance().newWSDLReader();
// replace backslash characters with slash to make the URL rfc1738 compliant
wsdlDef = reader.readWSDL(url);
}
catch (WSDLException e)
{
String msg = String.format("Error loading WSDL document %s : %s",
url,
e.getMessage());
ErrorManager.recordOrShowError(11748, msg, false, false, false);
if (LOG.isLoggable(Level.SEVERE))
{
LOG.log(Level.SEVERE, msg, e);
}
return ERR_LOADING_WSDL_DOCUMENT;
}
// step2a. check if both service and binding are specified
if ((options.getServiceNamespace() != null || options.getPortName() != null) &&
(options.getBindingName() != null || options.getSoapEndpoint() != null))
{
final int[] errCodes;
final String[] errMsgs;
final String msg1 =
"If -Service and/or -Port are specified in the SERVER:CONNECT " +
"connection-parameters string, neither -Binding nor -SOAPEndpoint is valid";
final String msg2 =
"If '-Binding' is specified in the SERVER:CONNECT connection-parameters string, " +
"'-SOAPEndPoint' must also be specified";
if (options.getBindingName() != null && options.getSoapEndpoint() == null)
{
errCodes = new int[] { 11499, 11501 };
errMsgs = new String[] { msg1, msg2 };
}
else
{
errCodes = new int[] { 11499 };
errMsgs = new String[] { msg1 };
}
ErrorManager.recordOrShowError(errCodes, errMsgs, false, false, false);
return ERR_INVALID_PARAMS_IN_CONNECTION_STRING;
}
// step 2b. if -Binding is specified, -SoapEndpoint must be specified
if (options.getBindingName() != null && options.getSoapEndpoint() == null)
{
final String msg =
"If '-Binding' is specified in the SERVER:CONNECT connection-parameters string, " +
"'-SOAPEndPoint' must also be specified";
ErrorManager.recordOrShowError(11501, msg, false, false, false);
return ERR_INVALID_PARAMS_IN_CONNECTION_STRING;
}
// TODO: validate -ServiceNamespace and -BindingNamespace
String ns = wsdlDef.getTargetNamespace();
PortType portType = null;
Service service = null;
// step 3. only service or binding must be specified
if (options.getServiceName() != null)
{
// step 3a. check if specified [Service [ServiceNamespace]] [Port] are valid
service = wsdlDef.getService(new QName(ns, options.getServiceName()));
if (service == null)
{
String msg = String.format("The Service name %s is not in the WSDL document",
options.getServiceName());
ErrorManager.recordOrShowError(11736, msg, false, false, false);
return ERR_SERVICE_NOT_IN_WSDL_DOCUMENT;
}
final Port servicePort;
if (options.getPortName() != null)
{
servicePort = service.getPort(options.getPortName());
}
else
{
servicePort = getImplicitSoapPort(service.getPorts().values());
if (servicePort == null)
{
noServicesError();
return ERR_NO_SERVICE_ERROR;
}
}
// validate the port
Binding binding = servicePort == null ? null : servicePort.getBinding();
if (servicePort == null ||
binding == null ||
getExtension(binding.getExtensibilityElements(), SOAPBinding.class) == null)
{
String msg = String.format("The Port name %s is not in the WSDL document",
options.getPortName());
ErrorManager.recordOrShowError(11742, msg, false, false, false);
return ERR_PORT_NOT_IN_WSDL_DOCUMENT;
}
portType = binding.getPortType();
SOAPAddress soapAddress = getExtension(servicePort.getExtensibilityElements(),
SOAPAddress.class);
options.setSoapEndpoint((soapAddress == null) ? null : soapAddress.getLocationURI());
options.setSoapEndpointPassword(null);
options.setSoapEndpointUserid(null);
}
else if (options.getBindingName() != null)
{
// step 3b. check if specified [[Binding [BindingNamespace]] [SOAPEndpoint]] are valid
Binding binding = wsdlDef.getBinding(new QName(ns, options.getBindingName()));
if (binding != null &&
getExtension(binding.getExtensibilityElements(), SOAP12Binding.class) != null)
{
String msg = String.format(
"The %s:%s Binding is not supported, it must be a SOAP 1.1 binding",
ns, options.getBindingName());
ErrorManager.recordOrShowError(12100, msg, false, false, false);
return ERR_BINDING_NOT_SUPPORTED;
}
if (binding == null ||
getExtension(binding.getExtensibilityElements(), SOAPBinding.class) == null)
{
String msg = String.format(
"The %s:%s Binding name cannot be found in the WSDL document",
ns, binding);
ErrorManager.recordOrShowError(11740, msg, false, false, false);
return ERR_BINDING_NOT_IN_WSDL_DOCUMENT;
}
// compute the port type
portType = binding.getPortType();
// determine the target service, based on the binding and associated port type
for (Service srv : (Iterable<Service>) wsdlDef.getAllServices().values())
{
for (Port p : (Iterable<Port>) srv.getPorts().values())
{
if (p.getBinding().equals(binding))
{
service = srv;
break;
}
}
}
if (service == null)
{
String msg = String.format(
"Internal error locating Service and Port for Binding %s:%s",
ns, binding);
ErrorManager.recordOrShowError(11739, msg, false, false, false, true);
return ERR_SERVICE_AND_PORT_NOT_FOUND;
}
// TODO: check if [soapEndpoint] endpoint is a valid URL
}
else
{
// no explicit binding or service, get the default one.
Map services = wsdlDef.getServices();
if (services.size() != 1)
{
if (services.isEmpty())
{
noServicesError();
return ERR_NO_SERVICE_ERROR;
}
else
{
String msg = "A specific Service name must be specified from the WSDL document";
ErrorManager.recordOrShowError(11737, msg, false, false, false);
return ERR_NO_SERVICE_SPECIFIED;
}
}
service = (Service) services.values().iterator().next();
Port servicePort = getImplicitSoapPort(service.getPorts().values());
if (servicePort == null)
{
noServicesError();
return ERR_NO_SERVICE_ERROR;
}
Binding binding = servicePort.getBinding();
portType = binding.getPortType();
SOAPAddress soapAddress = getExtension(binding.getExtensibilityElements(),
SOAPAddress.class);
options.setSoapEndpoint((soapAddress == null ? null : soapAddress.getLocationURI()));
options.setSoapEndpointPassword(null);
options.setSoapEndpointUserid(null);
}
// step 4. check if the targetNamespace matches the WSDL document's targetNamespace
if (options.getTargetNamespace() != null &&
!options.getTargetNamespace().equals(wsdlDef.getTargetNamespace()))
{
String msg = String.format(
"The TargetNamespace option %s does not match the WSDL's target namespace",
options.getTargetNamespace());
ErrorManager.recordOrShowError(11750, msg, false, false, false, true);
return ERR_TARGETNAMESPACE_NOT_MATCH_WDSL;
}
// if everything is fine generate an id for this web service, register and return it
int id = Utils.uniqueId();
WebServiceData newWsd = new WebServiceData(id, wsdlDef, portType, service, options);
WorkArea wa = local.get();
synchronized (wa)
{
wa.addWebService(id, newWsd);
}
return id;
}
/**
* Get the name of the PortType of the specified connection.
*
* @param id
* The id of the connection.
*
* @return the name of the PortType of specified connection or empty string if there is no
* connection with requested id.
*/
public String getPortTypeName(int id)
{
WorkArea wa = local.get();
synchronized (wa)
{
WebServiceData serviceData = wa.locate(id);
if (serviceData != null)
{
return serviceData.portType.getQName().getLocalPart();
}
}
return "";
}
/**
* Disconnect the web service. If a web service invocation is in progress the internal state
* if dropped first.
*
* @param id
* The ID of the associated web service on P2J Client side.
*
* @return {@code true} if no errors were encountered during disconnect.
*/
@Override
public boolean disconnect(int id)
{
WorkArea wa = local.get();
synchronized (wa)
{
WebServiceData serviceData = wa.locate(id);
if (serviceData == null)
{
return false;
}
serviceData.cleanupInvocation();
wa.removeWebService(id);
}
return true;
}
/**
* Prepare the connection for an invocation of an operation on the Web Service. The parameters
* are validated and, on success, the internal state of the call is temporarily saved.
*
* @param id
* The id of an open connection.
* @param portTypeName
* The name of the port type to be invoked.
* @param operationName
* The name of the operation to be invoked on the Web Service.
* @param modes
* A string representation of the modes of each parameter. May not be {@code null}.
* @param args
* The operation's arguments.
*
* @return {@code true} if validation was successful and {@code false} if some errors occurred
* and invocation is not available (incorrect operation name, invalid parameters or
* could not build the request message).
*/
@SuppressWarnings("unchecked")
public boolean prepareInvoke(int id,
String portTypeName,
String operationName,
String modes,
Object... args)
{
WebServiceData wsd = null;
WorkArea wa = local.get();
synchronized (wa)
{
wsd = wa.locate(id);
if (wsd == null)
{
return false;
}
}
String ns = wsd.wsdlDef.getTargetNamespace();
PortType portType = wsd.wsdlDef.getPortType(new QName(ns, portTypeName));
Operation targetOp = null;
// validate the operation name
for (Operation op : (Iterable<Operation>) portType.getOperations())
{
// search is done case-insensitive
if (op.getName().equalsIgnoreCase(operationName))
{
targetOp = op;
break;
}
}
if (targetOp == null)
{
final String msg = "Failure initializing SOAP Call: " +
"Cannot find a SOAP Operation named %s (11775)";
ErrorManager.recordOrThrowError(11762, String.format(msg, operationName), false, true);
return false;
}
Object[] inputParams = collectParameters(modes, args, true);
Object[] outputParams = collectParameters(modes, args, false);
QName[][] inMsgParams = getMessageParameters(targetOp.getInput().getMessage(), wsd);
QName[][] outMsgParams = getMessageParameters(targetOp.getOutput().getMessage(), wsd);
if (!validateParameters(targetOp,
wsd.service,
inputParams,
outputParams,
inMsgParams.length,
outMsgParams.length))
{
return false;
}
// prepare the message to be sent as request to ws provider
OMElement reqMsg = buildInputMessage(wsd, targetOp, inMsgParams, inputParams);
if (reqMsg == null)
{
// if request message could not be created, abort
return false;
}
wsd.setRequestMessage(reqMsg);
wsd.setOutputParameters(outputParams, outMsgParams);
wsd.setTargetOperation(targetOp);
return true;
}
/**
* Get the wsdl namespace of this connection.
*
* @param id
* The id of an open connection.
*
* @return the wsdl namespace of this connection or {@code null} if {@code id} does not
* represent the identifier of a valid open connection.
*/
public String getNamespace(int id)
{
WorkArea wa = local.get();
synchronized (wa)
{
WebServiceData serviceData = wa.locate(id);
if (serviceData != null)
{
return serviceData.wsdlDef.getTargetNamespace();
}
}
return null;
}
/**
* Get the header entries of the response to last invoked operation on the specified connection.
*
* @param id
* The id of an open connection.
*
* @return the header entries of the specified connection or {@code null} if the {@code id} is
* not the identifier of an open and prepared connection or the last response envelope
* does not contain any headers.
*/
public String[] getHeaderEntries(int id)
{
WebServiceData wsd = null;
WorkArea wa = local.get();
synchronized (wa)
{
wsd = wa.locate(id);
if (wsd == null || wsd.getServiceClient() == null)
{
return null;
}
}
MessageContext resp = null;
try
{
resp = wsd.getServiceClient().getLastOperationContext()
.getMessageContext(WSDLConstants.MESSAGE_LABEL_IN_VALUE);
}
catch (AxisFault axisFault)
{
LOG.severe("", axisFault);
return null;
}
SOAPHeader soapHeader = resp.getEnvelope().getHeader();
if (soapHeader == null)
{
return null;
}
// collect the header entries
ArrayList<String> resHeaders = new ArrayList<>();
Iterator hiter = soapHeader.getHeadersToProcess(null);
while (hiter.hasNext())
{
SOAPHeaderBlock respHeaderEntry = (SOAPHeaderBlock) hiter.next();
resHeaders.add(respHeaderEntry.toString());
}
String[] res = new String[resHeaders.size()];
return resHeaders.toArray(res);
}
/**
* Clears the temporary state of an invocation. Does not close open connections. It is called
* when the result of the invocation was obtained on server side or when an error occurred. If
* {@code id} is invalid nothing happens.
*
* @param id
* The id of an open connection.
*/
public void cleanupInvocation(int id)
{
WorkArea wa = local.get();
synchronized (wa)
{
WebServiceData wsd = wa.locate(id);
if (wsd != null)
{
wsd.cleanupInvocation();
}
}
}
/**
* Do the actual Web Service invocation. The call uses an already open and prepared connection
* identified by {@code id}.
*
* @param id
* The id of an open connection.
* @param operationName
* The name of the operation to be invoked on the Web Service.
* @param portTypeName
* The name of the port type to be invoked.
* @param requestHeader
* The request header if any was obtained form a call to a configured header callback.
*
* @return {@code true} if the service operation was successful and {@code false} if
* {@code id} does not represent a valid connection or other error occurred.
*/
@SuppressWarnings("unchecked")
public boolean invoke(int id,
String operationName,
String portTypeName,
String requestHeader)
{
WebServiceData wsd = null;
WorkArea wa = local.get();
synchronized (wa)
{
wsd = wa.locate(id);
if (wsd == null || wsd.getTargetOperation() == null)
{
return false;
}
}
String ns = wsd.wsdlDef.getTargetNamespace();
String serviceName = wsd.service.getQName().getLocalPart();
Operation targetOp = wsd.getTargetOperation();
// invoke the remote operation
try
{
String portName = getPortName(portTypeName, wsd.wsdlDef);
ServiceClient sc = new ServiceClient(
null, wsd.wsdlDef, new QName(ns, serviceName), portName);
// compatibility issue, avoids "Transport error: 411 Error: Length Required" error
sc.getOptions().setProperty(HTTPConstants.CHUNKED, false);
sc.getOptions().setProperty(HTTPConstants.REUSE_HTTP_CLIENT, wsd.isSessionReuse());
wsd.setServiceClient(sc);
OMElement requestMessage = wsd.getRequestMessage();
// Industry standards supported by OpenEdge according to web services manual:
//
// - WSDL 1.1 is the de facto standard according to the W3C.
// - SOAP 1.1 HTTP Binding is the de facto standard according to the W3C.
// - XML Schema W3C 2001 XML Schema Recommendation.
//
// By default axis2 use SOAP 1.2 for binding. For compatibility when port name
// is automatically choose from the list of available ports by axis2 client we
// force also the use of SOAP 1.1 binding as default.
if (portName == null)
{
sc.getOptions().setSoapVersionURI(SOAP11Constants.SOAP_ENVELOPE_NAMESPACE_URI);
}
if (requestHeader != null)
{
// add the request header to the message
try
{
sc.addHeader(AXIOMUtil.stringToOM(requestHeader));
}
catch (XMLStreamException e)
{
if (LOG.isLoggable(Level.SEVERE))
{
final String msg = "Could not add the SOAP Request header: %s";
LOG.log(Level.SEVERE, String.format(msg, requestHeader), e);
}
}
}
// TODO: how to terminate a running operation?
// maybe invoke it async? (note that at this time, if asyncReq is not null, we
// are already in a different thread, thus we still need to act as we are in
// blocking mode.
if (wsd.getSoapEndpoint() != null)
{
sc.getOptions().setTo(new EndpointReference(wsd.getSoapEndpoint()));
}
if (wsd.getSoapEndpointUserid() != null)
{
HttpTransportPropertiesImpl.Authenticator auth =
new HttpTransportPropertiesImpl.Authenticator();
auth.setUsername(wsd.getSoapEndpointUserid());
auth.setPassword(wsd.getSoapEndpointPassword());
sc.getOptions().setProperty(HTTPConstants.AUTHENTICATE, auth);
}
LowLevelSocketImpl.setNoHostVerify(wsd.isNoHostVerify());
String schemaTargetNs = null;
for (XmlSchema schema : wsd.getXmlSchemas())
{
if (schema.getElementByName(operationName) != null)
{
// the same operation needs to be defined in only one schema
schemaTargetNs = schema.getTargetNamespace();
break;
}
}
if (schemaTargetNs == null)
{
LOG.log(Level.WARNING, "Operation %s not found in any schema in wsdl.", operationName);
return false;
}
// use the resolved operation name, not the received one, as AXIS is case-sensitive
OMElement res = sc.sendReceive(new QName(schemaTargetNs, targetOp.getName()), requestMessage);
wsd.setResponseMessage(res);
}
catch (AxisFault fault)
{
// the response-header callback is not invoked in case of SOAP faults
String faultActor = (fault.getNodeURI() == null) ? "" : fault.getNodeURI();
String faultString = (fault.getReason() == null) ? "" : fault.getReason();
String faultCode =
(fault.getFaultCode() == null) ? "" : fault.getFaultCode().getLocalPart();
int nlIdx = faultString.indexOf('\n');
String faultMsg = (nlIdx >= 0) ? faultString.substring(0, nlIdx) : faultString;
String faultDetailXML = fault.getFaultDetailElement() != null ?
fault.getFaultDetailElement().toString() :
null;
ErrorManager.handleSoapFault(operationName, faultMsg, faultActor, faultCode, faultString,
faultDetailXML);
return false;
}
finally
{
LowLevelSocketImpl.setNoHostVerify(false);
}
// TODO: test how interruptions are treated when we are blocked invoking the operation
return true;
}
/**
* Obtain the values of output parameters after a successful Web Service invocation.
*
* @param id
* The id of an open connection.
*
* @return An array with output values returned by the last web service operation.
*/
public Object[] getOutputValues(int id)
{
WebServiceData wsd = null;
WorkArea wa = local.get();
synchronized (wa)
{
wsd = wa.locate(id);
if (wsd == null || wsd.getResponseMessage() == null)
{
return null;
}
}
Object[] outputParams = wsd.getOutputParameters();
// process the response and set the output parameters
if (!processOutputMessage(wsd.getOutputMsgParameters(),
outputParams,
wsd.getResponseMessage()))
{
return null;
}
// in P4GL the response SOAP Envelope namespaces are merged with the namespace(s) in
// response. The response SOAP Envelope namespaces are fetched in reverse order from right
// to left
if (outputParams.length == 1 && outputParams[0] instanceof Text)
{
// get SOAP Envelope namespaces an merge with response namespaces.
String envns = null;
try
{
envns = getEnvelopeNameSpaces(wsd.getServiceClient());
}
catch (AxisFault axisFault)
{
LOG.severe("", axisFault);
return null;
}
mergeNameSpaces((Text) outputParams[0], envns);
}
return outputParams;
}
/**
* Search the given parameter modes and return the input or output parameters, based on the
* <code>input</code> flag.
* <p>
* The {@link InternalEntry#INPUT_OUTPUT_MODE INPUT-OUTPUT} will be returned in both cases.
*
* @param modes
* The parameter modes.
* @param args
* The passed arguments.
* @param input
* On {@code true}, collect input parameters; else, collect output parameters.
*
* @return The collected parameters from the <code>args</code> array.
*/
private static Object[] collectParameters(String modes, Object[] args, boolean input)
{
List<Object> params = new ArrayList<>();
for (int i = 0; i < modes.length(); i++)
{
char mode = modes.charAt(i);
if (mode == InternalEntry.INPUT_OUTPUT_MODE ||
(input && mode == InternalEntry.INPUT_MODE) ||
(!input && mode == InternalEntry.OUTPUT_MODE))
{
params.add(args[i]);
}
}
return params.toArray();
}
/**
* Resolve the parameters from the specified schema element and save them in the
* <code>params</code> list: on index 0 the name and on index 1 the schema type.
*
*
* @param element
* The schema element which needs to be resolved.
* @param name
* The name of this element.
* @param params
* The list where to save the parameters.
*/
private static void resolveParameters(XmlSchemaElement element,
QName name,
List<QName[]> params)
{
if (element == null)
{
return;
}
// TODO: what other features need to be treated?
XmlSchemaType elType = element.getSchemaType();
if (elType instanceof XmlSchemaSimpleType)
{
// found a simple type, use it
params.add(new QName[] { name, element.getSchemaTypeName() });
}
else if (elType instanceof XmlSchemaComplexType)
{
XmlSchemaComplexType type = (XmlSchemaComplexType) elType;
XmlSchemaParticle particle = type.getParticle();
if (particle instanceof XmlSchemaAny)
{
// TODO:
}
else if (particle instanceof XmlSchemaElement)
{
// TODO:
}
else if (particle instanceof XmlSchemaAll)
{
// TODO:
}
else if (particle instanceof XmlSchemaChoice)
{
// TODO:
}
else if (particle instanceof XmlSchemaSequence)
{
// TODO:
XmlSchemaSequence xmlSeq = (XmlSchemaSequence) particle;
int no = xmlSeq.getItems().size();
for (int i = 0; i < no; i++)
{
XmlSchemaSequenceMember obj = xmlSeq.getItems().get(i);
if (obj instanceof XmlSchemaElement)
{
XmlSchemaElement elObj = (XmlSchemaElement) obj;
resolveParameters(elObj, elObj.getQName(), params);
}
}
}
else if (particle instanceof XmlSchemaGroupRef)
{
// TODO:
}
}
}
/**
* Search the through the list of {@link ExtensibilityElement}s for one matching the give
* type.
*
* @param extensions
* The list of extensibility elements.
* @param type
* The type to be matched.
*
* @return The found element or {@code null} if not found.
*/
@SuppressWarnings("unchecked")
private static <T> T getExtension(List<?> extensions, Class<T> type)
{
if (extensions == null)
{
return null;
}
Iterator<ExtensibilityElement> extElIter =
(Iterator<ExtensibilityElement>) extensions.iterator();
while (extElIter.hasNext())
{
ExtensibilityElement extEl = extElIter.next();
if (type.isAssignableFrom(extEl.getClass()))
{
return (T) extEl;
}
}
return null;
}
/**
* Get the implicit port from the given collection of ports. The found {@link Port} must be
* associated with a {@link Binding} with a {@link SOAPBinding SOAP 1.1. extensibility}. If
* more than one port matches or no port found, return {@code null}.
*
* @param ports
* The {@link Port} instances to be searched.
*
* @return The found {@link Port} following the above rules.
*/
@SuppressWarnings("unchecked")
private static Port getImplicitSoapPort(Collection<?> ports)
{
Iterator<Port> portIter = (Iterator<Port>) ports.iterator();
Port soapPort = null;
while (portIter.hasNext())
{
Port port = portIter.next();
Binding binding = port.getBinding();
if (getExtension(binding.getExtensibilityElements(), SOAPBinding.class) != null)
{
if (soapPort != null)
{
// more than one SOAP port found, return "not found"
return null;
}
soapPort = port;
}
}
return soapPort;
}
/**
* Show a specific 4GL-style error meaning that no service could be resolved from the WSDL
* document.
*/
private static void noServicesError()
{
ErrorManager.recordOrShowError(11735, "No Services located in the WSDL document",
false, false, false);
}
/**
* Validate the operation's parameters against the input and output parameters sent by the
* caller.
* <p>
* The rules are:
* <ol>
* <li>all output parameters must be a {@link BaseDataType};
* <li>if only one input (or output) parameter is specified, is assumed to be wrapped;
* <li>if no input (or output) parameters are specified, then the operation must have no
* input (or output) parameters.
* </ol>
*
* @param op
* The service operation to be invoked.
* @param service
* TBA
* @param input
* The passed input parameters.
* @param output
* The passed output parameters.
* @param inParams
* The number of input parameters defined at the operation.
* @param outParams
* The number of output parameters defined at the operation.
*
* @return {@code true} if the parameters are valid.
*/
private static boolean validateParameters(Operation op,
Service service,
Object[] input,
Object[] output,
int inParams,
int outParams)
{
// all outputParams must be BDT
int i = 0;
for (Object param : output)
{
if (!(param instanceof BaseDataType))
{
String msg = "Error invoking operation %s on service %s: Parameter %d is of " +
"type %s, which is not BaseDataType compatible!";
msg = String.format(msg, op.getName(), service.getQName().getLocalPart(), i,
(param == null ? "null" : param.getClass().getName()));
throw new IllegalStateException(msg);
}
i++;
}
// validate input parameters
boolean paramError = false;
if (input.length > 0)
{
for (Object param : input)
{
if (!(param instanceof BaseDataType))
{
// this is uncommon, the 'inline' parameters should be wrapped
}
else if (((BaseDataType) param).isUnknown())
{
ErrorManager.recordOrThrowError(11790, "The 4GL value may not be UNKNOWN", false);
return false;
}
}
if (input.length == 1)
{
// in this case, we assume that the parameter is passed as XML message OR there is
// only one input parameter compatible with the one at the operation
// type validation will be done later, when the input message is computed
}
else if (input.length != inParams)
{
paramError = true;
}
}
else if (inParams > 0)
{
// no input parameters specified, fail as the operation has input parameters
paramError = true;
}
// validate output parameters
if (output.length > 0)
{
if (output.length == 1)
{
// in this case, we assume that the parameter is passed as XML message OR there is
// only one output parameter compatible with the one at the operation
// type validation will be done later, when the output message is computed
}
else if (output.length != outParams)
{
paramError = true;
}
}
else if (outParams > 0)
{
// no output parameters specified, fail as the operation has output parameters
paramError = true;
}
if (paramError)
{
// TODO: alternate errors found while testing with wrong parameters:
// Mismatched parameter types passed to procedure <procedure>. (3230)
// Mismatched number of parameters passed to routine <name>. (3234)
String msg = "Failure initializing SOAP Call: Cannot match signature for operation '%s'";
ErrorManager.recordOrThrowError(11762, String.format(msg, op.getName()));
}
return !paramError;
}
/**
* Using the input parameters configured at the operation's input message and the input
* parameters sent by the caller, create a new XML payload representing the input message
* passed to the operation call.
* <p>
* The caller may not pass null or unknown values. If the caller passes only one input
* parameter of type {@link Text} or {@link String}, then it is assumed we are in wrapped
* mode and the parameter holds the entire XML payload.
*
*
* @param wsd
* The definition of the WSDL that contains the operation.
* @param op
* The operation to be invoked.
* @param inMsgParams
* The (name, type) mappings of each parameter, defined at the operation's input.
* @param inputParams
* The input parameters sent by the caller.
*
* @return A new {@link OMElement} structure with the XML payload for the input message.
*/
@SuppressWarnings("unchecked")
private static OMElement buildInputMessage(WebServiceData wsd,
Operation op,
QName[][] inMsgParams,
Object[] inputParams)
{
Definition wsdlDef = wsd.wsdlDef;
// we need to build a new request message
String ns = wsdlDef.getTargetNamespace();
String prefix = wsdlDef.getPrefix(ns);
if (inputParams.length == 1)
{
// if there is only one input parameter, then that parameter must be a character/longchar
// with the input message encoded as XML.
// get the string representation of the parameter
Object oxml = inputParams[0];
String xml = null;
if (oxml instanceof Text)
{
xml = ((Text) oxml).toStringMessage();
}
else if (oxml instanceof String) // never ?
{
xml = (String) oxml;
}
else
{
xml = (oxml == null ? "?" : oxml.toString());
}
// naive attempt to verify if we are in wrapped mode
if (xml.startsWith("<?xml version=\"1.0\" encoding=\"utf-8\"?>"))
{
try
{
// TODO: the exception to this is when the only argument is of type char
// in this case, it is possible to pass it in unwrapped mode
// 4GL does not care of what the XML contains - it must be a valid XML
OMElement om = AXIOMUtil.stringToOM(xml);
if (om.getNamespace() == null)
{
om.setNamespace(om.declareNamespace(ns, prefix));
}
return om;
}
catch (XMLStreamException e)
{
final String msg = "Malformed XML fragment: %s (11781)";
ErrorManager.recordOrThrowError(
11781, String.format(msg, e.getMessage()), false, true);
return null;
}
}
}
// reaching this point means we are in no-wrapping mode (if a single text parameter was
// supplied, is it not in xml format)
BaseDataType[] params = new BaseDataType[inputParams.length];
for (int i = 0; i < inputParams.length; i++)
{
Object param = inputParams[i];
if (param == null ||
(param instanceof BaseDataType && ((BaseDataType) param).isUnknown()))
{
ErrorManager.recordOrThrowError(11790, "The 4GL value may not be UNKNOWN (11790)",
false, true);
return null;
}
if (param instanceof BaseDataType)
{
params[i] = (BaseDataType) param;
}
else
{
// TODO: build a BDT based on the param's java type
params[i] = new character(inputParams[i].toString());
}
}
String schemaTargetNs = null;
for (XmlSchema schema : wsd.getXmlSchemas())
{
if (schema.getElementByName(op.getName()) != null)
{
// the same operation needs to be defined in only one schema
schemaTargetNs = schema.getTargetNamespace();
break;
}
}
OMElement root = soapFactory.createOMElement(new QName(schemaTargetNs, op.getName(), prefix));
for (int i = 0; i < params.length; i++)
{
// the order of the passed parameters must match the order of the parts at the message
// definition. the check is done by converting the value to the expected type
// attempt to convert to the XSD type, and from there to string
String sval = marshallValue(inMsgParams[i][0], inMsgParams[i][1], params[i]);
QName elName = new QName(schemaTargetNs, inMsgParams[i][0].getLocalPart(), prefix);
OMElement child = soapFactory.createOMElement(elName);
child.addChild(soapFactory.createOMText(child, sval));
root.addChild(child);
}
return root;
}
/**
* Marshall a value from Java/Progress style to SOAP style. This will also validate the input
* value with the parameter's type defined at the schema.
* <p>
* See the "Table 3-2: Supported XML data types" from OpenEdge(R) Development: Web Services,
* page 3-15.
*
* @param paramName
* The parameter's name, as defined at the SOAP message.
* @param paramType
* The parameter's type, as defined at the schema.
* @param value
* The JAva or Progress-style value to be converted.
*
* @return The string representation of this value. The method does the best effort to
* convert to a string representation of XSD parameter type. The last resort being
* {@link BaseDataType#toStringMessage()} method.
*/
private static String marshallValue(QName paramName, QName paramType, BaseDataType value)
{
if (value instanceof Text)
{
return value.toStringMessage();
}
else if (value instanceof logical)
{
return Boolean.toString(((logical) value).booleanValue());
}
else if (value instanceof decimal)
{
return Double.toString(((decimal) value).doubleValue());
}
else if (value instanceof integer)
{
return Integer.toString(((integer) value).intValue());
}
else if (value instanceof int64)
{
return Long.toString(((int64) value).longValue());
}
else if (value instanceof datetime)
{
// format is ISO = 'CCYY-MM-DDThh:mm:ss[.zzzzzzzz][+/-hh:mm]
return ((datetime) value).getIsoDate();
}
else if (value instanceof date)
{
// format is ISO = 'CCYY-MM-DD' (might contain a timezone, too)
return ((date) value).getIsoDate();
}
else
{
return value.toStringMessage(); // in any other case
}
}
/**
* Unmarshall a value received as a response for an service operation request to a
* {@link BaseDataType}, depending on the parameter's type defined at the operation's output
* message. This will also validate the value with the parameter's type defined at the schema.
* <p>
* See the "Table 3-2: Supported XML data types" from OpenEdge(R) Development: Web Services,
* page 3-15.
*
* @param paramName
* The parameter's name, as defined at the SOAP message.
* @param paramType
* The parameter's type, as defined at the schema.
* @param param
* The response received after an operation invocation.
*
* @return A {@link BaseDataType} instance for the received parameter or {@code null} if
* conversion is not possible.
*/
private static BaseDataType unmarshallValue(QName paramName, QName paramType, OMElement param)
{
// TODO: check if paramName.getLocalPart() == param.getLocalName() or throw exception ?
// TODO: use some constants for types from: http://www.w3.org/2001/XMLSchema-datatypes
switch (paramType.getLocalPart())
{
case "string":
return new character(param.getText());
case "boolean":
return new logical(param.getText());
case "float":
case "double":
case "decimal":
// the web service will return the the value in 'standard' decimal format
return new decimal(Double.parseDouble(param.getText()));
case "byte":
case "int":
case "integer":
case "short":
return new integer(Integer.parseInt(param.getText()));
case "long":
return new int64(Long.parseLong(param.getText()));
case "date":
// format is ISO = 'CCYY-MM-DD'(might contain a timezone, too)
return new date(param.getText(), date.DEFAULT_DATETIME_FORMAT, date.DEFAULT_DATE_WINDOWING_YEAR);
case "datetime":
// format is ISO = 'CCYY-MM-DDThh:mm:ss[.zzzzzzzz]/[+/-hh:mm]
return datetimetz.fromLiteral(param.getText());
// TODO: add other types
}
return null;
}
/**
* Process the message received after an operation invocation and populate the output
* parameters sent by the caller accordingly.
* <p>
* If the caller passes only one input parameter of type {@link Text}, then it is assumed we
* are in wrapped mode and that parameter will hold the entire XML payload for the received
* message, serialized as string.
*
* @param outMsgParams
* The (name, type) mappings of each parameter, defined at the operation's output.
* @param outputParams
* The output parameters sent by the caller; each element is a {@link BaseDataType}.
* @param message
* The message received after the operation invocation.
*
* @return {@code true} if message processed or {@code false} if there were
* problems during conversion of the received to their {@link BaseDataType} expected
* type.
*/
@SuppressWarnings("unchecked")
private static boolean processOutputMessage(QName[][] outMsgParams,
Object[] outputParams,
OMElement message)
{
// if (outputParams.length == 1)
// {
// // check if we are emitting the entire XML
// BaseDataType param = (BaseDataType) outputParams[0];
// if (param instanceof Text)
// {
// // TODO: what if there is only one message output parameter, of type char? should we
// // unwrap it?
//
// Text txt = (Text) param;
// txt.assign(message.toString());
//
// return true;
// }
// }
// in all other cases, match the output parameters with the received message
int i = 0;
Iterator<?> itrElem = message.getChildren();
while (itrElem.hasNext())
{
Object child = itrElem.next();
if (!(child instanceof OMElement))
{
continue;
}
OMElement omChild = (OMElement) child;
BaseDataType param = (BaseDataType) outputParams[i];
BaseDataType value = unmarshallValue(outMsgParams[i][0], outMsgParams[i][1], omChild);
if (value == null)
{
String xmlType = outMsgParams[i][1].getLocalPart();
String legacyType = param.getTypeName();
final String msg = "Unable to convert XML data type %s to 4GL type %s (11793)";
ErrorManager.recordOrThrowError(11793, String.format(msg, xmlType, legacyType));
return false;
}
param.assign(value);
i++;
}
return true;
}
/**
* Get the WSDL port name we'd like to access. Check if {@code portTypeName} is in the list
* of WSDL ports. If {@code true} the {@code portTypeName} is equals to port name and could be
* used for bindings. If {@code false} return {@code null} and let service to choose the
* appropriate SOAP port.
*
* @param portTypeName
* port type name.
* @param wsdlDef
* The definition of the WSDL that contains the port type.
*
* @return name of the WSDL port we'd like to access.
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
private static String getPortName(String portTypeName, Definition wsdlDef)
{
// WSDL bindings map
Map bindings = wsdlDef.getBindings();
if ((bindings != null) && (portTypeName != null))
{
// list of ports names
Set<QName> ports = bindings.keySet();
for (QName port : ports)
{
if (portTypeName.equals(port.getLocalPart()))
{
return portTypeName;
}
}
}
return null;
}
/**
* Resolve the parameters for the given message.
*
* @param msg
* The message.
* @param wsd
* A {@code WebServiceData} structure that contains information about schemas and
* parameters.
*
* @return An array where, for each element, on index 0 is the parameter's name and on index
* 1 is the parameter's type.
*/
@SuppressWarnings("unchecked")
private static QName[][] getMessageParameters(Message msg, WebServiceData wsd)
{
buildSchema(wsd);
// TODO: complex types can define parameters as i.e. "sequence", "all" or "choice". this
// means is not enough to determine the entire list of possible parameters, as for "all" and
// "choice" matching might be done via the parameter's type (for input) or the passed name
// and type (for output). thus, this method might need to return some complex structure
// instead of (name, type) mappings.
QName msgName = msg.getQName();
if (wsd.msgParams.containsKey(msgName))
{
return wsd.msgParams.get(msgName);
}
// resolve the parameters
List<QName[]> params = new ArrayList<>();
for (Part part : (Iterable<Part>) msg.getParts().values())
{
QName name = part.getElementName();
/// walk the schemas and determine the element
for (XmlSchema schema : wsd.xmlSchemas)
{
XmlSchemaElement element = schema.getElementByName(name);
resolveParameters(element, name, params);
}
}
QName[][] qparams = params.toArray(new QName[][] {});
wsd.msgParams.put(msgName, qparams);
return qparams;
}
/**
* If the {@link WebServiceData#xmlSchemas} list is not yet build, extract all the schema
* extensibility elements from the {@link WebServiceData#wsdlDef WSDL definition} and save
* them in the {@link WebServiceData#xmlSchemas} list.
*
* @param serviceData
* The structure that contains information about the current connection and where the
* schema will be stored.
*/
@SuppressWarnings("unchecked")
private static void buildSchema(WebServiceData serviceData)
{
if (serviceData.getXmlSchemas() != null)
{
return;
}
List<XmlSchema> xmlSchemas = new ArrayList<>();
serviceData.setXmlSchemas(xmlSchemas);
Types types = serviceData.wsdlDef.getTypes();
for (ExtensibilityElement el :
(Iterable<ExtensibilityElement>) types.getExtensibilityElements())
{
if (el instanceof Schema)
{
Schema schema = (Schema) el;
String baseUri = schema.getDocumentBaseURI();
XmlSchemaCollection xsc = new XmlSchemaCollection();
if (baseUri != null)
{
xsc.setBaseUri(baseUri);
}
XmlSchema xmlSchema = xsc.read(schema.getElement());
xmlSchemas.add(xmlSchema);
}
}
}
/**
* Build a string containing the SOAP Envelope namespaces in reverse order.
*
* @param sc
* ServiceClient instance.
*
* @return SOAP Envelope namespaces serialized as as string.
*
* @throws AxisFault
* In case of errors.
*/
@SuppressWarnings("unchecked")
private static String getEnvelopeNameSpaces(ServiceClient sc)
throws AxisFault
{
StringBuilder sb = new StringBuilder();
MessageContext messageContext = sc.getLastOperationContext()
.getMessageContext(WSDLConstants.MESSAGE_LABEL_IN_VALUE);
SOAPEnvelope soapEnv = messageContext.getEnvelope();
// collect envelope namespaces.
List<OMNamespace> names = new LinkedList<>();
Iterator<OMNamespace> iter = soapEnv.getAllDeclaredNamespaces();
while (iter.hasNext())
{
names.add(iter.next());
}
// iterate in reverse order
ListIterator<OMNamespace> listIter = names.listIterator(names.size());
while (listIter.hasPrevious())
{
OMNamespace ns = listIter.previous();
sb.append(" xmlns:").append(ns.getPrefix()).
append("=\"").append(ns.getNamespaceURI()).append("\"");
}
return sb.toString();
}
/**
* Merge response namespace with SOAP Envelope namespaces.
*
* @param res
* Response string in XML format.
* @param envns
* A string containing serialized SOAP Envelope namespaces
*
* @return Response merged with SOAP Envelope namespaces
*/
private static Text mergeNameSpaces(Text res, String envns)
{
if (envns != null)
{
StringBuilder sb = new StringBuilder();
String str = res.getValue().trim();
if (str.charAt(0) == '<')
{
int pos = str.indexOf(">");
if (pos > 0)
{
sb.append(str.substring(0, pos));
sb.append(envns);
sb.append(str.substring(pos));
res.assign(sb.toString());
}
}
}
return res;
}
/**
* Container for the context-local data. This will also ensure sync access to the
* {@link #webServices} registry.
*/
private static class WorkArea
{
/** A registry containing the connected Web Services. */
private final Map<Integer, WebServiceData> webServices = new HashMap<>();
/**
* Add a new WebService to the {@link #webServices registry}.
*
* @param id
* The ID of the associated WebService on P2J Client side.
* @param sd
* The data for this Web Service.
*/
public synchronized void addWebService(int id, WebServiceData sd)
{
webServices.put(id, sd);
}
/**
* Locate the {@link WebServiceData} for the the given ID.
*
* @param id
* The ID of the associated WebService on P2J Client side.
*/
private synchronized WebServiceData locate(int id)
{
return webServices.get(id);
}
/**
* Remove the WebService with the given ID from the {@link #webServices registry}.
*
* @param id
* The ID of the associated WebService on P2J Client side.
*/
private synchronized void removeWebService(int id)
{
webServices.remove(id);
}
}
/**
* Data structure that is used to store data on client side between calls from P2J server side.
* A {@code WebServiceData} uniquely identifies an <i>open</i> connection to a web service. It
* can be in one of two states:
* <ul>
* <li>idle - when no invocation is in progress. It only contains <i>final</i> data
* relative to connection itself;
* <li>busy - when waiting messages from server-side. In this case the structure also
* contains a second temporary state that is carried between those messages. This state
* is released when WebService operation is finished and the connection goes back to
* idle state and awaits for other operations.
* </ul>
*
* The busy state is very similar to a state in a mini-session. However, at this moment the
* implementation only allows single session/ invocation per connection at a given moment.
* Serialized calls to same or different operations using the same connection are possible, of
* course.
*/
private static class WebServiceData
{
/** The id of the open connection. */
private final int id;
/** The WSDL document definition. */
private final Definition wsdlDef;
/** The WSDL port type. */
private final PortType portType;
/** The WSDL service. */
private final Service service;
/** A collection of all web service options used to create this connection. */
private final WebServiceConnectOptions wsOptions;
/** The {@link ServiceClient} for current invocation. {@code null} in idle state. */
private ServiceClient sc = null;
/** The {@link Operation} for current invocation. {@code null} in idle state. */
private Operation targetOp = null;
/** The built request message for current invocation. {@code null} in idle state. */
private OMElement requestMessage = null;
/** The response from WebService in current invocation. {@code null} in idle state. */
private OMElement responseMessage = null;
/** The output parameters for current invocation. {@code null} in idle state. */
private Object[] outputParams = null;
/** The description for output parameters. {@code null} in idle state. */
private QName[][] outMsgParams = null;
/** List of XML Schema's loaded from the WSDL document. */
private List<XmlSchema> xmlSchemas = null;
/** Cache of the resolved parameters, for each message. */
private final Map<QName, QName[][]> msgParams = new HashMap<>();
/**
* The constructors initialize the final fields that compose the immutable part of the
* connection. They are unchanged as long the connection is alive.
*
* @param id
* The id of the open connection.
* @param wsdlDef
* The WSDL document definition.
* @param portType
* The WSDL port type.
* @param service
* The WSDL service.
* @param wsOptions
* A collection of all web service options used for connection.
*/
public WebServiceData(int id,
Definition wsdlDef,
PortType portType,
Service service,
WebServiceConnectOptions wsOptions)
{
this.id = id;
this.wsdlDef = wsdlDef;
this.portType = portType;
this.service = service;
this.wsOptions = wsOptions;
}
/**
* Get the id of this open connection.
*
* @return the id of this connection.
*/
public int getId()
{
return id;
}
/**
* Get the optional the SOAP endpoint.
*
* @return the SOAP endpoint.
*/
public String getSoapEndpoint()
{
return wsOptions.getSoapEndpoint();
}
/**
* Get the optional the SOAP endpoint user ID.
*
* @return the SOAP endpoint user id.
*/
public String getSoapEndpointUserid()
{
return wsOptions.getSoapEndpointUserid();
}
/**
* Get the optional the SOAP endpoint password.
*
* @return the SOAP endpoint password.
*/
public String getSoapEndpointPassword()
{
return wsOptions.getSoapEndpointPassword();
}
/**
* Get the {@link ServiceClient} for current invocation. {@code null} in idle state.
*
* @return the {@link ServiceClient} for current invocation.
*/
public ServiceClient getServiceClient()
{
return sc;
}
/**
* Instruct the connection whether to reuse or create a new SSL session when reconnecting
* to the same Web server using HTTPS.
*
* @return {@code true} if every SSL connections should be reused.
*/
public boolean isSessionReuse()
{
return !wsOptions.isNoSessionReuse();
}
/**
* Specifies whether host verification for connection using HTTPS should be disabled.
*
* @return {@code true} if host verification for SSL connections will be skipped.
*
*/
public boolean isNoHostVerify()
{
return wsOptions.isNoHostVerify();
}
/**
* Get the maximum number of simultaneous (parallel) connections maintained between the
* client and Web service for asynchronous requests.
*
* @return the maximum number of simultaneous (parallel) connections maintained between
* the client and Web service for asynchronous requests.
*/
public int getMaxConnections()
{
return wsOptions.getMaxConnections();
}
/**
* Get the maximum number of seconds that a given connection can be reused for asynchronous
* requests before it is destroyed.
*
* @return the maximum number of seconds that a given connection can be reused for
* asynchronous requests before it is destroyed.
*/
public int getConnectionLifetime()
{
return wsOptions.getConnectionLifetime();
}
/**
* Set the {@link ServiceClient} for current invocation.
*
* @param sc
* The new {@link ServiceClient} for current invocation.
*/
public void setServiceClient(ServiceClient sc)
{
if (this.sc != null)
{
this.sc = null;
// throw new InvalidStateException(
// "must finish current invocation before preparing a second one")
}
this.sc = sc;
}
/**
* Get the built request message for current invocation.
*
* @return the built request message for current invocation. In idle state it is
* {@code null}.
*/
public OMElement getRequestMessage()
{
return requestMessage;
}
/**
* Set the built request message for current invocation.
*
* @param requestMessage
* the new built request message for current invocation.
*/
public void setRequestMessage(OMElement requestMessage)
{
this.requestMessage = requestMessage;
}
/**
* Get the response from WebService in current invocation.
*
* @return the response from WebService in current invocation. It is {@code null} in
* idle state.
*/
public OMElement getResponseMessage()
{
return responseMessage;
}
/**
* Set the response from WebService in current invocation.
*
* @param responseMessage
* the new response from WebService in current invocation.
*/
public void setResponseMessage(OMElement responseMessage)
{
this.responseMessage = responseMessage;
}
/**
* Get the {@link Operation} for current invocation.
*
* @return the {@link Operation} for current invocation. {@code null} in idle state.
*/
public Operation getTargetOperation()
{
return targetOp;
}
/**
* Set the {@link Operation} for current invocation.
*
* @param targetOp
* the new {@link Operation} for current invocation.
*/
public void setTargetOperation(Operation targetOp)
{
this.targetOp = targetOp;
}
/**
* Get the list of XML Schema's loaded from the WSDL document.
*
* @return the list of XML Schema's loaded from the WSDL document.
*/
public List<XmlSchema> getXmlSchemas()
{
return xmlSchemas;
}
/**
* Get the list of XML Schema's loaded from the WSDL document.
*
* @param xmlSchemas
* the new list of XML Schema's loaded from the WSDL document.
*/
public void setXmlSchemas(List<XmlSchema> xmlSchemas)
{
this.xmlSchemas = xmlSchemas;
}
/**
* Get the output parameters for current invocation.
*
* @return the output parameters for current invocation. {@code null} in idle state.
*/
public Object[] getOutputParameters()
{
return outputParams;
}
/**
* Get the description for output parameters.
*
* @return the description for output parameters. {@code null} in idle state.
*/
public QName[][] getOutputMsgParameters()
{
return outMsgParams;
}
/**
* Set details about output parameters that will be used in a future call.
*
* @param outputParams
* the output parameters for current invocation.
* @param outMsgParams
* the description for output parameters.
*/
public void setOutputParameters(Object[] outputParams, QName[][] outMsgParams)
{
this.outputParams = outputParams;
this.outMsgParams = outMsgParams;
}
/**
* Cleans the temporary state used during invocation sequence. Must be done after each
* successful call or if the invocation fails.
*/
public void cleanupInvocation()
{
setRequestMessage(null);
setResponseMessage(null);
setOutputParameters(null, null);
setTargetOperation(null);
setServiceClient(null);
setXmlSchemas(null);
msgParams.clear();
}
}
}