Tuesday, April 17, 2012

A WSDL-first asynchronous JAX-WS webservice. Correlating the messages using WS-Addressing and a callback interface

This how to is a WSDL-first (top-down) example of implementing a pure JAX-WS asynchronous web service with the following features:
  • the service, named JobProcessor is designed top-down, starting from WSDL and generating the interface and classes
  • the service support WS-Addressing for correlating the asynchronous request message with another request message targeted to callback endpoint
  • the service convention is document/literal
  • the service supports a single asynchronous operation: JobProcessor.processJob()
  • the desired message exchange pattern is simple:
    • a caller invokes the JobProcessor.processJob() operation in asynchronous manner
    • a <wsa:MessageID/>'s WS-Addressing header is supplied in the request
    • a callback endpoint is also supplied with the call using WS-Addressing headers, specifically <wsa:ReplyTo/> 
    • the service simulates some time-consuming processing (hence the need for asynchronous WS)
    • once the processing is finished, the service callbacks the caller issuing  correlation via WS-Addressing headers, that is using <wsa:RelatesTo/> 
  • the service also acts like a client when invokes the callback endpoint, so Messages, PortTypes, Bindings, etc.. are also needed

NOTE: the term caller denotes an external entity/user invoking the JobProcessor service, the term client denotes the role of the service when acts like a client

Hands on


I use JDeveloper on this how to, it can be easily done using another IDE.
  1. Create an empty generic application and project, named them JobProcessorApp and JobProcessorProj respectively, add only Java and Web Service technologies. Choose a default package name like dev.home.examples.jobprocessor 
  2. Create an new folder, named it wsdl. 
  3. Create a new XML Schema archive into the wsdl folder, name it jobprocessor.xsd and use a custom namespace like http://examples.home.dev/jobprocessor/types and a significant prefix like jpt 
  4. Create a new WSDL Document into the wsdl folder, named it jobprocessor.wsdl, use a custom namespace like: http://examples.home.dev/jobprocessor 
  5. Edit the WSDL an replace the default namespace prefix from tns to a significant one like jp, and declares the XML Schema namespace using the jpt prefix: xmlns:jpt="http://examples.home.dev/jp/types" 
  6. Edit the WSDL an add the namespace WS-Addressing for WSDL: xmlns:wsaw="http://www.w3.org/2006/05/addressing/wsdl" 
  7. Include the XSD archive into the schema section of WSDL: definitions/types/xsd:schema/xsd:include, at this point the WSDL should look like:
  8. <?xml version="1.0" encoding="UTF-8" ?>
    <definitions targetNamespace="http://examples.home.dev/jobprocessor"
                 xmlns:jpt="http://examples.home.dev/jobprocessor/types"
                 xmlns:jp="http://examples.home.dev/jobprocessor"
                 xmlns="http://schemas.xmlsoap.org/wsdl/"
                 xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
                 xmlns:xsd="http://www.w3.org/2001/XMLSchema"
                 xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
                 xmlns:mime="http://schemas.xmlsoap.org/wsdl/mime/"
                 xmlns:soap12="http://schemas.xmlsoap.org/wsdl/soap12/"
                 xmlns:wsaw="http://www.w3.org/2006/05/addressing/wsdl">
      <types>
        <xsd:schema targetNamespace="http://examples.home.dev/jobprocessor/types"
                    elementFormDefault="qualified">
          <xsd:include schemaLocation="jobprocessor.xsd"/>
        </xsd:schema>
      </types>
    </definitions>
    
    
    
  9. Define schema types and elements for message payloads: Job, JobType, JobReply and JobReplyType, the final XSD should look like:
  10. <?xml version="1.0" encoding="UTF-8" ?>
    <xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
                xmlns:jpt="http://examples.home.dev/jobprocessor/types"
                targetNamespace="http://examples.home.dev/jobprocessor/types"
                elementFormDefault="qualified">
      <xsd:element name="Job" type="jpt:JobType"/>
      <xsd:complexType name="JobType">
        <xsd:sequence>
          <xsd:element name="jobId" type="xsd:string"/>
          <xsd:element name="payload" type="xsd:string"/>
        </xsd:sequence>
      </xsd:complexType>
      <xsd:element name="JobReply" type="jpt:JobReplyType"/>
      <xsd:complexType name="JobReplyType">
        <xsd:sequence>
          <xsd:element name="jobId" type="xsd:string"/>
          <xsd:element name="result" type="xsd:string"/>
        </xsd:sequence>
      </xsd:complexType>
    </xsd:schema>
  11. Define WSDL messages: Job and JobReply either with their corresponding element-based parts job and jobReply 
  12. Define WSDL port types: JobProcessor for the service interface and JobProcessorNotify for callback interface with their corresponding operations procesJob() and replyFinishedJob(), also specify the corresponding WS-Addressing actions for each input message, at this point the WSDL looks like:
  13. <?xml version="1.0" encoding="UTF-8" ?>
    <definitions targetNamespace="http://examples.home.dev/jobprocessor"
                 xmlns:jpt="http://examples.home.dev/jobprocessor/types"
                 xmlns:jp="http://examples.home.dev/jobprocessor"
                 xmlns="http://schemas.xmlsoap.org/wsdl/"
                 xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
                 xmlns:xsd="http://www.w3.org/2001/XMLSchema"
                 xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
                 xmlns:mime="http://schemas.xmlsoap.org/wsdl/mime/"
                 xmlns:soap12="http://schemas.xmlsoap.org/wsdl/soap12/"
                 xmlns:wsaw="http://www.w3.org/2006/05/addressing/wsdl">
      <types>
        <xsd:schema targetNamespace="http://examples.home.dev/jobprocessor/types"
                    elementFormDefault="qualified">
          <xsd:include schemaLocation="jobprocessor.xsd"/>
        </xsd:schema>
      </types>
      <message name="Job">
        <part name="job" element="jpt:Job"/>
      </message>
      <message name="JobReply">
        <part name="jobReply" element="jpt:JobReply"/>
      </message>
      <portType name="JobProcessor">
        <operation name="processJob">
          <input message="jp:Job" wsaw:Action="http://examples.home.dev/jobprocessor/processJob"/>
        </operation>
      </portType>
      <portType name="JobProcessorNotify">
        <operation name="replyFinishedJob">
          <input message="jp:JobReply" wsaw:Action="http://examples.home.dev/jobprocessor/replyFinishedJob"/>
        </operation>
      </portType>
    </definitions>
    
  14. Define document-literal bindings with HTTP transport for each port type JobProcessor and JobProcessorNotify, enforce WS-Addressing using <wsaw:UsingAddressing required="true"/>
  15. Define the service JobProcessor and the callback service JobProcessorNotify, the final WSDL should look like:
  16. <?xml version="1.0" encoding="UTF-8" ?>
    <definitions targetNamespace="http://examples.home.dev/jobprocessor"
                 xmlns:jpt="http://examples.home.dev/jobprocessor/types"
                 xmlns:jp="http://examples.home.dev/jobprocessor"
                 xmlns="http://schemas.xmlsoap.org/wsdl/"
                 xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
                 xmlns:xsd="http://www.w3.org/2001/XMLSchema"
                 xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
                 xmlns:mime="http://schemas.xmlsoap.org/wsdl/mime/"
                 xmlns:soap12="http://schemas.xmlsoap.org/wsdl/soap12/"
                 xmlns:wsaw="http://www.w3.org/2006/05/addressing/wsdl">
      <types>
        <xsd:schema targetNamespace="http://examples.home.dev/jobprocessor/types"
                    elementFormDefault="qualified">
          <xsd:include schemaLocation="jobprocessor.xsd"/>
        </xsd:schema>
      </types>
      <message name="Job">
        <part name="job" element="jpt:Job"/>
      </message>
      <message name="JobReply">
        <part name="jobReply" element="jpt:JobReply"/>
      </message>
      <portType name="JobProcessor">
        <operation name="processJob">
          <input message="jp:Job" wsaw:Action="http://examples.home.dev/jobprocessor/processJob"/>
        </operation>
      </portType>
      <portType name="JobProcessorNotify">
        <operation name="replyFinishedJob">
          <input message="jp:JobReply" wsaw:Action="http://examples.home.dev/jobprocessor/replyFinishedJob"/>
        </operation>
      </portType>
      <binding name="JobProcessor" type="jp:JobProcessor">
        <wsaw:UsingAddressing wsdl:required="true"/>
        <soap:binding style="document"
                      transport="http://schemas.xmlsoap.org/soap/http"/>
        <operation name="processJob">
          <soap:operation style="document"
                          soapAction="http://examples.home.dev/jobprocessor/processJob"/>
          <input>
            <soap:body use="literal" parts="job"/>
          </input>
        </operation>
      </binding>
      <binding name="JobProcessorNotify" type="jp:JobProcessorNotify">
        <wsaw:UsingAddressing required="true"/>
        <soap:binding style="document"
                      transport="http://schemas.xmlsoap.org/soap/http"/>
        <operation name="replyFinishedJob">
          <soap:operation style="document"
                          soapAction="http://examples.home.dev/jobprocessor/replyFinishedJob"/>
          <input>
            <soap:body use="literal" parts="jobReply"/>
          </input>
        </operation>
      </binding>
      <service name="JobProcessor">
        <port name="jobProcessor" binding="jp:JobProcessor">
          <soap:address location="http://localhost/JobProcessor"/>
        </port>
      </service>
      <service name="JobProcessorNotify">
        <port name="jobProcessorNotify" binding="jp:JobProcessorNotify">
          <soap:address location="http://localhost/JobProcessorNotify"/>
        </port>
      </service>
    </definitions>
    
  17. Once finished the WSDL design, start coding. Go to File > New > Java Web Service from WSDL 
  18. Choose Java EE 1.5 with support for JAX-WS RI 

  19. Choose the jobprocessor.wsdl  archive, ensure that the interface is also generated by marking Add Service Endpoint Interface, also uncheck copy WSDL locally:

  20. Choose JobProcessor service
  21. Enter dev.home.examples.jobprocessor.ws for the Package Name and dev.home.examples.jobprocessor.types for Root Package for Generated types (SDO types). 

  22. Choose jobProcessor port then click Finish
  23. You will end with several classes and interfaces generated from the WSDL. But we won't implement the JobProcessorNotify service in Java, so you can delete the JobProcessorNotify.java and JobProcessorNotifyImpl.java   
  24. Annotate the JobProcessorImpl class to enforce Addressing with javax.xml.ws.soap.Addressing:
  25. @Addressing(required = true)
    public class JobProcessorImpl {
    ...
    }
    
  26. It's necessary to create a Java-based WS client to invoke the caller via the  JobProcessorNotify  PortType. Unfortunately JDeveloper 11.1.1.5.0 cannot do that in pure JAX-WS RI manner. Instead I use JDK's wsimport tool in this way, open a terminal/command prompt and type:  
  27. cd [PROJECT HOME DIRECTORY]
    wsimport -p dev.home.examples.jobprocessor.client -d classes/ -s src/ \
      -verbose wsdl/jobprocessor.wsdl 
    
  28. Then delete the unnecessary class dev.home.examples.jobprocessor.client.JobProcessor_Service and the interface dev.home.examples.jobprocessor.client.JobProcessor since we won't call ourselves
  29. At this point you will notice that the SDO types JobType and JobReplyType were generated twice, the first time in the dev.home.examples.jobprocessor.types package and the second one on dev.home.examples.jobprocessor.client package,  if you compare them you'll notice they are almost identical except for the package name. So, delete dev.home.examples.jobprocessor.client.JobType and dev.home.examples.jobprocessor.client.JobReply leaving the SDO only on the  dev.home.examples.jobprocessor.types package.
  30. Now the magic behind, the correlation is very simple: 1) grab request.MessageID and request.ReplyTo headers from the request message, and 2) set on the reply (a new request) message the reply.RelatesTo = MessageID and reply.To = ReplyTo. To achieve this I create a helper class named CorrelationHelper, it takes a javax.xml.ws.Service subtype as a template parameter, and pass a service of this type as constructor argument, a WebServiceContext is also needed:
  31. /**
     * Performs correlation between incoming one-way message and an outgoing 
     * one-way message by mapping the header wsa:To with wsa:ReplyTo and 
     * wsa:RelatesTo with wsa:MessageID
     *
     * @param <S> the service type
     */
    public final class CorrelationHelper<S extends Service> {
    
        private WebServiceContext wsc;
        private S service;
    
        public CorrelationHelper(S service, WebServiceContext wsc) {
            this.service = service;
            this.wsc = wsc;
        }
    
        /**
         * Retrieves the headers
         * @return
         */
        private HeaderList getHeaders() {
            return (HeaderList)wsc.getMessageContext().get(JAXWSProperties.INBOUND_HEADER_LIST_PROPERTY);
        }
    
        /**
         * Creates the correlated port, by appending a WS-Addressing relatesTo
         * header and assignind the MessageID
         * @param <P> the port type, castable to WSBindingProvider
         * @return the correlated port
         */
        public <P> P getCorrelatedPort(Class<P> portType) {
            P port = service.getPort(getReplyTo(), portType);
            ((WSBindingProvider)port).setOutboundHeaders(Headers.create(AddressingVersion.W3C.relatesToTag,
                                                                        getMessageId()));
            return port;
        }
    
        /**
         * Grab WS-Addressing ReplyTo/Address header
         * @return
         */
        private EndpointReference getReplyTo() {
            return getHeaders().getReplyTo(AddressingVersion.W3C,
                                           SOAPVersion.SOAP_11).toSpec();
        }
    
        /**
         * Grab WS-Addressing MessageID header
         * @return
         */
        private String getMessageId() {
            return getHeaders().getMessageID(AddressingVersion.W3C,
                                             SOAPVersion.SOAP_11);
        }
    }
    
  32. The processJob() implementation is trivial:
  33. @WebService(serviceName = "JobProcessor",
                targetNamespace = "http://examples.home.dev/jobprocessor",
                portName = "jobProcessor",
                endpointInterface = "dev.home.examples.jobprocessor.ws.JobProcessor")
    @HandlerChain(file = "JobProcessor-HandlerChain.xml")
    @Addressing(required = true)
    public class JobProcessorImpl {
    
        @Resource
        private WebServiceContext wsc;
    
        private CorrelationHelper<JobProcessorNotify_Service> correlationHelper;
        private Random random;
    
        public void processJob(JobType job) {
            
            // do processing
            int seconds = doJob();
    
            // prepare reply message
            JobReplyType jobReply = new JobReplyType();
            jobReply.setJobId(job.getJobId());
            jobReply.setResult(String.format("Job payload %s processed in %d seconds!",
                                             job.getPayload(), seconds));
            
            // do correlation and perform the callback
            JobProcessorNotify jobProcessorNotify =
                correlationHelper.getCorrelatedPort(JobProcessorNotify.class);
            jobProcessorNotify.replyFinishedJob(jobReply);
        }
    
        /**
         * Sleeps random time between 5 and 10 seconds to simulate processing
         * @return
         */
        private int doJob() {
            int seconds = random.nextInt(6) + 5;
            try {
                Thread.currentThread().sleep(1000 * seconds);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return seconds;
        }
    
        @PostConstruct
        public void doPostConstruct() {
            correlationHelper =
                    new CorrelationHelper<JobProcessorNotify_Service>(new JobProcessorNotify_Service(),
                                                                      wsc);
            random = new Random(System.nanoTime());
        }
    }
    
  34. As you noticed, all the magic occurs in the getCorrelatedPort() template method, which takes a generic parameter Class<P> and supplies the wsa:ReplyTo header as the endpoint argument in the javax.xml.ws.Service.getPort() invocation, then relates to the outgoing message with the incoming one via wsa:RelatesTo = wsaMessageID   
  35. I also changed some initialization routines in the generated code, specially in JobProcessorNotify_Service to able to load the WSDL from the classpath, notice I changed the wsdlLocation's @WebServiceClient parameter and url assigment statement:
  36. @WebServiceClient(name = "JobProcessorNotify",
                      targetNamespace = "http://examples.home.dev/jobprocessor",
                      wsdlLocation = "classpath:wsdl/jobprocessor.wsdl")
    public class JobProcessorNotify_Service extends Service {
    
        private final static URL JOBPROCESSORNOTIFY_WSDL_LOCATION;
        private final static WebServiceException JOBPROCESSORNOTIFY_EXCEPTION;
        private final static QName JOBPROCESSORNOTIFY_QNAME =
            new QName("http://examples.home.dev/jobprocessor",
                      "JobProcessorNotify");
    
        static {
            URL url =
                ClassLoader.getSystemClassLoader().getResource("classpath:wsdl/jobprocessor.wsdl");
            WebServiceException e = null;
            JOBPROCESSORNOTIFY_WSDL_LOCATION = url;
            JOBPROCESSORNOTIFY_EXCEPTION = e;
        }
    ...
    }
    
  37. Finally notice that the generic class CorrelationHelper<S extends Service> can be used in any kind of JAX-WS web service (hence the generic) that engages this message exchange pattern.


What's next?

The testing of course:
  1. Use soapUI to make your life easier.
  2. Deploy the WS to a testing / staging weblogic/tomcat/... server, once deployed you will end up with an arbitrary endpoint address, e.g.: http://HOST/JobProcessor
  3. Create a new soapUI project by supplying the WSDL address, it should be http://HOST/JobProcessor?WSDL or simply use  the system path's WSDL
  4. Check "Create a Web Service Simulation from the imported WSDL" and uncheck everything else:
  5. Generate the Mock service ONLY for replyFinishedJob operation, check Starts MockService immediately. 
  6. Name the mock JobProcessorNotify MockService, your mock will end up listening on the address http://YOURPC:8080/mockJobProcessNotify
  7. Go to JobProcessor / processJob,  create a new request and fill it:
  8. <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" 
      xmlns:typ="http://examples.home.dev/jobprocessor/types">
       <soapenv:Header/>
       <soapenv:Body>
          <typ:Job>
             <typ:jobId>1</typ:jobId>
             <typ:payload>Euardo Lago Aguilar</typ:payload>
          </typ:Job>
       </soapenv:Body>
    </soapenv:Envelope>
    
  9. In the request window's bottom, open WS-Addressing related settings, check Enable/Disable WS-A Addressing
  10. The Action should be already filled up with: http://examples.home.dev/jobprocessor/processJob
  11. Set To equal to: http://HOST/JobProcessor and also set the endpoint address (above) to the same value
  12. Set ReplyTo equal to your mock listening address:  http://YOURPC:8088/mockJobProcessorNotify
  13. Check Randomly generate MessageId, at the end the request should look like:
  14. Send the request and you should receive a reply message within 5 and 10 seconds later, see the mock log in the picture below:


  15. open the log entry by double clicking, the content should be:
    <S:Envelope xmlns:S="http://schemas.xmlsoap.org/soap/envelope/">
       <S:Header>
          <wsa:RelatesTo xmlns:wsa="http://www.w3.org/2005/08/addressing">uuid:732cf156-6de4-4701-9ae8-1aaa1c7a3bd9</wsa:RelatesTo>
          <wsa:To xmlns:wsa="http://www.w3.org/2005/08/addressing">http://YOURPC:8088/mockJobProcessorNotify</wsa:To>
          <wsa:Action xmlns:wsa="http://www.w3.org/2005/08/addressing">http://examples.home.dev/jobprocessor/replyFinishedJob</wsa:Action>
          <work:WorkContext xmlns:work="http://oracle.com/weblogic/soap/workarea/">rO0ABXoAA...AAAA=</work:WorkContext>
       </S:Header>
       <S:Body>
          <JobReply xmlns="http://examples.home.dev/jobprocessor/types">
             <jobId>1</jobId>
             <result>Job payload Euardo Lago Aguilar processed in 8 seconds!</result>
          </JobReply>
       </S:Body>
    </S:Envelope>
    
That's all folks! Thanks for be patient! Get the code here: JobProcessorApp.tar.gz

7 comments:

  1. Hello Eduardo, great post. I have a question, and i think maybe you can help me.:

    We are building an asynchronous web service for a bpel process, and bpel requires roles to be defined in the webservice. Do you know how to define different roles in the WSDL/XSD?

    Thanks a lot.

    ReplyDelete
  2. Hi Eduardo,
    I tried to follow your instructions on JDev 11.1.1.7, and the WSDL getting generated seemd to have only the service for the JobProcessor, and I'm not able to create the mockservice for the callback operation. Please help.

    Thanks,
    Debojit

    ReplyDelete
    Replies
    1. As you may noticed I didn't create a mock service in JDev. I did it using the SoapUI tool, it's more easy and sophisticated for testing purposes.
      Nevertheless you should be able to create it easily using the same steps, if such thing fails, then consider to split the original WSDL in two pieces: one for the service, and one for the callback service. Of course don't forget to reuse the same XSD in both.
      Thanks for your feedback!

      Delete
  3. Hi, Eduardo,

    Thanks for your great post. I've followed this demo step by step. Now, I've got a NullPointerException when the CorrelationHelper tries to get the Soap HeaderList. After debugging, I've found that the return of WebServiceContext.getMessageContext.get(JAXWSPropertities.INBOUND ...) is Null. That seems to say the incoming Soap Header is empty. But according to the log information of CXF, I can see the SOAP Header information that I put it by SoapUI client, e.g. Action, ReplyTo, etc. Really don't understand why it is null in the code, could you help me out ? many thanks ...

    ReplyDelete
    Replies
    1. This comment has been removed by the author.

      Delete
    2. Hi Eduardo and Shawn,

      I've exactly same problem (and also see all soapenv:Header elements as wsa:ReplyTo, wsa:MessageID and wsa:To in message payload), did yo resolve it anyhow?

      Thank you.

      Delete