RESTful Invoke BPM Process Using Apache CXF

Apache CXF is a services framework that is the open source evolution of IONA Celtix and Codehaus XFire, hence the name CXF. CXF has extensive support for Web Service standards WS-*, JAX-WS and JAX-RS APIs etc. but the focus of this article is on the CXF web service client proxy factory and running it in a WebLogic web app implementing a REST service. As a demonstration we will build a service using JDeveloper that invokes an Oracle BPM process with a message start service interface. REST “clients” are lightweight and simpler to implement than SOAP web service clients. A REST to SOAP converter for BPM processes make them more accessible.

For the impatient, the two key enablers are to determine the proper subset of CXF library jars and resolve any conflicts with WebLogic libraries. The CXF distribution has 149 jars and you don’t want to just simply add them all to your project. The following list is based on the dynamic client sample in the CXF distribution from the 3.1.0 release.

cxf-rt-frontend-jaxws-3.1.0.jar
cxf-rt-transports-http-3.1.0.jar
cxf-rt-transports-http-jetty-3.1.0.jar
jaxb-xjc-2.2.11.jar
aopalliance-1.0.jar
asm-5.0.3.jar
cxf-core-3.1.0.jar
cxf-rt-bindings-soap-3.1.0.jar
cxf-rt-bindings-xml-3.1.0.jar
cxf-rt-databinding-jaxb-3.1.0.jar
cxf-rt-frontend-simple-3.1.0.jar
cxf-rt-ws-addr-3.1.0.jar
cxf-rt-ws-policy-3.1.0.jar
cxf-rt-wsdl-3.1.0.jar
javax.servlet-api-3.1.0.jar
jaxb-core-2.2.11.jar
jaxb-impl-2.2.11.jar
jcl-over-slf4j-1.7.9.jar
jetty-continuation-9.2.9.v20150224.jar
jetty-http-9.2.9.v20150224.jar
jetty-io-9.2.9.v20150224.jar
jetty-security-9.2.9.v20150224.jar
jetty-server-9.2.9.v20150224.jar
jetty-util-9.2.9.v20150224.jar
neethi-3.0.3.jar
slf4j-api-1.7.9.jar
slf4j-jdk14-1.7.9.jar
spring-aop-4.1.1.RELEASE.jar
spring-beans-4.1.1.RELEASE.jar
spring-context-4.1.1.RELEASE.jar
spring-core-4.1.1.RELEASE.jar
spring-expression-4.1.1.RELEASE.jar
stax2-api-3.1.4.jar
woodstox-core-asl-4.4.1.jar
wsdl4j-1.6.3.jar
xml-resolver-1.2.jar
xmlschema-core-2.2.1.jar

The only significant conflict with WebLogic is from the WS Policy Neethi library and is fixed by adding the following application preference to weblogic.xml:

  <container-descriptor>
    <prefer-application-packages>
      <package-name>org.apache.neethi.*</package-name>
    </prefer-application-packages>
  </container-descriptor>

 

The Demo Service

The demo is a RESTful service that invokes a message start BPM process via the usual SOAP Web Service call based on the published WSDL for the process. The service will be deployed and run on WebLogic. The process can have any number of parameters which we will assume to be all of type string to keep things simple. It would be straightforward to handle arbitrary types since we introspect the generated proxy class but I’ll leave that as an exercise for the reader. The most common BPM process invoke is asynchronous with no callback, go do your work and don’t ever bother me about it. That is call mechanism implemented in the demo.

BPM Process

A sample BPM process is needed to test the REST service. A representative process will have a message Start with End type set to None since we won’t be listening for a callback.

Sample BPM Process

The defined interface with four sample string arguments would look like

Message Start Arguments

The published WSDL excerpt shows the start method with four arguments. The service accesses this WSDL via CXF to generate the proxy.

<wsdl:definitions ... targetNamespace="http://xmlns.oracle.com/bpmn/bpmnProcess/SampleProcess">
        <xsd:schema targetNamespace="http://xmlns.oracle.com/bpmn/bpmnProcess/SampleProcess">
            <xsd:element name="start">
                <xsd:complexType>
                    <xsd:sequence>
                        <xsd:element name="sampleArg1" type="xsd:string"/>
                        <xsd:element name="sampleArg2" type="xsd:string"/>
                        <xsd:element name="sampleArg3" type="xsd:string"/>
                        <xsd:element name="sampleArg4" type="xsd:string"/>
                    </xsd:sequence>
                </xsd:complexType>
            </xsd:element>
        </xsd:schema>
    </wsdl:types>
    <wsdl:message name="start">
        <wsdl:part name="parameters" element="tns:start"/>
    </wsdl:message>
    <wsdl:portType name="SampleProcessPortType">
        <wsdl:operation name="start">
            <wsdl:input message="tns:start"/>
        </wsdl:operation>
    </wsdl:portType>
    <wsdl:binding name="SampleProcessBinding" type="tns:SampleProcessPortType">
        <wsdlsoap:binding transport="http://schemas.xmlsoap.org/soap/http"/>
        <wsdl:operation name="start">
            <wsdlsoap:operation style="document" soapAction="start"/>
            <wsdl:input>
                <wsdlsoap:body use="literal"/>
            </wsdl:input>
        </wsdl:operation>
    </wsdl:binding>
    <wsdl:service name="SampleProcess.service">
        <wsdl:port name="SampleProcessPort" binding="tns:SampleProcessBinding">
            <wsdlsoap:address location="http://oramint:7101/soa-infra/services/default/BPMsample%211.0*soa_b0779884-0f0b-4ba5-afaf-0a3191ddb105/SampleProcess.service"/>
        </wsdl:port>
    </wsdl:service>
</wsdl:definitions>

RESTful Frontend

Start with a new class named InvokeBPM and service method named invokeBPM (feels a bit odd that we’re not doing a ‘State Transfer’ with the REST call but credit REST for being more than originally envisioned). Pass incoming info for the process call via query parameters.  We need host server and port, composite ID, process name and finally a comma separated list of arguments to the process.

public Response invokeBPM(@QueryParam("serverAndPort") String serverAndPort,
                          @QueryParam("compositeId") String compositeId,
                          @QueryParam("processName") String processName,
                          @QueryParam("argumentList") String argumentList)

Proxy

The CXF proxy generator will need the WSDL URL and the SOAP service reference in the form of a qualified name so we need to construct the service path string and XML target namespace as well as the ?WSDL URL.

BPM processes in the default partition have service URLs in the form

http://<server and port>/soa-infra/services/default/<composite ID>/<process name>.service

construct the service URL string using the incoming data

String serviceStr =
  "http://" + serverAndPort + "/soa-infra/services/default/" + compositeId + "/" + processName + ".service";

The WSDL URL just needs “?WSDL” appended to the service path string.

URL wsdlURL = new URL(serviceStr + "?WSDL");

The qualified name for the service is “.service” appended to the process name combined with the target namespace seen in the WSDL above.

QName qualServiceName =
     new QName("http://xmlns.oracle.com/bpmn/bpmnProcess/" + processName, processName + ".service");

Everything is set for the main event, using the CXF factory to build the proxy. The factory creates a class at runtime based on the WSDL read from the WSDL URL. The class generation is reported in the standard output log of the service with a message like “INFO: Created classes: com.oracle.xmlns.bpmn.bpmnprocess.sampleprocess.Start”

JaxWsDynamicClientFactory factory = JaxWsDynamicClientFactory.newInstance();
Client client = factory.createClient(wsdlURL.toExternalForm(), qualServiceName);
ClientImpl clientImpl = (ClientImpl) client;

(Note that CXF also has the JaxWsClientFactoryBean class which is similar to JaxWsDynamicClientFactory except it does not handle complex objects in the WSDL)

Making the Call

Once the proxy is built we need to instantiate an XML document object message, fill in the argument values and make the SOAP call via the proxy. This is a document literal SOAP call, the arguments are mapped to fields in the message object. The CXF factory does essentially the same thing clientgen does at dev time, it just does it at runtime. In this simple example the class has four string fields. In the general case, the message document could be arbitrarily complex.

To get the message part class use the following sequence, get the endpoint from the clientImpl, service info from the endpoint, SOAP binding from the service info, operation from the binding and finally message part from the operation.

First the endpoint

Endpoint endpoint = clientImpl.getEndpoint();

then the service info – there is only one in our case

ServiceInfo serviceInfo = endpoint.getService().getServiceInfos().get(0);

then the SOAP binding – also only one

BindingInfo binding = serviceInfo.getBindings().iterator().next();

then the operation – only one, the start operation, we need the qualified name

QName opName = binding.getOperations().iterator().next().getName();

then the input message

BindingOperationInfo boi = binding.getOperation(opName);
BindingMessageInfo inputMessageInfo = boi.getInput();

finally, the list of message parts

List<MessagePartInfo> parts = inputMessageInfo.getMessageParts();

get the class for the input message part – there is only one and it’s called “parameters”

Class<?> partClass = parts.get(0).getTypeClass();

Now we have the class that was created by the CXF factory, partClass. The declared fields of this class are the arguments we are looking for. Declare inputFields to identify the array of fields so we can get the field names later, we’ll also want to know the field count.

Field[] inputFields = partClass.getDeclaredFields();

make an input object based on the proxy class

Object inputObject = partClass.newInstance();

All that’s left to do now is write the field values. The values were passed in the comma separated string argumentList which will be easier to use split into an array.

List<String> argItems = Arrays.asList(argumentList.split("\\s*,\\s*"));

In a perfect world the arrays argItems and inputFields will have the same length. Maybe most of the time they will but to protect against index out of bounds error let’s use the minimum of the two.

int lastArgPosition = Math.min(inputFields.length, argItems.size());

We’ll need to instantiate a property descriptor for each field in order to use its getWriteMethod() to load the value from argItems, loop over fields.

PropertyDescriptor fieldPropDesc = null;
for (int i = 0; i < lastArgPosition; i++) {
   fieldPropDesc = new PropertyDescriptor(inputFields[i].getName(), partClass);
   fieldPropDesc.getWriteMethod().invoke(inputObject, argItems.get(i));
}

The input object is now populated, the proxy will use it to create the XML document for the SOAP call. As mentioned in the beginning, we want an asynchronous call which means we need to use the client invoke signature with a client callback argument. We don’t expect a callback so we won’t bother setting up the callback service.

client.invoke(new ClientCallback(), boi, inputObject);

Build, Run and Test

The CXF binary and source distributions are available at http://cxf.apache.org/download.html. Unless you are feeling adventurous and want to build the source just take the binaries and unpack anywhere you can access the /lib folder from JDeveloper. The steps for building the demo are;

  • create a custom type application with REST Web Service feature
  • create the RESTful service with InvokeBPM class
  • add the CXF library jars to the project classpath
  • add the code described above for InvokeBPM
  • add the prefer application override for Neethi to weblogic.xml
  • create the war deployment profile
  • finally generate the war

In JDeveloper, select “New Application …” and then select “Custom Application”

JDeveloper New Custom Application

For project features select “REST Web Services” (and not SOAP Web Services). “Java” will be auto selected. Name the project “CallBPMService”.

REST Project Feature

Enter a package path in the next dialog and finish the new application wizard.

Create the service class using the JDeveloper wizard which sets up the class with the appropriate REST annotations and adds WEB-INF/web.xml and WEB-INF/weblogic.xml to the project.

REST Service Menu

Name the class InvokeBPM and use http GET call semantics. Also select application/json and application/xml media types as a convention. As mentioned before we’re not doing any “State Transfer” so there is nothing produced by the service.

Create REST dialog

The wizard creates the .java file for the service which you can code as discussed or paste in the completed code available in the InvokdeBPM.java file listed at the end of the article.

Add the CXF jars listed in the beginning to the project classpath

CXF jars classpath

WEB-INF/weblogic.xml needs to be edited. It will have a library reference which should be deleted and the “prefer-application-packages” setting for Neethi added. The original looks like

weblogic.xml with library ref

remove the library-ref and add the following

weblogic.xml prefer application

Create a WAR file deployment profile

WAR deployment profile

give the desired context root

Context Root

select the CXF jars that were added to the project classpath to go in the WAR, the JAX-RS Jersey 2.x library is in WebLogic 12c so that doesn’t need to go in the WAR

CXF lib jars

set the default platform to Weblogic 12

WLS platform setting

That completes the WAR file deployment profile. Use it to generate a WAR file and deploy it on your WebLogic server.

After the service is deployed, make a REST call to it using an http URL with the query parameters. You can also use a tool that reads the WADL and helps build the URL and parameters. You should see audit history for the sample BPM process with payload data that originated from the REST call.

Using http analyzer in JDeveloper load the application WADL

REST call from http analyzer

Enter query parameters for server and port etc., here I’ve entered “ArgOne, ArgTwo, ArgThree, ArgFour” as the argument list parameter. Click “Send Request” button and the BPM process will be invoked with the four string arguments in the payload.

BPM completed processes

open the audit history to check the payload

process audit history

The payload shows the four string arguments from the csv string in the REST call

payload data

The full text of the payload looks like

<auditQueryPayload auditId="35" ciKey="10" xmlns="http://xmlns.oracle.com/bpmn/engine/audit">
   <serviceOutput>
      <element name="sampleArg4" isBusinessIndicator="false">
         <value>
            <![CDATA[<sampleArg4>ArgFour</sampleArg4>]]>
         </value>
      </element>
      <element name="sampleArg3" isBusinessIndicator="false">
         <value>
            <![CDATA[<sampleArg3>ArgThree</sampleArg3>]]>
         </value>
      </element>
      <element name="sampleArg2" isBusinessIndicator="false">
         <value>
            <![CDATA[<sampleArg2>ArgTwo</sampleArg2>]]>
         </value>
      </element>
      <element name="sampleArg1" isBusinessIndicator="false">
         <value>
            <![CDATA[<sampleArg1>ArgOne</sampleArg1>]]>
         </value>
      </element>
   </serviceOutput>
</auditQueryPayload>

Summary

The Apache CXF dynamic Web Service proxy client factory enables a convenient mechanism to invoke message start BPM processes. If you have BPM processes with the same message start payload you can statically generate the proxy client class and use JAX-WS API to set the binding at runtime and do not need CXF runtime class factory.

Things to Try Next

Some suggestions for things to experiment using CXF; create and use a callback service, create a callback service at runtime using CXF service builder, use complex types in the BPM message start interface.

Complete Java Source

package oracle.ateam;

import java.beans.IntrospectionException;
import java.beans.PropertyDescriptor;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;

import java.net.MalformedURLException;
import java.net.URL;

import java.util.Arrays;
import java.util.List;

import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Response;

import javax.xml.namespace.QName;

import org.apache.cxf.endpoint.Client;
import org.apache.cxf.endpoint.ClientCallback;
import org.apache.cxf.endpoint.ClientImpl;
import org.apache.cxf.endpoint.Endpoint;
import org.apache.cxf.jaxws.endpoint.dynamic.JaxWsDynamicClientFactory;
import org.apache.cxf.service.model.BindingInfo;
import org.apache.cxf.service.model.BindingMessageInfo;
import org.apache.cxf.service.model.BindingOperationInfo;
import org.apache.cxf.service.model.MessagePartInfo;
import org.apache.cxf.service.model.ServiceInfo;

@Path("bpm")
@Consumes(value = { "application/json", "application/xml" })
@Produces(value = { "application/json", "application/xml" })
public class InvokeBPM {

    @GET
    @Produces(value = { "application/json", "application/xml" })
    @Path("/invoke")
    public Response invokeBPM(@QueryParam("serverAndPort") String serverAndPort,
                              @QueryParam("compositeId") String compositeId,
                              @QueryParam("processName") String processName,
                              @QueryParam("argumentList") String argumentList) throws MalformedURLException,
                                                                                      InstantiationException,
                                                                                      IllegalAccessException,
                                                                                      IntrospectionException,
                                                                                      InvocationTargetException,
                                                                                      Exception {

        // build the service URL string with BPM server, composite and process info
        String serviceStr =
            "http://" + serverAndPort + "/soa-infra/services/default/" + compositeId + "/" + processName + ".service";

        URL wsdlURL = new URL(serviceStr + "?WSDL");

        // the target namespace should always be the same for BPM 12c and 11g, as well as the .service suffix
        QName qualServiceName =
            new QName("http://xmlns.oracle.com/bpmn/bpmnProcess/" + processName, processName + ".service");

        // run the CXF factory to create the proxy class
        JaxWsDynamicClientFactory factory = JaxWsDynamicClientFactory.newInstance();
        Client client = factory.createClient(wsdlURL.toExternalForm(), qualServiceName);
        ClientImpl clientImpl = (ClientImpl) client;

        // traverse Endpoint->ServiceInfo->SOAPBinding->operation->Input MessagePart to get the proxy class reference
        Endpoint endpoint = clientImpl.getEndpoint();
        ServiceInfo serviceInfo = endpoint.getService().getServiceInfos().get(0);
        BindingInfo binding = serviceInfo.getBindings().iterator().next();

        QName opName = binding.getOperations().iterator().next().getName();
        BindingOperationInfo boi = binding.getOperation(opName);
        BindingMessageInfo inputMessageInfo = boi.getInput();

        List<MessagePartInfo> parts = inputMessageInfo.getMessageParts();
        Class<?> partClass = parts.get(0).getTypeClass();

        // fields are all string for the demo but can be complex types in general
        Field[] inputFields = partClass.getDeclaredFields();

        // create a proxy object using the hard won class type
        Object inputObject = partClass.newInstance();

        // split the comma separted argument list string into a List
        List<String> argItems = Arrays.asList(argumentList.split("\\s*,\\s*"));

        // only write the minimum arguments if there is a mis-match, nominally will have same number of each
        int lastArgPosition = Math.min(inputFields.length, argItems.size());

        // write the field values
        PropertyDescriptor fieldPropDesc = null;
        for (int i = 0; i < lastArgPosition; i++) {
            fieldPropDesc = new PropertyDescriptor(inputFields[i].getName(), partClass);
            fieldPropDesc.getWriteMethod().invoke(inputObject, argItems.get(i));
        }

        // and make the call, ClientCallback makes it asynchronous but not expecting a callback so no service setup
        client.invoke(new ClientCallback(), boi, inputObject);

        return Response.ok().build();
    }
}

Add Your Comment