- 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.
- 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
- Create an new folder, named it wsdl.
- 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
- Create a new WSDL Document into the wsdl folder, named it jobprocessor.wsdl, use a custom namespace like: http://examples.home.dev/jobprocessor
- 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"
- Edit the WSDL an add the namespace WS-Addressing for WSDL: xmlns:wsaw="http://www.w3.org/2006/05/addressing/wsdl"
- Include the XSD archive into the schema section of WSDL: definitions/types/xsd:schema/xsd:include, at this point the WSDL should look like:
- Define schema types and elements for message payloads: Job, JobType, JobReply and JobReplyType, the final XSD should look like:
- Define WSDL messages: Job and JobReply either with their corresponding element-based parts job and jobReply
- 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:
- Define document-literal bindings with HTTP transport for each port type JobProcessor and JobProcessorNotify, enforce WS-Addressing using <wsaw:UsingAddressing required="true"/>
- Define the service JobProcessor and the callback service JobProcessorNotify, the final WSDL should look like:
- Once finished the WSDL design, start coding. Go to File > New > Java Web Service from WSDL
- Choose Java EE 1.5 with support for JAX-WS RI
- Choose the jobprocessor.wsdl archive, ensure that the interface is also generated by marking Add Service Endpoint Interface, also uncheck copy WSDL locally:
- Choose JobProcessor service
- Enter dev.home.examples.jobprocessor.ws for the Package Name and dev.home.examples.jobprocessor.types for Root Package for Generated types (SDO types).
- Choose jobProcessor port then click Finish
- 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
- Annotate the JobProcessorImpl class to enforce Addressing with javax.xml.ws.soap.Addressing:
- 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:
- 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
- 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.
- 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:
- The processJob() implementation is trivial:
- 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
- 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:
- 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.
<?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>
<?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>
<?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>
<?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>
@Addressing(required = true) public class JobProcessorImpl { ... }
cd [PROJECT HOME DIRECTORY] wsimport -p dev.home.examples.jobprocessor.client -d classes/ -s src/ \ -verbose wsdl/jobprocessor.wsdl
/** * 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); } }
@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()); } }
@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; } ... }
What's next?
The testing of course:- Use soapUI to make your life easier.
- 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
- 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
- Check "Create a Web Service Simulation from the imported WSDL" and uncheck everything else:
- Generate the Mock service ONLY for replyFinishedJob operation, check Starts MockService immediately.
- Name the mock JobProcessorNotify MockService, your mock will end up listening on the address http://YOURPC:8080/mockJobProcessNotify
- Go to JobProcessor / processJob, create a new request and fill it:
- In the request window's bottom, open WS-Addressing related settings, check Enable/Disable WS-A Addressing
- The Action should be already filled up with: http://examples.home.dev/jobprocessor/processJob
- Set To equal to: http://HOST/JobProcessor and also set the endpoint address (above) to the same value
- Set ReplyTo equal to your mock listening address: http://YOURPC:8088/mockJobProcessorNotify
- Check Randomly generate MessageId, at the end the request should look like:
- Send the request and you should receive a reply message within 5 and 10 seconds later, see the mock log in the picture below:
<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>
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>
Hello Eduardo, great post. I have a question, and i think maybe you can help me.:
ReplyDeleteWe 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.
Hi Eduardo,
ReplyDeleteI 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
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.
DeleteNevertheless 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!
Hi, Eduardo,
ReplyDeleteThanks 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 ...
This comment has been removed by the author.
DeleteHi Eduardo and Shawn,
DeleteI'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.