/*
* 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-2009
*/
package org.jboss.soa.esb.actions.soap.proxy;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.StringReader;
import java.net.URL;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.wsdl.Binding;
import javax.wsdl.BindingOperation;
import javax.wsdl.Definition;
import javax.wsdl.Input;
import javax.wsdl.Operation;
import javax.wsdl.Part;
import javax.wsdl.Port;
import javax.wsdl.PortType;
import javax.wsdl.Service;
import javax.wsdl.extensions.ExtensibilityElement;
import javax.wsdl.extensions.soap.SOAPAddress;
import javax.wsdl.extensions.soap.SOAPOperation;
import javax.wsdl.extensions.soap12.SOAP12Address;
import javax.wsdl.extensions.soap12.SOAP12Operation;
import javax.xml.namespace.QName;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import org.jboss.internal.soa.esb.publish.ContractProvider;
import org.jboss.internal.soa.esb.publish.ContractProviderLifecycleResource;
import org.jboss.internal.soa.esb.publish.Publish;
import org.jboss.soa.esb.Configurable;
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.soap.WebServiceUtils;
import org.jboss.soa.esb.helpers.ConfigTree;
import org.jboss.soa.esb.http.HttpRequest;
import org.jboss.soa.esb.lifecycle.LifecycleResourceException;
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.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;
import org.xml.sax.helpers.XMLReaderFactory;
/**
* A SOAPProxy focuses on the consumption of an external WS endpoint (e.g. hosted on .NET, another external Java-based AS, LAMP)
* and re-publication of a WS endpoint via the ESB. The ESB sits between the ultimate consumer/client (e.g. .NET WinForm
* application) and the ultimate producer (e.g. RoR-hosted WS). The purpose of this intermediary is to provide an abstraction
* layer that solves the following problems:
* <ul>
* <li>Provides for more loose coupling between the client & service; they are both completely unaware of each other.</li>
* <li>The client no longer has a direct connection to the remote service's hostname/IP address.</li>
* <li>The client will see modified WSDL that changes the inbound/outbound parameters. At a minimum, the WSDL must be tweaked so that the client is pointed to the ESB's exposed endpoint instead of the original, now proxied endpoint.</li>
* <li>A transformation of the SOAP envelope/body can be introduced via the ESB action chain both for the inbound request and outbound response. (see XsltAction or SmooksAction)</li>
* <li>Service versioning is possible since clients can connect to 2 or more proxy endpoints on the ESB, each with its own WSDL and/or transformations and routing requirements, and the ESB will send the appropriate message to the appropriate endpoint and provide an ultimate response.</li>
* <li>Complex context-based routing via ContentBasedRouter.</li>
* </ul>
*
* Other mechanisms of doing this are inappropriate or inadequate:
* <ul>
* <li>SOAPClient is used to invoke external web services, not mirror them.</li>
* <li>SOAPProducer only executes internally-deployed JBoss WS services.</li>
* <li>HttpRouter requires too much by-hand configuration for easy WS proxying.</li>
* <li>EBWS strips out the SOAP Envelope and only passes along the body.</li>
* </ul>
*
* With a SOAPProxy action:
* <ul>
* <li>It is both a producer and consumer of web services.</li>
* <li>All that is required is a property pointing to the external wsdl.</li>
* <li>The wsdl can be automatically transformed via the optional wsdlTransform property.</li>
* <li>It is understood that SOAP is not tied to http. The wsdl is read, and if an http transport is defined, that will be used. Other transports (jms) will need future consideration.</li>
* <li>If using http, any of the HttpRouter properties can also optionally be applied to as overrides.</li>
* </ul>
*
* <i>Configuration Properties</i><br/>
* <ul>
* <li><b>wsdl</b> (required): The original wsdl {@link URL url} whose WS endpoint will get re-written and exposed as new wsdl from
* the ESB. Depending upon the <definitions><service><port><soap:address location attribute's protocol (for
* example "http"), a protocol-specific {@link SOAPProxyTransport} implementation is used. The value can reference a location based on
* five different schemes:<br/>
* <ul>
* <li><b>http://</b><br/>
* <ul>
* <li>Usage: When you want to pull wsdl from an external web server.</li>
* <li>Example: http://host/foo/HelloWorldWS?wsdl</li>
* </ul></li>
* <li><b>https://</b><br/>
* <ul>
* <li>Usage: When you want to pull wsdl from an external web server over SSL.</li>
* <li>Example: https://host/foo/HelloWorldWS?wsdl</li>
* </ul></li>
* <li><b>file://</b><br/>
* <ul>
* <li>Usage: When your wsdl is located on disk, accessible by the ESB JVM.</li>
* <li>Example: file:///tmp/HelloWorldWS.wsdl</li>
* <li><i>Note: <b>3</b> slashes in the example above. This is so we can specify an absolute vs. relative file path.</i></li>
* </ul></li>
* <li><b>classpath://</b><br/>
* <ul>
* <li>Usage: When you want to package your wsdl inside your ESB archive.</li>
* <li>Example: classpath:///META-INF/HelloWorldWS.wsdl</li>
* <li><i>Note: <b>3</b> slashes in the example above. This is so we can specify an absolute vs. relative classloader resource path.</i></li>
* </ul></li>
* <li><b>internal://</b><br/>
* <ul>
* <li>Usage: When the wsdl is being provided by a JBossWS web service <b>inside the same JVM</b> as this ESB deployment.</li>
* <li>Example: internal://HelloWorldWS</li>
* <li><i>Note: This scheme should be used instead of http or https in the usage described above. This is because on server restart, Tomcat may not yet be accepting incoming http/s requests, and thus cannot serve the wsdl.</i></li>
* </ul></li>
* </ul></li>
* <li><b>wsdlTransform</b> (optional): A <smooks-resource-list> xml config file allowing for flexible wsdl transformation.</li>
* <li><b>wsdlCharset</b> (optional): The character set the original wsdl (and imported resources) is encoded in, if not UTF-8. It will be transformed to
* UTF-8 if it is a <a href="http://java.sun.com/javase/6/docs/technotes/guides/intl/encoding.doc.html">supported encoding</a> by the underlying platform.</li>
* <li><b>*</b> (optional): Any of the HttpRouter properties can be applied, if the wsdl specifies an http transport.</li>
* <li><b>endpointUrl</b> (optional): Example of an HttpRouter property, but useful when domain name matching is important for SSL certs.</li>
* <li><b>file</b> (optional): Apache Commons HTTPClient properties file, useful when proxying to a web service via SSL</li>
* <li><b>clientCredentialsRequired</b> (optional; default is "true"): Whether the Basic Auth credentials are required to come from the end
* client, or if the credentials specified inside <b>file</b> can be used instead.</li>
* <li><b>wsdlUseHttpClientProperties</b> (optional): if true then WSDL retrieval will use the same http-client-property as main webservice, otherwise will use wsdl-http-client-property entries.
* </ul>
* <b>*</b> For other possible configuration properties, see the specific {@link SOAPProxyTransport} implementations themselves.<p/>
*
* <i>Example of a straightforward scenario:</i><br/>
* <pre>
* <action name="proxy" class="org.jboss.soa.esb.actions.soap.proxy.SOAPProxy">
* <property name="wsdl" value="http://host/foo/HelloWorldWS?wsdl"/>
* </action>
* </pre>
* <i>Example of a basic auth + ssl scenario:</i><br/>
* <pre>
* <action name="proxy" class="org.jboss.soa.esb.actions.soap.proxy.SOAPProxy">
* <property name="wsdl" value="https://host/foo/HelloWorldWS?wsdl"/>
* <property name="endpointUrl" value="https://host/foo/HelloWorldWS"/>
* <property name="file" value="/META-INF/httpclient-8443.properties"/>
* <property name="clientCredentialsRequired" value="true"/>
* </action>
* </pre>
*
* @author dward at jboss.org
* @author <a href="mailto:mageshbk@jboss.com">Magesh Kumar B</a>
*/
@Publish(SOAPProxyWsdlContractPublisher.class)
public class SOAPProxy extends AbstractActionPipelineProcessor
{
private static Logger logger = Logger.getLogger(SOAPProxy.class);
private MessagePayloadProxy payloadProxy;
private Map<String,QName> soapaction_to_binding = new HashMap<String,QName>();
private Map<QName,QName> element_to_operation = new HashMap<QName,QName>();
private Map<QName,QName> operation_to_binding = new HashMap<QName,QName>();
private Map<QName,SOAPProxyTransport> binding_to_transport = new HashMap<QName,SOAPProxyTransport>();
@SuppressWarnings({ "unchecked", "rawtypes" })
public SOAPProxy(ConfigTree config) throws ConfigurationException
{
payloadProxy = new MessagePayloadProxy(config);
initialiseContractPublisher(config);
SOAPProxyWsdlLoader wsdl_loader = SOAPProxyWsdlLoader.newLoader(config);
Definition wsdl_def;
try
{
wsdl_loader.load(false);
wsdl_def = WebServiceUtils.readWSDL(wsdl_loader.getURL());
}
catch (Exception ioe)
{
throw new ConfigurationException(ioe);
}
finally
{
wsdl_loader.cleanup();
}
Collection<Binding> bindings = wsdl_def.getBindings().values();
for ( Binding wsdl_bind : bindings )
{
PortType wsdl_portType = wsdl_bind.getPortType();
if (wsdl_portType != null)
{
Collection<Operation> operations = wsdl_portType.getOperations();
for (Operation wsdl_portType_oper : operations)
{
Input wsdl_portType_oper_input = wsdl_portType_oper.getInput();
if (wsdl_portType_oper_input != null)
{
javax.wsdl.Message wsdl_portType_oper_input_msg = wsdl_portType_oper_input.getMessage();
if (wsdl_portType_oper_input_msg != null)
{
Collection wsdl_portType_oper_input_msg_parts = wsdl_portType_oper_input_msg.getParts().values();
if (wsdl_portType_oper_input_msg_parts.size() != 1)
{
// size should only be 1 for document
continue;
}
Part wsdl_portType_oper_input_msg_part = (Part)wsdl_portType_oper_input_msg_parts.iterator().next();
QName element = wsdl_portType_oper_input_msg_part.getElementName();
if ( element != null && !element_to_operation.containsKey(element) )
{
QName operation = new QName(wsdl_portType.getQName().getNamespaceURI(), wsdl_portType_oper.getName());
// no need for a duplicate mapping
if ( operation != null && !element.equals(operation) )
{
element_to_operation.put(element, operation);
if ( logger.isInfoEnabled() )
{
logger.info("mapped element [" + element + "] to operation [" + operation + "]");
}
}
}
}
}
}
}
List<BindingOperation> operations = wsdl_bind.getBindingOperations();
for ( BindingOperation wsdl_bind_oper : operations )
{
QName binding = wsdl_bind.getQName();
if (binding != null)
{
// http://www.w3.org/TR/2000/NOTE-SOAP-20000508/#_Toc478383528
// http://www.ws-i.org/Profiles/BasicProfile-1.1.html#SOAPAction_HTTP_Header
String soapaction = null;
List<ExtensibilityElement> extList = wsdl_bind_oper.getExtensibilityElements();
for (ExtensibilityElement extElement : extList)
{
if (extElement instanceof SOAPOperation)
{
SOAPOperation soapOp = (SOAPOperation)extElement;
soapaction = soapOp.getSoapActionURI();
}
else if (extElement instanceof SOAP12Operation)
{
SOAP12Operation soapOp = (SOAP12Operation)extElement;
soapaction = soapOp.getSoapActionURI();
}
}
if (soapaction != null)
{
if ( !soapaction.startsWith("\"") && !soapaction.endsWith("\"") )
{
soapaction = '"' + soapaction + '"';
}
if ( !soapaction_to_binding.containsKey(soapaction) )
{
soapaction_to_binding.put(soapaction, binding);
if ( logger.isInfoEnabled() )
{
logger.info("mapped soapaction [" + soapaction + "] to binding [" + binding + "]");
}
}
}
QName operation = new QName(wsdl_portType.getQName().getNamespaceURI(), wsdl_bind_oper.getOperation().getName());
if ( operation != null && !operation_to_binding.containsKey(operation) )
{
operation_to_binding.put(operation, binding);
if ( logger.isInfoEnabled() )
{
logger.info("mapped operation [" + operation + "] to binding [" + binding + "]");
}
}
}
}
}
Collection<Service> services = wsdl_def.getServices().values();
for ( Service wsdl_svc : services )
{
Collection<Port> ports = wsdl_svc.getPorts().values();
for ( Port wsdl_end : ports )
{
QName binding = wsdl_end.getBinding().getQName();
SOAPProxyTransport transport = null;
String endpointAddress = getSOAPAddress(wsdl_end);
if ( endpointAddress.toLowerCase().startsWith("http") )
{
transport = new HttpSOAPProxyTransport(config, payloadProxy, endpointAddress);
}
// else if jms ...
if (transport != null)
{
if ( !binding_to_transport.containsKey(binding) )
{
binding_to_transport.put(binding, transport);
if ( logger.isInfoEnabled() )
{
logger.info("mapped binding [" + binding + "] to transport [" + transport.getClass().getName() + "] with endpoint address: [" + transport.getEndpointAddress() + "]");
}
}
}
else
{
if ( logger.isEnabledFor(Level.WARN) )
{
logger.warn("could not map binding [" + binding + "] to transport with endpoint address: [" + endpointAddress + "]");
}
}
}
}
}
private void initialiseContractPublisher(final ConfigTree config)
throws ConfigurationException
{
final ConfigTree parent = config.getParent();
if (parent != null)
{
final String category = parent.getAttribute(ListenerTagNames.SERVICE_CATEGORY_NAME_TAG);
final String name = parent.getAttribute(ListenerTagNames.SERVICE_NAME_TAG);
if ((category != null) && (name != null))
{
final ContractProvider provider ;
try
{
provider = ContractProviderLifecycleResource.getContractProvider(category, name);
}
catch (final LifecycleResourceException lre)
{
throw new ConfigurationException("Unexpected exception querying contract provider", lre);
}
if ((provider != null) && (provider instanceof Configurable))
{
final Configurable configurable = Configurable.class.cast(provider) ;
configurable.setConfiguration(config);
}
}
}
}
public void initialise() throws ActionLifecycleException
{
for ( SOAPProxyTransport transport : binding_to_transport.values() )
{
transport.initialise();
}
}
public Message process(Message message) throws ActionProcessingException
{
HttpRequest request = HttpRequest.getRequest(message);
String soapaction = (request != null) ? request.getHeaderValue("soapaction") : null;
if (soapaction == null)
{
soapaction = (String)message.getProperties().getProperty("soapaction");
}
QName element = null;
QName operation = null;
QName binding = (soapaction != null) ? soapaction_to_binding.get(soapaction) : null;
if (binding == null)
{
if ( logger.isEnabledFor(Level.WARN) )
{
logger.warn("null binding for soapaction [" + soapaction + "]; parsing envelope to find element or operation...");
}
element = findElement(message);
operation = element;
if (element != null)
{
if ( element_to_operation.containsKey(element) )
{
operation = element_to_operation.get(element);
}
binding = (operation != null) ? operation_to_binding.get(operation) : null;
}
}
if ( binding == null && logger.isEnabledFor(Level.ERROR) )
{
logger.error("null binding for element [" + element + "] or operation [" + operation + "] in addition to soapaction [" + soapaction + "]");
}
SOAPProxyTransport transport = (binding != null) ? binding_to_transport.get(binding) : null;
if (transport == null)
{
throw new ActionProcessingException("null transport for soapaction [" + soapaction + "], element [" + element + "], operation [" + operation + "], binding [" + binding + "]");
}
if ( logger.isDebugEnabled() )
{
logger.debug("using transport [" + transport.getClass().getName() + "] with endpoint address: [" + transport.getEndpointAddress() + "] for binding [" + binding + "]");
}
return transport.process(message);
}
public void destroy() throws ActionLifecycleException
{
for ( SOAPProxyTransport transport : binding_to_transport.values() )
{
transport.destroy();
}
}
/** Get the endpoint address from the ports extensible element
*/
private String getSOAPAddress(Port srcPort) throws ConfigurationException
{
String soapAddress = "dummy";
@SuppressWarnings("unchecked")
List<ExtensibilityElement> elements = srcPort.getExtensibilityElements();
for ( ExtensibilityElement extElement : elements )
{
QName elementType = extElement.getElementType();
if ( extElement instanceof SOAPAddress )
{
SOAPAddress addr = (SOAPAddress)extElement;
soapAddress = addr.getLocationURI();
break;
}
else if ( extElement instanceof SOAP12Address )
{
SOAP12Address addr = (SOAP12Address)extElement;
soapAddress = addr.getLocationURI();
break;
}
else if ("address".equals(elementType.getLocalPart()))
{
logger.warn("Unprocessed extension element: " + elementType);
}
}
if (soapAddress == null)
throw new ConfigurationException("Cannot obtain SOAP address");
return soapAddress;
}
// This is a best guess (and potentially expensive)! See logger.warn(String) warning in process(Message) above.
private QName findElement(Message message) throws ActionProcessingException
{
Object payload;
try
{
payload = payloadProxy.getPayload(message);
}
catch (MessageDeliverException mde)
{
throw new ActionProcessingException(mde);
}
InputSource is = null;
if (payload instanceof byte[])
{
byte[] byte_payload = (byte[])payload;
if (byte_payload.length == 0)
{
throw new ActionProcessingException("message contains zero-length byte[] payload");
}
is = new InputSource( new ByteArrayInputStream(byte_payload) );
}
else if (payload instanceof String)
{
String string_payload = (String)payload;
if (string_payload.length() == 0)
{
throw new ActionProcessingException("message contains zero-length String payload");
}
is = new InputSource( new StringReader(string_payload) );
}
else
{
throw new ActionProcessingException( "unsupported payload type: " + payload.getClass().getName() );
}
QName element = null;
ContentHandler ch = new ElementFinder();
try
{
XMLReader xr = XMLReaderFactory.createXMLReader();
xr.setContentHandler(ch);
xr.parse(is);
}
catch (SAXException saxe)
{
throw new ActionProcessingException(saxe);
}
catch (IOException ioe)
{
throw new ActionProcessingException(ioe);
}
catch (ElementFinder.ElementFound ef)
{
element = ef.element;
}
return element;
}
private static class ElementFinder extends DefaultHandler
{
private boolean insideEnvelope = false;
private boolean insideBody = false;
@Override
public void startElement(String uri, String localName, String qName, Attributes atts)
{
if ( localName.equals("Envelope") )
{
insideEnvelope = true;
}
else if ( localName.equals("Body") )
{
insideBody = true;
}
else if (insideEnvelope && insideBody)
{
// stop parsing as soon as possible!
throw new ElementFound( new QName(uri, localName) );
}
}
@SuppressWarnings("serial")
private static class ElementFound extends RuntimeException
{
private QName element;
private ElementFound(QName element)
{
this.element = element;
}
}
}
}