/*
* JBoss, Home of Professional Open Source
* Copyright 2006, JBoss Inc., and individual contributors as indicated
* by the @authors tag. See the copyright.txt in the distribution for a
* full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.jboss.soa.esb.actions.soap;
import java.io.File;
import java.io.IOException;
import java.io.StringReader;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Properties;
import javax.xml.namespace.QName;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.apache.commons.httpclient.Header;
import org.apache.commons.httpclient.HostConfiguration;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.methods.StringRequestEntity;
import org.apache.log4j.Logger;
import org.jboss.internal.soa.esb.publish.Publish;
import org.jboss.internal.soa.esb.soap.OGNLUtils;
import org.jboss.internal.soa.esb.util.StreamUtils;
import org.jboss.soa.esb.ConfigurationException;
import org.jboss.soa.esb.actions.AbstractActionPipelineProcessor;
import org.jboss.soa.esb.actions.ActionLifecycleException;
import org.jboss.soa.esb.actions.ActionProcessingException;
import org.jboss.soa.esb.actions.ActionUtils;
import org.jboss.soa.esb.helpers.ConfigTree;
import org.jboss.soa.esb.http.HttpClientFactory;
import org.jboss.soa.esb.http.configurators.Connection;
import org.jboss.soa.esb.listeners.ListenerTagNames;
import org.jboss.soa.esb.listeners.message.MessageDeliverException;
import org.jboss.soa.esb.message.Message;
import org.jboss.soa.esb.message.MessagePayloadProxy;
import org.jboss.soa.esb.message.ResponseHeader;
import org.jboss.soa.esb.message.ResponseStatus;
import org.jboss.soa.esb.message.body.content.BytesBody;
import org.jboss.soa.esb.util.ClassUtil;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.annotations.Annotations;
import com.thoughtworks.xstream.io.HierarchicalStreamReader;
import com.thoughtworks.xstream.io.xml.QNameMap;
import com.thoughtworks.xstream.io.xml.StaxDriver;
/**
* SOAP Client action processor.
* <p/>
* Uses the soapUI Client Service to construct and populate a message
* for the target service. This action then routes that message to that service.
*
* <h2>Endpoint Operation Specification</h2>
* Specifying the endpoint operation is a straightforward task. Simply specify the "wsdl" and
* "SOAPAction" properties on the SOAPClient action as follows:
* <pre>
* <action name="soapui-client-action" class="org.jboss.soa.esb.actions.soap.SOAPClient">
* <property name="wsdl" value="http://localhost:18080/acme/services/OrderManagement?wsdl"/>
* <property name="SOAPAction" value="http://www.acme.com/OrderManagement/SendSalesOrderNotification"/>
* </action></pre>
* The SOAP operation is derived from the SOAPAction.
*
* <h2 id="request-construction">SOAP Request Message Construction</h2>
* The SOAP operation parameters are supplied in one of 2 ways:
* <ol>
* <li>As a {@link Map} instance set on the <i>default body location</i> (Message.getBody().add(Map))</li>
* <li>As a {@link Map} instance set on in a <i>named body location</i> (Message.getBody().add(String, Map)),
* where the name of that body location is specified as the value of the "paramsLocation" action property.
* </li>
* </ol>
* The parameter {@link Map} itself can also be populated in one of 2 ways:
* <ol>
* <li><b>Option 1</b>: With a set of Objects that are accessed (for SOAP message parameters) using the
* <a href="http://www.ognl.org/">OGNL</a></li> framework. More on the use of OGNL below.
* <li><b>Option 2</b>: With a set of String based key-value pairs(<String, Object>), where the
* key is an OGNL expression identifying the SOAP parameter to be populated with
* the key's value. More on the use of OGNL below.
* </li>
* </ol>
* As stated above, <a href="http://www.ognl.org/">OGNL</a> is the mechanism we use for selecting
* the SOAP parameter values to be injected into the SOAP message from the supplied parameter
* {@link Map}. The OGNL expression for a specific parameter within the SOAP message depends on that
* the position of that parameter within the SOAP body. In the following message:
* <pre>
* <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
* xmlns:cus="http://schemas.acme.com">
* <soapenv:Header/>
* <soapenv:Body>
* <cus:<b color="red">customerOrder</b>>
* <cus:<b color="red">header</b>>
* <cus:<b color="red">customerNumber</b>>123456</cus:customerNumber>
* </cus:header>
* </cus:customerOrder>
* </soapenv:Body>
* </soapenv:Envelope>
* </pre>
*
* The OGNL expression representing the customerNumber parameter is "<b color="red">customerOrder.header.customerNumber</b>".
* <p/>
* Once the OGNL expression has been calculated for a parameter, this class will check the supplied parameter
* map for an Object keyed off the full OGNL expression (Option 1 above). If no such parameter Object is present on the map,
* this class will then attempt to load the parameter by supplying the map and OGNL expression instances to the
* OGNL toolkit (Option 2 above). If this doesn't yield a value, this parameter location within the SOAP message will
* remain blank.
* <p/>
* Taking the sample message above and using the "Option 1" approach to populating the "customerNumber" requires an
* object instance (e.g. an "Order" object instance) to be set on the parameters map under the key "customerOrder". The "customerOrder"
* object instance needs to contain a "header" property (e.g. a "Header" object instance). The object instance
* behind the "header" property (e.g. a "Header" object instance) should have a "customerNumber" property.
* <p/>
* OGNL expressions associated with Collections are constructed in a slightly different way. This is easiest explained
* through an example:
* <pre>
* <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
* xmlns:cus="http://schemas.active-endpoints.com/sample/customerorder/2006/04/CustomerOrder.xsd"
* xmlns:stan="http://schemas.active-endpoints.com/sample/standardtypes/2006/04/StandardTypes.xsd">
* <soapenv:Header/>
* <soapenv:Body>
* <cus:<b color="red">customerOrder</b>>
* <cus:<b color="red">items</b>>
* <cus:item>
* <cus:<b color="red">partNumber</b>>FLT16100</cus:partNumber>
* <cus:description>Flat 16 feet 100 count</cus:description>
* <cus:quantity>50</cus:quantity>
* <cus:price>490.00</cus:price>
* <cus:extensionAmount>24500.00</cus:extensionAmount>
* </cus:item>
* <cus:item>
* <cus:<b color="red">partNumber</b>>RND08065</cus:partNumber>
* <cus:description>Round 8 feet 65 count</cus:description>
* <cus:quantity>9</cus:quantity>
* <cus:price>178.00</cus:price>
* <cus:extensionAmount>7852.00</cus:extensionAmount>
* </cus:item>
* </cus:items>
* </cus:customerOrder>
* </soapenv:Body>
* </soapenv:Envelope>
* </pre>
* The above order message contains a collection of order "items". Each entry in the collection is represented
* by an "item" element. The OGNL expressions for the order item "partNumber" is constructed as
* "<b color="red">customerOrder.items[0].partnumber</b>" and "<b color="red">customerOrder.items[1].partnumber</b>".
* As you can see from this, the collection entry element (the "item" element) makes no explicit appearance in the OGNL
* expression. It is represented implicitly by the indexing notation. In terms of an Object Graph (Option 1 above),
* this could be represented by an Order object instance (keyed on the map as "customerOrder") containing an "items"
* list ({@link java.util.List} or array), with the list entries being "OrderItem" instances, which in turn contains
* "partNumber" etc properties.
* <p/>
* Option 2 (above) provides a quick-and-dirty way to populate a SOAP message without having to create an Object
* model ala Option 1. The OGNL expressions that correspond with the SOAP operation parameters are exactly the same
* as for Option 1, except that there's not Object Graph Navigation involved. The OGNL expression is simply used as
* the key into the {@link Map}, with the corresponding key-value being the parameter.
*
* <h2>SOAP Response Message Consumption</h2>
* The SOAP response object instance can be is attached to the ESB {@link Message} instance in one of the
* following ways:
* <ol>
* <li>On the <i>default body location</i> (Message.getBody().add(Map))</li>
* <li>On in a <i>named body location</i> (Message.getBody().add(String, Map)),
* where the name of that body location is specified as the value of the "responseLocation" action property.
* </li>
* </ol>
* The response object instance can also be populated (from the SOAP response) in one of 3 ways:
* <ol>
* <li><b>Option 1</b>: As an Object Graph created and populated by the
* <a href="http://xstream.codehaus.org">XStream</a> toolkit.
* <li><b>Option 2</b>: As a set of String based key-value pairs(<String, String>), where the
* key is an OGNL expression identifying the SOAP response element and the value is a String
* representing the value from the SOAP message. This option should only be used for simple
* responses. If your response contains collections etc, this option is not viable. You need
* to use option 1.
* </li>
* <li><b>Option 3</b>: If Options 1 or 2 are not specified in the action configuration, the raw SOAP
* response message (String) is attached to the message.
* </li>
* </ol>
* Using <a href="http://xstream.codehaus.org">XStream</a> as a mechanism for populating an Object Graph
* (Option 1 above) is straightforward and works well, as long as the XML and Java object models are in line with
* each other.
* <p/>
* The XStream approach (Option 1) is configured on the action as follows:
* <pre>
* <action name="soapui-client-action" class="org.jboss.soa.esb.actions.soap.SOAPClient">
* <property name="wsdl" value="http://localhost:18080/acme/services/OrderManagement?wsdl"/>
* <property name="SOAPAction" value="http://www.acme.com/OrderManagement/GetOrder"/>
* <property name="paramsLocation" value="get-order-params" />
* <property name="responseLocation" value="get-order-response" />
* <property name="responseXStreamConfig">
* <alias name="customerOrder" class="com.acme.order.Order" namespace="http://schemas.acme.com/services/CustomerOrder.xsd" />
* <alias name="orderheader" class="com.acme.order.Header" namespace="http://schemas.acme.com/services/CustomerOrder.xsd" />
* <alias name="item" class="com.acme.order.OrderItem" namespace="http://schemas.acme.com/services/CustomerOrder.xsd" />
* </property>
* </action>
* </pre>
* In the above example, we also include an example of how to specify non-default named locations for the request
* parameters {@link Map} and response object instance.
* <p/>
* We also provide, in addition to the above <a href="http://xstream.codehaus.org">XStream</a> configuration options,
* the ability to specify field name mappings and <a href="http://xstream.codehaus.org">XStream</a> annotated classes.
* <pre>
* <property name="responseXStreamConfig">
* <fieldAlias name="header" class="com.acme.order.Order" fieldName="headerFieldName" />
* <annotation class="com.acme.order.Order" />
* </property>
* </pre>
* Field mappings can be used to map XML elements onto Java fields on those occasions when the local name of the element
* does not correspond to the field name in the Java class.
* <p/>
* To have the SOAP response data extracted into an OGNL keyed map (Option 2 above) and attached to the ESB
* {@link Message}, simply replace the "responseXStreamConfig" property with the "responseAsOgnlMap" property
* having a value of "true" as follows:
* <pre>
* <action name="soapui-client-action" class="org.jboss.soa.esb.actions.soap.SOAPClient">
* <property name="wsdl" value="http://localhost:18080/acme/services/OrderManagement?wsdl"/>
* <property name="SOAPAction" value="http://www.acme.com/OrderManagement/GetOrder"/>
* <property name="paramsLocation" value="get-order-params" />
* <property name="responseLocation" value="get-order-response" />
* <property name="responseAsOgnlMap" value="true" />
* </action>
* </pre>
* To return the raw SOAP message as a String (Option 3), simply omit both the "responseXStreamConfig"
* and "responseAsOgnlMap" properties.
*
* <h2>Transforming the SOAP Request Message</h2>
* It's often necessary to be able to transform the SOAP request before sending it. This may be to simply
* add some SOAP headers.
* <p/>
* Transformation of the SOAP request (before sending) is supported by configuring the SOAPClient action
* with a Smooks transformation configuration property as follows:
* <pre>
* <property name="smooksTransform" value="/transforms/order-transform.xml" /></pre>
*
* The value of the "smooksTransform" property is resolved by first checking it as a filesystem based resource.
* Failing that, it's checked as a classpath resource and failing that, as a URI based resource.
*
* <h3>Specifying a different SOAP schema</h3>
* <pre>
* <property name="SOAPNS" value="http://www.w3.org/2009/09/soap-envelope"/>
* </pre>
* This is an optional property and can be used to specify the SOAP schema that should be
* used by OGNL.
*
* @author <a href="mailto:tom.fennelly@jboss.com">tom.fennelly@jboss.com</a>
*/
@Publish(WsdlContractPublisher.class)
public class SOAPClient extends AbstractActionPipelineProcessor {
private Logger logger = Logger.getLogger(SOAPClient.class);
private String wsdl;
private String soapAction;
private String soapServiceName;
private String soapNs;
private String paramsLocation;
private String responseLocation;
private boolean responseAsOgnlMap;
private String smooksTransform;
private SoapUIInvoker soapUIInvoker;
private static DocumentBuilderFactory docBuilderFactory = createDocumentBuilderFactory();
private XStream responseXStreamDeserialzer;
private QNameMap responseXStreamQNameMap = new QNameMap();
private Properties httpClientProps = new Properties();
private HttpClient httpclient;
private String endpoint;
private String endpointUrl;
private String contentType;
private boolean endpointInitialized = false;
private MessagePayloadProxy payloadProxy;
private boolean httpResponseStatusEnabled;
public SOAPClient(ConfigTree config) throws ConfigurationException {
wsdl = config.getRequiredAttribute("wsdl");
soapAction = config.getRequiredAttribute("SOAPAction");
soapServiceName = config.getAttribute("SOAPService");
createPayloadProxy(config);
responseAsOgnlMap = "true".equalsIgnoreCase(config.getAttribute("responseAsOgnlMap"));
smooksTransform = config.getAttribute("smooksTransform");
if(smooksTransform != null) {
try {
smooksTransform = StreamUtils.getResourceAsString(smooksTransform, "UTF-8");
} catch (UnsupportedEncodingException e) {
logger.error("Unable to read Smooks resource '" + smooksTransform + "'. Must be UTF-8 encoded.");
}
}
ConfigTree[] xstreamAliases = config.getChildren("alias");
ConfigTree[] xstreamFieldAliases = config.getChildren("fieldAlias");
ConfigTree[] xstreamAnnotations = config.getChildren("annotation");
if ((xstreamAliases.length != 0) || (xstreamFieldAliases.length != 0) ||
(xstreamAnnotations.length != 0))
{
configureXStreamDeserializer(xstreamAliases, xstreamFieldAliases, xstreamAnnotations);
}
soapNs = config.getAttribute("SOAPNS");
// Extract the HttpClient creation properties from the ConfigTree. These are passed
// to the HttpClientFacatory...
extractHttpClientProps(config);
httpclient = HttpClientFactory.createHttpClient(httpClientProps);
endpointUrl = config.getAttribute("endpointUrl");
httpResponseStatusEnabled = ResponseStatus.isHttpEnabled(config);
}
private void createPayloadProxy(ConfigTree config) {
paramsLocation = config.getAttribute("paramsLocation");
responseLocation = config.getAttribute("responseLocation");
String[] legacyGetLocs;
String[] legacySetLocs;
if(paramsLocation != null) {
legacyGetLocs = new String[] {paramsLocation, BytesBody.BYTES_LOCATION, ActionUtils.POST_ACTION_DATA};
} else {
legacyGetLocs = new String[] {BytesBody.BYTES_LOCATION, ActionUtils.POST_ACTION_DATA};
}
if(responseLocation != null) {
legacySetLocs = new String[] {responseLocation, ActionUtils.POST_ACTION_DATA};
} else {
legacySetLocs = new String[] {ActionUtils.POST_ACTION_DATA};
}
payloadProxy = new MessagePayloadProxy(config, legacyGetLocs, legacySetLocs);
}
public void initialise() throws ActionLifecycleException {
super.initialise();
// Create the SoapUIInvoker instance for this SOAPClient...
soapUIInvoker = new MBeanSoapUIInvoker();
}
public SoapUIInvoker getSoapUIInvoker() {
return soapUIInvoker;
}
public void setSoapUIInvoker(SoapUIInvoker soapUIInvoker) {
this.soapUIInvoker = soapUIInvoker;
}
@Override
public void destroy() throws ActionLifecycleException {
if (httpclient != null) {
HttpClientFactory.shutdown(httpclient);
}
super.destroy();
}
private void extractHttpClientProps(ConfigTree config) {
ConfigTree[] httpClientConfigTrees = config.getChildren(HttpClientFactory.HTTP_CLIENT_PROPERTY);
httpClientProps.setProperty(HttpClientFactory.TARGET_HOST_URL, wsdl);
final ConfigTree parent = config.getParent();
if (parent != null) {
final String maxThreads = parent.getAttribute(ListenerTagNames.MAX_THREADS_TAG);
if (maxThreads != null) {
httpClientProps.setProperty(Connection.MAX_TOTAL_CONNECTIONS, maxThreads);
httpClientProps.setProperty(Connection.MAX_CONNECTIONS_PER_HOST, maxThreads);
}
}
// The HttpClient properties are attached under the factory class/impl property as <http-client-property name="x" value="y" /> nodes
for(ConfigTree httpClientProp : httpClientConfigTrees) {
String propName = httpClientProp.getAttribute("name");
String propValue = httpClientProp.getAttribute("value");
if(propName != null && propValue != null) {
httpClientProps.setProperty(propName, propValue);
}
}
}
public Message process(final Message message) throws ActionProcessingException {
Object payload;
try {
payload = payloadProxy.getPayload(message);
} catch (MessageDeliverException e) {
throw new ActionProcessingException("Error getting SOAP message parameters from payload.", e);
}
catch (final ClassCastException ex)
{
throw new ActionProcessingException("Required a Map in the payload, but got something else!");
}
if (!(payload instanceof Map)) {
throw new ActionProcessingException("Invalid payload type in message body location '" + payloadProxy.getGetPayloadLocation() + "'. Expected 'java.util.Map', was '" + payload.getClass().getName() + "'.");
}
Map params = (Map) payload;
if (params.isEmpty()) {
logger.warn("Params Map found in message, but the map is empty.");
}
String request;
try {
request = soapUIInvoker.buildRequest(wsdl, getEndpointOperation(), soapServiceName, params, httpClientProps, smooksTransform, soapNs);
} catch (IOException e) {
throw new ActionProcessingException("soapUI Client Service invocation failed.", e);
} catch (SAXException e) {
throw new ActionProcessingException("soapUI Client Service invocation failed.", e);
}
Response response = invokeEndpoint(request);
if(responseAsOgnlMap) {
try {
String mergedResponse = soapUIInvoker.mergeResponseTemplate(wsdl, getEndpointOperation(), soapServiceName, response.getBody(), httpClientProps, null, soapNs);
response.setBody(mergedResponse);
} catch (IOException e) {
throw new ActionProcessingException("soapUI Client Service invocation failed.", e);
} catch (SAXException e) {
throw new ActionProcessingException("soapUI Client Service invocation failed.", e);
}
}
// And process the response into the message...
processResponse(message, response);
return message;
}
public String getSoapNS()
{
return soapNs;
}
protected String getEndpointOperation() {
URI soapActionURI;
try {
soapActionURI = new URI(soapAction);
return new File(soapActionURI.getPath()).getName();
} catch (URISyntaxException e) {
return soapAction;
}
}
protected static class Response {
private Header[] headers;
private String body;
private int statusCode;
private String statusMessage;
protected Response(String body) {
this(body, new Header[0], 0, null);
}
protected Response(String body, Header[] headers, int statusCode, String statusMessage) {
this.headers = headers;
this.body = body;
this.statusCode = statusCode;
this.statusMessage = statusMessage;
}
protected Header[] getHeaders() {
return headers;
}
protected String getBody() {
return body;
}
protected void setBody(String body) {
this.body = body;
}
protected int getStatusCode() {
return statusCode;
}
protected String getStatusMessage() {
return statusMessage;
}
}
private Response invokeEndpoint(String request) throws ActionProcessingException {
if (!endpointInitialized) {
try {
if(endpointUrl != null) {
endpoint = endpointUrl;
} else {
endpoint = soapUIInvoker.getEndpoint(wsdl, soapServiceName, httpClientProps);
}
contentType = soapUIInvoker.getContentType(wsdl, soapServiceName, httpClientProps) + ";charset=UTF-8";
} catch (IOException e) {
throw new ActionProcessingException("soapUI Client Service invocation failed.", e);
}
endpointInitialized = true;
}
PostMethod post = new PostMethod(endpoint);
post.setRequestHeader("Content-Type", contentType);
post.setRequestHeader("Connection", "keep-alive");
post.setRequestHeader("SOAPAction", "\"" + soapAction + "\""); /// Customization to add quotes to Soap action
try {
post.setRequestEntity(new StringRequestEntity(request, "text/xml", "UTF-8"));
int result = httpclient.executeMethod(post);
if(result != HttpStatus.SC_OK) {
// TODO: We need to do more here!!
logger.warn("Received status code '" + result + "' on HTTP SOAP (POST) request to '" + endpoint + "'.");
}
return new Response(post.getResponseBodyAsString(), post.getResponseHeaders(), result, post.getStatusLine().getReasonPhrase());
} catch (IOException e) {
throw new ActionProcessingException("Failed to invoke SOAP Endpoint: '" + endpoint + " ' - '" + soapAction + "'.", e);
} finally {
post.releaseConnection();
}
}
/**
* Process the SOAP response into the ESB Message instance.
* @param message The ESB Message.
* @param response The SOAP response.
* @throws ActionProcessingException Error processing the response.
*/
protected void processResponse(Message message, Response response) throws ActionProcessingException {
Object responseObject;
if(responseXStreamDeserialzer != null) {
responseObject = applyXStreamResponseDeserializer(response.getBody());
} else if(responseAsOgnlMap) {
responseObject = populateResponseOgnlMap(response.getBody());
} else {
// Otherwise, the response Object is the raw SOAP String...
responseObject = response.getBody();
}
try {
payloadProxy.setPayload(message, responseObject);
} catch (MessageDeliverException e) {
throw new ActionProcessingException(e);
}
// JBESB-2511
org.jboss.soa.esb.message.Properties properties = message.getProperties();
for (Header header : response.getHeaders()) {
new ResponseHeader(header.getName(), header.getValue()).setPropertyNameThis(properties);
}
// JBESB-2761
if (httpResponseStatusEnabled) {
ResponseStatus.setHttpProperties(properties, response.getStatusCode(), response.getStatusMessage());
}
}
private Object applyXStreamResponseDeserializer(String response) {
StaxDriver driver = new StaxDriver(responseXStreamQNameMap);
HierarchicalStreamReader responseReader = driver.createReader(new StringReader(response));
// Move inside the soap body element...
responseReader.moveDown();
while(!responseReader.getNodeName().toLowerCase().endsWith("body")) {
responseReader.moveUp();
responseReader.moveDown();
}
responseReader.moveDown();
return responseXStreamDeserialzer.unmarshal(responseReader);
}
private Map<String, String> populateResponseOgnlMap(String response) throws ActionProcessingException {
Map<String, String> map = new LinkedHashMap<String, String>();
try {
DocumentBuilder docBuilder = getDocBuilder() ;
Document doc = docBuilder.parse(new InputSource(new StringReader(response)));
Element graphRootElement = getGraphRootElement(doc.getDocumentElement());
populateResponseOgnlMap(map, graphRootElement);
} catch (ParserConfigurationException e) {
throw new ActionProcessingException("Unexpected DOM Parser configuration error.", e);
} catch (SAXException e) {
throw new ActionProcessingException("Error parsing SOAP response.", e);
} catch (IOException e) {
throw new ActionProcessingException("Unexpected error reading SOAP response.", e);
}
return map;
}
private void populateResponseOgnlMap(Map<String, String> map, Element element) {
NodeList children = element.getChildNodes();
int childCount = children.getLength();
// If the element has a solitary TEXT child, add the text value
// against a map key of the elements OGNL expression value.
if(childCount == 1) {
Node childNode = children.item(0);
if(childNode.getNodeType() == Node.TEXT_NODE) {
String ognl = OGNLUtils.getOGNLExpression(element);
map.put(ognl, childNode.getTextContent());
return;
}
}
// So the element doesn't contain a solitary TEXT node. Drill down...
for(int i = 0; i < childCount; i++) {
Node childNode = children.item(i);
if(childNode.getNodeType() == Node.ELEMENT_NODE) {
populateResponseOgnlMap(map, (Element)childNode);
}
}
}
private Element getGraphRootElement(Element element) {
String ognl = OGNLUtils.getOGNLExpression(element);
if(ognl != null && !ognl.equals("")) {
return element;
}
NodeList children = element.getChildNodes();
int childCount = children.getLength();
for(int i = 0; i < childCount; i++) {
Node node = children.item(i);
if(node.getNodeType() == Node.ELEMENT_NODE) {
Element graphRootElement = getGraphRootElement((Element)node);
if(graphRootElement != null) {
return graphRootElement;
}
}
}
return null;
}
private static DocumentBuilderFactory createDocumentBuilderFactory() {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setNamespaceAware(true);
factory.setExpandEntityReferences(true);
return factory;
}
private static synchronized DocumentBuilder getDocBuilder() throws ParserConfigurationException {
return docBuilderFactory.newDocumentBuilder();
}
private void configureXStreamDeserializer(ConfigTree[] xstreamAliases, ConfigTree[] xstreamFieldAliases, ConfigTree[] xstreamAnnotations) throws ConfigurationException {
responseXStreamDeserialzer = new XStream();
for(ConfigTree xstreamAlias : xstreamAliases) {
String aliasName = xstreamAlias.getRequiredAttribute("name");
String aliasTypeName = xstreamAlias.getRequiredAttribute("class");
String namespace = xstreamAlias.getRequiredAttribute("namespace");
try {
Class aliasType = ClassUtil.forName(aliasTypeName, getClass());
responseXStreamQNameMap.registerMapping(new QName(namespace, aliasName), aliasType);
} catch (ClassNotFoundException e) {
throw new ConfigurationException("Invalid SOAP response deserializer config. XStream alias type '" + aliasTypeName + "' not found.");
}
}
for(ConfigTree xstreamFieldAlias : xstreamFieldAliases) {
final String alias = xstreamFieldAlias.getRequiredAttribute("name");
final String typeName = xstreamFieldAlias.getRequiredAttribute("class");
final String fieldName = xstreamFieldAlias.getRequiredAttribute("fieldName");
try {
final Class type = ClassUtil.forName(typeName, getClass());
responseXStreamDeserialzer.aliasField(alias, type, fieldName);
} catch (final ClassNotFoundException cnfe) {
throw new ConfigurationException("Invalid SOAP response deserializer config. XStream alias type '" + typeName + "' not found.");
}
}
for(ConfigTree xstreamAnnotation : xstreamAnnotations) {
final String typeName = xstreamAnnotation.getRequiredAttribute("class");
try {
final Class type = ClassUtil.forName(typeName, getClass());
Annotations.configureAliases(responseXStreamDeserialzer, type) ;
} catch (final ClassNotFoundException cnfe) {
throw new ConfigurationException("Invalid SOAP response deserializer config. XStream alias type '" + typeName + "' not found.");
}
}
}
}