/*
* JBoss, Home of Professional Open Source
* Copyright 2006, JBoss Inc., and others contributors as indicated
* by the @authors tag. All rights reserved.
* See the copyright.txt in the distribution for a
* full listing of individual contributors.
* This copyrighted material is made available to anyone wishing to use,
* modify, copy, or redistribute it subject to the terms and conditions
* of the GNU Lesser General Public License, v. 2.1.
* This program is distributed in the hope that it will be useful, but WITHOUT A
* 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,
* v.2.1 along with this distribution; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*
* (C) 2005-2006, JBoss Inc.
*/
package org.jboss.soa.esb.actions.soap;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.ServletContext;
import org.jboss.internal.soa.esb.publish.Publish;
import org.jboss.internal.soa.esb.util.JBossDeployerUtil;
import org.jboss.metadata.web.jboss.JBossWebMetaData;
import org.jboss.soa.esb.ConfigurationException;
import org.jboss.soa.esb.actions.AbstractActionPipelineProcessor;
import org.jboss.soa.esb.actions.ActionProcessingException;
import org.jboss.soa.esb.actions.ActionUtils;
import org.jboss.soa.esb.actions.soap.adapter.JBossWSFactory;
import org.jboss.soa.esb.actions.soap.adapter.SOAPProcessorFactory;
import org.jboss.soa.esb.actions.soap.adapter.SOAPProcessorHttpServletRequest;
import org.jboss.soa.esb.actions.soap.adapter.SOAPProcessorHttpServletResponse;
import org.jboss.soa.esb.helpers.ConfigTree;
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.Properties;
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.wsf.spi.deployment.Deployment;
import org.jboss.wsf.spi.deployment.Endpoint;
import org.jboss.wsf.spi.invocation.EndpointAssociation;
import org.jboss.wsf.spi.invocation.RequestHandler;
/**
* JBoss Webservices SOAP Processor.
* <p/>
* This action supports invocation of a JBossWS hosted webservice endpoint through any JBossESB hosted
* listener. This means the ESB can be used to expose Webservice endpoints for Services that don't
* already expose a Webservice endpoint. You can do this by writing a thin Service Wrapper Webservice
* (e.g. a JSR 181 implementation) that wraps calls to the target Service (that doesn't have a Webservice endpoint),
* exposing that Service via endpoints (listeners) running on the ESB. This also means that these Services
* are invocable over any transport channel supported by the ESB (http, ftp, jms etc).
*
* <h3>"ESB Message Aware" Webservice Endpoints</h3>
* Note that Webservice endpoints exposed via this action have direct access to the current
* JBossESB {@link org.jboss.soa.esb.message.Message} instance used to invoke this action's
* {@link #process(org.jboss.soa.esb.message.Message)} method. It can access
* the current {@link org.jboss.soa.esb.message.Message} instance via the {@link #getMessage()} method
* and can change the {@link org.jboss.soa.esb.message.Message} instance via the
* {@link #setMessage(org.jboss.soa.esb.message.Message)} method. This means that Webservice endpoints
* exposed via this action are "ESB Message Aware".
*
* <h3>Webservice Endpoint Deployment</h3>
* Any JBossWS Webservice endpoint can be exposed via ESB listeners using this action. That includes endpoints that are deployed
* from inside (i.e. the Webservice .war is bundled inside the .esb) and outside (e.g. standalone Webservice .war deployments,
* Webservice .war deployments bundled inside a .ear) a .esb deployment.
*
* <div style="margin-left: 20">
* <h4>JAXB Introductions</h4>
* The native JBossWS SOAP stack uses JAXB to bind to and from SOAP. This typically means that an unannotated typeset
* could not be used to build a JSR 181 endpoint on JBossWS. To overcome this we use a JBossESB and JBossWS feature
* called "JAXB Introductions" which basically means you can define an XML configuration to "Introduce" the JAXB Annotations.
* For more on this, see the section on this action in the Message Action Guide.
* </div>
*
* <h3>Action Configuration</h3>
* The <action ... /> configuration for this action is very straightforward. The action requires only one
* mandatory property value, which is the "jbossws-endpoint" property. This property names the JBossWS endpoint
* that the SOAPProcessor is exposing (invoking).
*
* <pre>
* <action name="ShippingProcessor" class="org.jboss.soa.esb.actions.soap.SOAPProcessor">
* <property name="<b>jbossws-endpoint</b>" value="<b>ABI_Shipping</b>"/>
* <property name="<b>rewrite-endpoint-url</b>" value="true/false"/> <-- Optional. Default "true". -->
* </action>
* </pre>
*
* The optional "rewrite-endpoint-url" property is there to support load balancing on HTTP endpoints,
* in which case the Webservice endpoint container will have been configured to set the HTTP(S) endpoint address
* in the WSDL to that of the Load Balancer. The "rewrite-endpoint-url" property can be used to turn off HTTP endpoint
* address rewriting in situations such as this. It has no effect for non-HTTP protocols.
*
* <h3>Quickstarts</h3>
* A number of quickstarts that demonstrate how to use this action are available in the JBossESB
* distribution (samples/quickstarts). See the "webservice_jbossws_adapter_01" and "webservice_bpel"
* quickstarts.
*
* @author <a href="mailto:tom.fennelly@jboss.com">tom.fennelly@jboss.com</a>
* @author <a href="mageshbk@jboss.com">Magesh Kumar B</a>
*/
@Publish(JBossWSWebserviceContractPublisher.class)
public class SOAPProcessor extends AbstractActionPipelineProcessor {
private static ThreadLocal<Message> messageTL = new ThreadLocal<Message>();
private String jbossws_endpoint;
private String jbossws_context;
private MessagePayloadProxy payloadProxy;
private boolean httpResponseStatusEnabled;
/**
* Public constructor.
* @param config Configuration.
* @throws ConfigurationException "jbossws-endpoint" not specified.
*/
public SOAPProcessor(ConfigTree config) throws ConfigurationException {
jbossws_endpoint = config.getRequiredAttribute(WebServiceUtils.JBOSSWS_ENDPOINT);
jbossws_context = config.getAttribute(WebServiceUtils.JBOSSWS_CONTEXT);
payloadProxy = new MessagePayloadProxy(config,
new String[] {BytesBody.BYTES_LOCATION, ActionUtils.POST_ACTION_DATA},
new String[] {ActionUtils.POST_ACTION_DATA});
httpResponseStatusEnabled = ResponseStatus.isHttpEnabled(config);
}
/**
* Process the SOAP message.
* <p/>
* Invokes the JBossWS endpoint and writes the SOAP response back into the message payload.
* @param message The ESB Aware (normalized) SOAP request message.
* @return The SOAP response message.
* @throws ActionProcessingException
*/
public Message process(Message message) throws ActionProcessingException {
Endpoint endpoint = WebServiceUtils.getServiceEndpoint(jbossws_endpoint, jbossws_context);
byte[] soapMessage;
if(endpoint == null) {
throw new ActionProcessingException("Unknown Service Endpoint '" + jbossws_endpoint + "'.");
}
soapMessage = getSOAPMessagePayload(message);
try {
messageTL.set(message);
RequestHandler requestHandler = endpoint.getRequestHandler();
final Map<String, List<String>> headers = new HashMap<String, List<String>>() ;
final Properties properties = message.getProperties() ;
final String[] names = properties.getNames() ;
for(final String name: names)
{
final Object value = properties.getProperty(name) ;
if (value != null)
{
String normalisedName = name.toLowerCase() ;
if ("content-type".equals(normalisedName))
{
if ("application/octet-stream".equals(value))
{
continue;
}
else
{
// CXF needs it to be case sensitive
normalisedName = "Content-Type";
}
}
final List<String> values = headers.get(normalisedName) ;
if (values == null)
{
final List<String> newValues = new ArrayList<String>() ;
newValues.add(value.toString()) ;
headers.put(normalisedName, newValues) ;
}
else
{
values.add(value.toString()) ;
}
}
}
//CXF throws NPE in handler if content-length not set
List<String> newValues = new ArrayList<String>();
newValues.add(String.valueOf(soapMessage.length));
headers.put("content-length", newValues);
final String endpointAddress = endpoint.getAddress() ;
String path = null ;
if (endpointAddress != null)
{
try
{
path = new URI(endpointAddress).getPath() ;
}
catch (final URISyntaxException urise) {} //ignore
}
if (path == null)
{
path = getHeaderValue(headers, "path") ;
}
final SOAPProcessorHttpServletRequest servletRequest = new SOAPProcessorHttpServletRequest(path, soapMessage, headers) ;
final SOAPProcessorHttpServletResponse servletResponse = new SOAPProcessorHttpServletResponse() ;
final ServletContext servletContext = SOAPProcessorFactory.getFactory().createServletContext(endpoint) ;
EndpointAssociation.setEndpoint(endpoint);
final ClassLoader old = Thread.currentThread().getContextClassLoader();
try
{
initialiseContextClassLoader(endpoint);
requestHandler.handleHttpRequest(endpoint, servletRequest, servletResponse, servletContext) ;
}
finally
{
Thread.currentThread().setContextClassLoader(old);
EndpointAssociation.removeEndpoint();
}
// This may have been changed, make sure we get the current version.
message = messageTL.get();
final Properties responseProperties = message.getProperties() ;
final String contentType = servletResponse.getContentType() ;
final Map<String, List<String>> responseHeaders = servletResponse.getHeaders() ;
// We deal with Content-Type below
// HTTP Headers *should* be case-insensitive but not with JBR
responseHeaders.remove("content-type") ;
for(Map.Entry<String, List<String>> header: responseHeaders.entrySet())
{
// We can only deal with the first value in the list.
// JBESB-2511
new ResponseHeader(header.getKey(), header.getValue().get(0)).setPropertyNameThis(properties);
}
// JBESB-2761
if (httpResponseStatusEnabled) {
ResponseStatus.setHttpProperties(properties, servletResponse.getStatus(), servletResponse.getStatusMessage());
}
final byte[] responseData = servletResponse.getContent() ;
if(contentType != null) {
responseProperties.setProperty("Content-Type", new ResponseHeader("Content-Type", contentType));
} else {
responseProperties.setProperty("Content-Type", new ResponseHeader("Content-Type", "text/xml"));
}
if ((contentType != null) && contentType.startsWith("multipart/")) {
payloadProxy.setPayload(message, responseData) ;
} else {
final String charset = servletResponse.getCharset() ;
if (charset == null) {
payloadProxy.setPayload(message, new String(responseData)) ;
} else {
payloadProxy.setPayload(message, new String(responseData, charset)) ;
}
}
} catch (Exception ex) {
throw new ActionProcessingException("Cannot process SOAP request", ex);
} finally {
messageTL.remove();
}
return message;
}
private void initialiseContextClassLoader(final Endpoint endpoint)
throws ActionProcessingException
{
if (JBossDeployerUtil.isWebMetaDataPresent())
{
final ClassLoader tccl = JBossWSFactory.getFactory().getClassLoader(endpoint) ;
if (tccl != null)
{
Thread.currentThread().setContextClassLoader(tccl);
return ;
}
throw new ActionProcessingException("Could not locate ENC ClassLoader for service endpoint '" + jbossws_endpoint + "'.");
}
}
private String getHeaderValue(final Map<String, List<String>> headers,
final String header)
{
final List<String> values = headers.get(header) ;
if (values != null)
{
return values.get(0) ;
}
return null ;
}
private byte[] getSOAPMessagePayload(Message message) throws ActionProcessingException {
byte[] soapMessage;
Object messagePayload;
try {
messagePayload = payloadProxy.getPayload(message);
} catch (MessageDeliverException e) {
throw new ActionProcessingException(e);
}
if(messagePayload instanceof byte[]) {
soapMessage = (byte[])messagePayload;
} else if(messagePayload instanceof String) {
try {
soapMessage = ((String)messagePayload).getBytes("UTF-8");
} catch (UnsupportedEncodingException e) {
throw new ActionProcessingException("Unable to decode SOAP message payload.", e);
}
} else {
throw new ActionProcessingException("Unable to decode SOAP message payload. Must be either a byte[] or java.lang.String.");
}
return soapMessage;
}
/**
* Set the {@link org.jboss.soa.esb.message.Message} instance for this invocation context.
* <p/>
* This allows message aware Webservice endpoints modify the {@link org.jboss.soa.esb.message.Message}
* instance for the current Action Processing Pipeline.
*
* @param message The new message instance.
*/
public static void setMessage(final Message message) {
messageTL.set(message);
}
/**
* Get the {@link org.jboss.soa.esb.message.Message} instance for this invocation context.
*
* @return The message instance.
* @deprecated
*/
public static Message getMessage() {
Message message = messageTL.get();
if (message == null)
throw new RuntimeException("ESB message not found!");
return message;
}
/**
* Removes {@link org.jboss.soa.esb.message.Message} instance for this invocation context.
*
*/
public static void removeMessage() {
messageTL.remove();
}
}