org.jbpm.bpel.integration.server.SoapHandler.java Source code

Java tutorial

Introduction

Here is the source code for org.jbpm.bpel.integration.server.SoapHandler.java

Source

/*
 * JBoss, Home of Professional Open Source
 * Copyright 2005, JBoss Inc., and individual contributors as indicated
 * by the @authors tag.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the JBPM BPEL PUBLIC LICENSE AGREEMENT as
 * published by JBoss Inc.; either version 1.0 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.
 */
package org.jbpm.bpel.integration.server;

import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import javax.jms.Destination;
import javax.jms.JMSException;
import javax.jms.MessageConsumer;
import javax.jms.MessageProducer;
import javax.jms.ObjectMessage;
import javax.jms.Session;
import javax.servlet.ServletContext;
import javax.wsdl.Binding;
import javax.wsdl.Definition;
import javax.wsdl.Message;
import javax.wsdl.Operation;
import javax.wsdl.Port;
import javax.wsdl.PortType;
import javax.wsdl.extensions.soap.SOAPBinding;
import javax.xml.namespace.QName;
import javax.xml.rpc.JAXRPCException;
import javax.xml.rpc.handler.Handler;
import javax.xml.rpc.handler.HandlerInfo;
import javax.xml.rpc.handler.MessageContext;
import javax.xml.rpc.handler.soap.SOAPMessageContext;
import javax.xml.rpc.soap.SOAPFaultException;
import javax.xml.soap.SOAPBody;
import javax.xml.soap.SOAPElement;
import javax.xml.soap.SOAPEnvelope;
import javax.xml.soap.SOAPException;
import javax.xml.soap.SOAPMessage;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.xml.sax.SAXException;

import org.jbpm.JbpmConfiguration;
import org.jbpm.JbpmContext;
import org.jbpm.bpel.BpelException;
import org.jbpm.bpel.graph.def.BpelProcessDefinition;
import org.jbpm.bpel.graph.exe.BpelFaultException;
import org.jbpm.bpel.integration.jms.IntegrationConstants;
import org.jbpm.bpel.integration.jms.IntegrationControl;
import org.jbpm.bpel.integration.jms.PartnerLinkEntry;
import org.jbpm.bpel.integration.jms.RequestListener;
import org.jbpm.bpel.integration.soap.MessageDirection;
import org.jbpm.bpel.integration.soap.SoapBindConstants;
import org.jbpm.bpel.integration.soap.SoapFormatter;
import org.jbpm.bpel.integration.soap.SoapUtil;
import org.jbpm.bpel.sublang.def.PropertyQuery;
import org.jbpm.bpel.variable.def.MessageType;
import org.jbpm.bpel.wsdl.PropertyAlias;
import org.jbpm.bpel.wsdl.xml.WsdlUtil;
import org.jbpm.bpel.xml.util.DatatypeUtil;
import org.jbpm.bpel.xml.util.XmlUtil;
import org.jbpm.util.ClassLoaderUtil;

/**
 * @author Alejandro Guizar
 * @version $Revision$ $Date: 2007/11/25 13:06:55 $
 */
public class SoapHandler implements Handler {

    private QName[] headers;

    private IntegrationControl integrationControl;

    private QName portTypeName;
    private QName serviceName;
    private String portName;
    private SoapFormatter formatter;

    private static final Log log = LogFactory.getLog(SoapHandler.class);
    private static final Map endpointMetadataLookups = readEndpointMetadataLookups();

    /** Message context property for the operation name */
    static final String OPERATION_NAME_PROP = "org.jbpm.operation.name";
    /** Message context property for the message parts */
    static final String MESSAGE_PARTS_PROP = "org.jbpm.message.parts";
    /** Message context property for the fault name */
    static final String FAULT_NAME_PROP = "org.jbpm.fault.name";
    /** Message context property for the SOAP fault exception */
    static final String FAULT_EXCEPTION_PROP = "org.jbpm.fault.exception";

    /** Servlet context attribute for the integration control instance */
    public static final String INTEGRATION_CONTROL_ATTR = "org.jbpm.integration.control";

    public static final String RESOURCE_ENDPOINT_METADATA_LOOKUPS = "resource.endpoint.metadata.lookups";

    /**
     * Allocates a new handler for JAX-RPC use.
     */
    public SoapHandler() {
    }

    /**
     * Allocates a new handler for test purposes.
     */
    SoapHandler(IntegrationControl integrationControl, QName serviceName, Port port) {
        this.integrationControl = integrationControl;
        this.serviceName = serviceName;
        portName = port.getName();
        Binding binding = port.getBinding();
        portTypeName = binding.getPortType().getQName();
        formatter = new SoapFormatter(binding);
    }

    public void init(HandlerInfo handlerInfo) throws JAXRPCException {
        // save headers
        headers = handlerInfo.getHeaders();
    }

    public void destroy() {
        // release port-component specific state
        integrationControl = null;
    }

    public QName[] getHeaders() {
        return headers;
    }

    public boolean handleRequest(MessageContext messageContext) throws JAXRPCException, SOAPFaultException {
        /*
         * WSEE 1.1 section 6.2.2.1: If Handler instances are pooled, they must be pooled by Port
         * component. This is because Handlers may retain non-client specific state across method calls
         * that are specific to the Port component.
         */
        if (integrationControl == null) {
            /*
             * COMPLIANCE NOTE: the state initialized in this call is port-component specific, but
             * non-client specific
             */
            lookupEndpointMetadata(messageContext);
        }

        JbpmContext jbpmContext = integrationControl.getIntegrationServiceFactory().getJbpmConfiguration()
                .createJbpmContext();
        try {
            Session jmsSession = integrationControl.createJmsSession();
            try {
                SOAPMessage soapMessage = ((SOAPMessageContext) messageContext).getMessage();
                ObjectMessage jmsRequest = sendRequest(soapMessage, jmsSession, jbpmContext);

                Destination replyTo = jmsRequest.getJMSReplyTo();
                if (replyTo != null) {
                    ObjectMessage jmsResponse = receiveResponse(jmsSession, replyTo, jmsRequest.getJMSMessageID(),
                            jbpmContext);

                    // remember operation name and message parts for handling response
                    messageContext.setProperty(OPERATION_NAME_PROP,
                            jmsRequest.getStringProperty(IntegrationConstants.OPERATION_NAME_PROP));
                    messageContext.setProperty(MESSAGE_PARTS_PROP, jmsResponse.getObject());

                    // is response a fault?
                    String faultName = jmsResponse.getStringProperty(IntegrationConstants.FAULT_NAME_PROP);
                    if (faultName != null) {
                        // remember fault name for handling fault
                        messageContext.setProperty(FAULT_NAME_PROP, faultName);
                        throw new SOAPFaultException(SoapBindConstants.CLIENT_FAULTCODE,
                                SoapBindConstants.BUSINESS_FAULTSTRING, null, null);
                    }
                }
            } finally {
                jmsSession.close();
            }
        }
        /*
         * NO need to set jbpm context as rollback only for any exception, since operations in try-block
         * only read definitions from database
         */
        catch (SOAPFaultException e) {
            log.debug("request caused a fault", e);
            messageContext.setProperty(FAULT_EXCEPTION_PROP, e);
        } catch (SOAPException e) {
            /*
             * BP 1.2 R2724: If an INSTANCE receives an envelope that is inconsistent with its WSDL
             * description, it SHOULD generate a soap:Fault with a faultcode of "Client", unless a
             * "MustUnderstand" or "VersionMismatch" fault is generated.
             */
            log.debug("incoming soap message carries invalid content", e);
            messageContext.setProperty(FAULT_EXCEPTION_PROP,
                    new SOAPFaultException(SoapBindConstants.CLIENT_FAULTCODE, e.getMessage(), null, null));
        } catch (JMSException e) {
            throw new JAXRPCException("message delivery failed", e);
        } finally {
            jbpmContext.close();
        }
        return true;
    }

    private void lookupEndpointMetadata(MessageContext messageContext) {
        // obtain metadata lookup strategy for the given message context class
        EndpointMetadataLookup endpointMetadataLookup = getEndpointMetadataLookup(messageContext.getClass());
        // lookup metadata in message context
        EndpointMetadata endpointMetadata = endpointMetadataLookup.lookupMetaData(messageContext);

        Definition definition = endpointMetadata.getWsdlDefinition();
        serviceName = endpointMetadata.getServiceName();
        portName = endpointMetadata.getPortName();

        Binding binding = definition.getService(serviceName).getPort(portName).getBinding();
        portTypeName = binding.getPortType().getQName();
        formatter = new SoapFormatter(binding, endpointMetadata.getFaultFormat());

        ServletContext servletContext = endpointMetadata.getServletContext();
        integrationControl = (IntegrationControl) servletContext.getAttribute(INTEGRATION_CONTROL_ATTR);
        integrationControl.getMyCatalog().addDefinition(definition);
    }

    private static EndpointMetadataLookup getEndpointMetadataLookup(Class messageContextClass) {
        String contextClassName = messageContextClass.getName();
        EndpointMetadataLookup endpointMetadataLookup = (EndpointMetadataLookup) endpointMetadataLookups
                .get(contextClassName);
        if (endpointMetadataLookup == null) {
            throw new BpelException("no endpoint metadata lookup for message context: " + contextClassName);
        }
        return endpointMetadataLookup;
    }

    /**
     * Obtain the mapping of known message context implementations to their corresponding endpoint
     * metadata lookups from the configuration.
     */
    private static Map readEndpointMetadataLookups() {
        // get activity readers resource name
        String resource = JbpmConfiguration.Configs.getString(RESOURCE_ENDPOINT_METADATA_LOOKUPS);

        // parse lookups document
        Element lookupsElem;
        try {
            lookupsElem = XmlUtil.parseResource(resource);
        } catch (SAXException e) {
            log.error("endpoint metadata lookups document contains invalid xml: " + resource, e);
            return Collections.EMPTY_MAP;
        } catch (IOException e) {
            log.error("could not read endpoint metadata lookups document: " + resource, e);
            return Collections.EMPTY_MAP;
        }

        // walk through endpointMetadataLookup elements
        HashMap lookups = new HashMap();
        for (Iterator i = XmlUtil.getElements(lookupsElem, null, "endpointMetadataLookup"); i.hasNext();) {
            Element lookupElem = (Element) i.next();
            String contextClassName = lookupElem.getAttribute("messageContextClass");

            // load lookup class
            String lookupClassName = lookupElem.getAttribute("lookupClass");
            try {
                Class lookupClass = ClassLoaderUtil.getClassLoader().loadClass(lookupClassName);
                // validate lookup class
                if (!EndpointMetadataLookup.class.isAssignableFrom(lookupClass)) {
                    log.warn("not an endpoint metadata lookup: " + lookupClassName);
                    continue;
                }

                // instantiate lookup class
                Object lookup = lookupClass.newInstance();
                // register lookup
                lookups.put(contextClassName, lookup);
                log.debug("registered endpoint metadata lookup: name=" + contextClassName + ", class="
                        + lookupClassName);
            } catch (ClassNotFoundException e) {
                log.debug("endpoint metadata lookup not found, skipping: " + lookupClassName);
            } catch (InstantiationException e) {
                log.warn("endpoint metadata lookup class not instantiable: " + lookupClassName, e);
            } catch (IllegalAccessException e) {
                log.warn("endpoint metadata lookup class or constructor not public: " + lookupClassName, e);
            }
        }
        return lookups;
    }

    public boolean handleResponse(MessageContext messageContext) throws JAXRPCException {
        Map parts = (Map) messageContext.getProperty(MESSAGE_PARTS_PROP);
        SOAPFaultException faultException = (SOAPFaultException) messageContext.getProperty(FAULT_EXCEPTION_PROP);

        // absence of both parts and fault means one-way operation
        if (parts == null && faultException == null)
            return true;

        String operationName = (String) messageContext.getProperty(OPERATION_NAME_PROP);
        SOAPMessage soapMessage = ((SOAPMessageContext) messageContext).getMessage();

        JbpmContext jbpmContext = integrationControl.getIntegrationServiceFactory().getJbpmConfiguration()
                .createJbpmContext();
        try {
            lookupEndpointMetadata(messageContext);

            SOAPEnvelope envelope = soapMessage.getSOAPPart().getEnvelope();
            // remove existing body, it might have undesirable content
            SOAPBody body = envelope.getBody();
            body.detachNode();
            // re-create body
            body = envelope.addBody();

            if (faultException == null)
                writeOutput(operationName, soapMessage, parts);
            else {
                String faultName = (String) messageContext.getProperty(FAULT_NAME_PROP);
                writeFault(operationName, soapMessage, faultName, parts, faultException);
            }
        }
        /*
         * NO need to set jbpm context as rollback only for any exception, since operations in try-block
         * only read definitions from database
         */
        catch (SOAPException e) {
            throw new JAXRPCException("could not compose outbound soap message", e);
        } finally {
            jbpmContext.close();
        }
        return true;
    }

    public boolean handleFault(MessageContext messageContext) throws JAXRPCException {
        return true;
    }

    protected ObjectMessage sendRequest(SOAPMessage soapMessage, Session jmsSession, JbpmContext jbpmContext)
            throws SOAPException, JMSException {
        // create a jms message to deliver the incoming content
        ObjectMessage jmsRequest = jmsSession.createObjectMessage();

        // put the partner link identified by handle in a jms property
        PartnerLinkEntry partnerLinkEntry = integrationControl.getPartnerLinkEntry(portTypeName, serviceName,
                portName);
        long partnerLinkId = partnerLinkEntry.getId();
        jmsRequest.setLongProperty(IntegrationConstants.PARTNER_LINK_ID_PROP, partnerLinkId);

        Operation operation = determineOperation(soapMessage);
        if (operation == null)
            throw new SOAPException("could not determine operation to perform");

        // put the operation name in a jms property
        String operationName = operation.getName();
        jmsRequest.setStringProperty(IntegrationConstants.OPERATION_NAME_PROP, operationName);

        log.debug("received request: partnerLink=" + partnerLinkId + ", operation=" + operationName);

        // extract message content
        HashMap requestParts = new HashMap();
        formatter.readMessage(operationName, soapMessage, requestParts, MessageDirection.INPUT);
        jmsRequest.setObject(requestParts);

        // fill message properties
        BpelProcessDefinition process = integrationControl.getDeploymentDescriptor()
                .findProcessDefinition(jbpmContext);
        MessageType requestType = process.getImportDefinition()
                .getMessageType(operation.getInput().getMessage().getQName());
        fillCorrelationProperties(requestParts, jmsRequest, requestType.getPropertyAliases());

        // set up producer
        MessageProducer producer = jmsSession.createProducer(partnerLinkEntry.getDestination());
        try {
            // is the exchange pattern request/response?
            if (operation.getOutput() != null) {
                Destination replyTo = integrationControl.getIntegrationServiceFactory().getResponseDestination();
                jmsRequest.setJMSReplyTo(replyTo);

                // have jms discard request message if response timeout expires
                Number responseTimeout = getResponseTimeout(jbpmContext);
                if (responseTimeout != null)
                    producer.setTimeToLive(responseTimeout.longValue());
            } else {
                // have jms discard message if one-way timeout expires
                Number oneWayTimeout = getOneWayTimeout(jbpmContext);
                if (oneWayTimeout != null)
                    producer.setTimeToLive(oneWayTimeout.longValue());
            }

            // send request message
            producer.send(jmsRequest);
            log.debug("sent request: " + RequestListener.messageToString(jmsRequest));

            return jmsRequest;
        } finally {
            // release producer resources
            producer.close();
        }
    }

    private static Number getResponseTimeout(JbpmContext jbpmContext) {
        Object responseTimeout = jbpmContext.getObjectFactory().createObject("jbpm.bpel.response.timeout");
        if (responseTimeout instanceof Number)
            return (Number) responseTimeout;
        else if (responseTimeout != null)
            log.warn("response timeout is not a number: " + responseTimeout);
        return null;
    }

    private static Number getOneWayTimeout(JbpmContext jbpmContext) {
        Object oneWayTimeout = jbpmContext.getObjectFactory().createObject("jbpm.bpel.oneway.timeout");
        if (oneWayTimeout instanceof Number)
            return (Number) oneWayTimeout;
        else if (oneWayTimeout != null)
            log.warn("one-way timeout is not a number: " + oneWayTimeout);
        return null;
    }

    private Operation determineOperation(SOAPMessage soapMessage) throws SOAPException {
        Binding binding = formatter.getBinding();

        SOAPBinding soapBinding = (SOAPBinding) WsdlUtil.getExtension(binding.getExtensibilityElements(),
                com.ibm.wsdl.extensions.soap.SOAPConstants.Q_ELEM_SOAP_BINDING);

        String style = soapBinding.getStyle();
        if (style == null) {
            // wsdlsoap:binding does not specify any style, assume 'document'
            style = SoapBindConstants.DOCUMENT_STYLE;
        }

        PortType portType = binding.getPortType();
        SOAPElement bodyElement = SoapUtil.getElement(soapMessage.getSOAPBody());

        if (style.equals(SoapBindConstants.RPC_STYLE)) {
            String operationName = bodyElement.getLocalName();
            return portType.getOperation(operationName, null, null);
        }

        List operations = portType.getOperations();
        for (int i = 0, n = operations.size(); i < n; i++) {
            Operation operation = (Operation) operations.get(i);
            Message inputMessage = operation.getInput().getMessage();
            QName docLitElementName = WsdlUtil.getDocLitElementName(inputMessage);

            if (XmlUtil.nodeNameEquals(bodyElement, docLitElementName))
                return operation;
        }
        return null;
    }

    /**
     * Gets the values of message properties from the request message parts and sets them in the
     * property fields of the JMS message.
     * @param requestParts the parts extracted from the request SOAP message
     * @param jmsRequest the JMS message whose properties will be set
     * @param propertyAliases the property aliases associated with the request message type
     * @throws JMSException
     */
    private static void fillCorrelationProperties(Map requestParts, ObjectMessage jmsRequest, Map propertyAliases)
            throws JMSException {
        // easy way out: no property aliases
        if (propertyAliases == null)
            return;
        // iterate through the property aliases associated with the message type
        for (Iterator i = propertyAliases.entrySet().iterator(); i.hasNext();) {
            Entry aliasEntry = (Entry) i.next();
            QName propertyName = (QName) aliasEntry.getKey();
            PropertyAlias alias = (PropertyAlias) aliasEntry.getValue();
            // get part accessor from operation wrapper
            String partName = alias.getPart();
            Object value = requestParts.get(partName);
            if (value == null) {
                log.debug("message part not found, cannot get property value: property=" + propertyName + ", part="
                        + partName);
                continue;
            }
            // evaluate the query against the part value, if any
            PropertyQuery query = alias.getQuery();
            if (query != null) {
                try {
                    value = query.getEvaluator().evaluate((Element) value);
                } catch (BpelFaultException e) {
                    // the most likely cause is a selection failure due to missing nodes
                    log.debug("query evaluation failed, " + "cannot get property value: property=" + propertyName
                            + ", part=" + partName + ", query=" + query.getText(), e);
                    continue;
                }
            }
            // set the value in a jms message property field
            jmsRequest.setObjectProperty(propertyName.getLocalPart(),
                    value instanceof Node ? DatatypeUtil.toString((Node) value) : value);
        }
    }

    protected ObjectMessage receiveResponse(Session jmsSession, Destination replyTo, String requestId,
            JbpmContext jbpmContext) throws JMSException, SOAPFaultException {
        // set up consumer
        String selector = "JMSCorrelationID='" + requestId + '\'';
        MessageConsumer consumer = jmsSession.createConsumer(replyTo, selector);
        try {
            // receive response message
            log.debug("listening for response: destination=" + replyTo + ", requestId=" + requestId);
            Number responseTimeout = getResponseTimeout(jbpmContext);
            ObjectMessage jmsResponse = (ObjectMessage) (responseTimeout != null
                    ? consumer.receive(responseTimeout.longValue())
                    : consumer.receive());
            // did a message arrive in time?
            if (jmsResponse == null) {
                log.debug("response timeout expired: destination=" + replyTo + ", requestId" + requestId);
                throw new SOAPFaultException(SoapBindConstants.SERVER_FAULTCODE,
                        SoapBindConstants.TIMEOUT_FAULTSTRING, null, null);
            }
            jmsResponse.acknowledge();
            log.debug("received response: " + RequestListener.messageToString(jmsResponse));
            return jmsResponse;
        } finally {
            // release consumer resources
            consumer.close();
        }
    }

    protected void writeOutput(String operationName, SOAPMessage soapMessage, Map responseParts)
            throws SOAPException {
        formatter.writeMessage(operationName, soapMessage, responseParts, MessageDirection.OUTPUT);
    }

    protected void writeFault(String operationName, SOAPMessage soapMessage, String faultName, Map faultParts,
            SOAPFaultException faultException) throws SOAPException {
        formatter.writeFault(operationName, soapMessage, faultName, faultParts, faultException.getFaultCode(),
                faultException.getFaultString());
    }
}