Creating Callback Enabled Clients for Asynchronous Web Services

by Reza Shafii
03/14/2005

Abstract

Web services are a promising technology that play an important role in Service Oriented Architectures (SOA). A key, emerging aspect of this technology is the ability to provide asynchronous services. Although today’s web service standards contain specifications for providing such asynchronous services, the details on using them from a client application perspective can be confusing and unclear. Web services callbacks are an important element in the realization of these asynchronous services. This article provides a practical guide to the creation of client applications for web services that contain callback operations. All code segments shown in this article are from the included example, which you can download. The example contains the complete implementation of these code segments and usage guidelines.

Terminology

Before starting a discussion on callback-enabled clients, it is important that we clarify the terminology involved. The following diagram shows the main entities involved when a client uses a Web service with a callback operation.

Figure 1
Figure 1. A client making a call to a web service

This figure depicts a client making a call to a web service. The web service can then respond, at some later time, by making a call back to the client. Therefore, a client of a web service that contains a callback operation is special in that the client itself must provide an endpoint. We call this the callback endpoint and define an endpoint as a unique address identified by a URI to which SOAP request messages can be sent.

The client itself starts the interaction with the web service by sending a SOAP message to the web service endpoint. The correlated request messages sent by the client to the web service and their associated response messages form the client initiated operations. As mentioned, the client can process the request messages sent by the web service to its own callback endpoint. Together, the correlated request and response messages are referred to as a callback operation.

With this terminology in mind, let's take a closer look at the concept of web service callbacks and their relationship to conversational web services and asynchronous web service invocation.

Asynchronous vs. Callbacks vs. Conversational

The concept of an asynchronous web service invocation is sometimes confused with the idea of web service callbacks and conversational web services. Although related, the three concepts are different.

An asynchronous web service invocation means that the client is able to send a SOAP message request without blocking to receive the corresponding response message from the server. The process of making such asynchronous web service calls on BEA WebLogic Platform 8.1 web services is already comprehensively documented and is therefore not discussed further in this article.

Web service callbacks refer to scenarios that involve the web service provider sending a SOAP message back to its clients. The Web Services Description Language (WSDL) specifications define such operations as being of the type “solicit/response.” Clients of web services that support callback operations must have a web service endpoint themselves, which the web service can use for sending callback requests at any point in time, or, in other words, asynchronously. Note that this is unrelated to the concept of asynchronous invocation discussed earlier.

A web service is conversational if the series of messages going back and forth between the two endpoints are tracked with a unique conversation ID with specific operations flagged to start and end the flow of messages. Web services that provide callbacks are also, by definition, conversational. This is because in order for the web service to be able to make a callback call on a client, it has to have its callback endpoint information. This is only possible after an initial invocation has been made by the client. Therefore, at the very least, the initial operation initiated by the client and the callback operation made by the web service are related to each other and must be tracked with a unique conversation ID. If this is not done, there is no way for the client to differentiate between callback operations relating to different initiating calls.

We will now take these considerations into account and create callback-enabled clients. As we've just seen, these clients form the basis of solicit-response and conversational web services.

Creating Callback-Enabled Clients

As discussed earlier, callback-enabled web service clients need to provide a callback endpoint capable of asynchronously receiving and processing callback operation messages. To avoid the complications of having to provide a callback endpoint, a technique known as polling can be used as an alternative. However, this technique requires the client to periodically call the server to check for callback events. The overhead of these calls can be significant if the event in question does not occur frequently. This overhead can be avoided by having the client provide a callback endpoint and process the callback operations directly.

It should also be noted that although it is possible to create callback-enabled clients by using a web service over JMS (if such binding is offered), this approach has some limitations, one of the important ones being that it forces the client to use the same JMS implementation as the web service provider. We will therefore concentrate on accomplishing our task over the HTTP transport. There are two areas that require attention in creating such a client: the creation of proper client-initiated SOAP messages, and the handling of callback operations.

When our client initiates operations of a web service containing a callback, it needs to somehow include the URI of the callback endpoint on which it is listening within the request message. The Web Services Addressing and the SOAP Conversation Protocol specifications both define a SOAP header element that allows you to achieve this goal. Ideally, the specification used in specifying the callback endpoint would not matter. However, most web services containers, including BEA WebLogic Server 8.1, do not yet contain an implementation of the Web Services Addressing specifications. Currently, BEA WebLogic Workshop 8.1 web services support the SOAP Conversation Protocol specifications and that is the example used within our example client.

According to the SOAP Conversation Protocol, the required SOAP headers differ depending on the phase of the conversation. For the first client-initiated (start) operation within the conversation, we need to provide the web service with our callback endpoint through the callbackLocation header element. All operations, including the first one, will also require a unique ID that will be used in our SOAP messages throughout the dialogue. This is achieved through the conversationID element. The following is an example of a SOAP request message that starts a callback-enabled conversation:

<soapenv:Envelope soapenv:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" 

    xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" 

    xmlns:xsd="http://www.w3.org/2001/XMLSchema" 

    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 

    xmlns:enc="http://schemas.xmlsoap.org/soap/encoding/" 

    xmlns:env="http://schemas.xmlsoap.org/soap/envelop/" 

    xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/">

  <soapenv:Header>

    <con:StartHeader soapenv:actor="http://schemas.xmlsoap.org/soap/actor/next" 

     soapenv:mustUnderstand="0" 

     xmlns:con="http://www.openuri.org/2002/04/soap/conversation/">

     
                        
 <con:conversationID>[123456]:192.168.1.100:8181</con:conversationID>

         <con:callbackLocation>

          http://192.168.1.100:8181/StockNotificationCallback

         </con:callbackLocation>

      </con:StartHeader>

 </soapenv:Header>

  <soapenv:Body>

    <n1:registerForThresholdNotif xmlns:n1="http://www.openuri.org/">

      <n1:stockTicker>CCC</n1:stockTicker>

      <n1:threshold>10</n1:threshold>

    </n1:registerForThresholdNotif>

  </soapenv:Body>

</soapenv:Envelope>

                      

Most of the existing Java web service toolkits (for example, BEA WebLogic’s clientgen, Apache Axis, and JWSDP) allow you to create a proxy library that can be used by client applications to easily invoke web services operations. However, none of these frameworks supports callback operations with the main issue being that their proxies do not generate the required headers. Until such support is provided, it would be highly desirable to be able to use all the benefits of these frameworks (for example, complex type XML mapping) by extending them with support for callback operations. One technique for achieving this result is the use of a SOAP message handler.

All the web services toolkits named above can support SOAP message handlers. A message handler is a Java class that implements the javax.xml.rpc.handler.GenericHandler interface and contains methods called right before sending out (or after receiving) a SOAP message. We can introduce a message handler within our client that is able to find out the current phase of a particular conversation and accordingly extend the outgoing request message with the required headers.

It is important to note that the client SOAP message handler must be able to determine the phase of the conversation that the message in question belongs to so that the appropriate header elements are created. The generation of the conversation ID is also a task that is left up to the client handler.

Once the extended request message is received by the web service endpoint, it will then send request messages to the callback endpoint specified by the callbackLocation element of the start message. In most cases, the client process itself needs to provide this endpoint and handle the callback messages appropriately. If our client is running within a web service container, this could be accomplished by deploying a web service with the callback operation within the same container. However, if the client is not running within a container, the job can be accomplished by implementing the callback endpoint within a lightweight container embedded in the client process itself. This gives us the ability to invoke the client generated operations and handle the incoming callback operations within the same process context. Note that the processing entity behind the callback endpoint (and within the client) needs to be able to handle the XML mapping of the message parameters as well as the dispatching of the operation to the right area of code.

Off course the client could also choose to specify a callback endpoint living within a container separate from the calling process by simply setting the callbackLocation header element appropriately.

As we have seen, the creation of clients for web services containing callback operations is not trivial and contains complex elements including the handling of SOAP messages, management of the conversation (both in terms of phases and IDs), mapping of parameters, and dispatching of operations. These complexities are all abstracted when the web service in question is accessed through a BEA WebLogic Workshop Service Control, which automatically performs all these operations for the user.

Creating Callback-Enabled Client Using a Service Control

The accessing of web services through a WebLogic Workshop Service Control is already well-documented. This solution is by far the simplest way to use callback-enabled web services if the client entity is able to use controls (that is, it is a Java Process Definition business process or another control; Page Flows are not able to use control callbacks). All the complexities involved in accessing callback-enabled web services are abstracted by this method.

Example Stock Notification Service

The example included in this article contains a Stock Notification conversational web service and a sample client illustrating the concepts discussed here. The web service provides operations allowing clients to register stock tickers and an associated threshold price. The service then monitors the stock, and every time the price passes that threshold, notifies the client through a callback ( onThresholdPassed()).

Figure 2
Figure 2. This diagram illustrates the configuration for our example

It is clear that a polling technique is not an optimal solution for the creation of our client: Stock prices could pass their associated thresholds on a frequent basis or very seldom. A short polling interval could therefore lead to many unnecessary calls whereas a long polling interval could result in the client being notified of a threshold crossing long after the fact.

The implementation of a callback endpoint within the client application seems like a better choice. Let's first look at how the example client sends operations to the StockNotification web service. We have implemented the client using two distinct techniques. The first technique uses the WebLogic Workshop-generated proxy library and is implemented by the makeCallUsingBEAProxy() method of the StockNotificationClient class:

public void makeCallUsingBEAProxy() {

    try {

      // Get the proxy instance so that we can start calling the web service operations

      StockNotificationSoap sns = null;

      StockNotification_Impl snsi = new StockNotification_Impl();

      sns = snsi.getStockNotificationSoap();



      // Register our conversational handler so that it can properly set our

      // our conversation headers with a callback location

      QName portName = new QName("http://www.openuri.org/",

                                 "StockNotificationSoap");

      HandlerRegistry registry = snsi.getHandlerRegistry();

      List handlerList = new ArrayList();

      handlerList.add(new HandlerInfo(
                        
ClientConversationHandler.class, null, null));

      registry.setHandlerChain(portName, handlerList);



      // Register, call some methods, then sleep a bit so that our callback

      // location can receive some notifications and finally finish the conversation.

      sns.registerForThresholdNotif("AAAAA", "100");

      sns.addToRegistration("BBBBB", "5");

      StockList stocks = sns.getAllRegisteredStocks();



      ...



      sns.endAllNotifications();

    }

    catch (Exception e) {

      throw new RuntimeException(e);

    }

}

                      

As you can see in this code segment, we are able to use the classes generated by clientgen as we would normally. The only thing that stands out in this code as compared to the invocation of a web service without callbacks is the instantiation and registration of a client-side SOAP message handler at the beginning of the method. This handler intercepts the outgoing SOAP messages and adds the necessary conversational headers. Every time a SOAP request message is sent from the client process the handleRequest() method of our message handler is called with a MessageContext object containing information about the outgoing SOAP request. Below is a code segment of our example client’s message handler:

package weblogic.webservice.core.handler;



...



public class ClientConversationHandler

    extends GenericHandler {



public boolean handleRequest(MessageContext ctx) {  

  ...

  if (phase.equals("START")) {

    headerElement

              = (SOAPHeaderElement) header

              .addChildElement("StartHeader",

                               "con",

                               "http://www.openuri.org/2002/04/soap/conversation/");



    headerElement.addChildElement("con:callbackLocation")

              .addTextNode(CALLBACK_URI);

  }

  else if (phase.equals("CONTINUE") || phase.equals("FINISH")) {

    headerElement

              = (SOAPHeaderElement) header

              .addChildElement("ContinueHeader",

                               "con",

                               "http://www.openuri.org/2002/04/soap/conversation/");

  }



  headerElement.addChildElement("con:conversationID").addTextNode(convID);

  ...

}

}

The proxy library generated by the BEA clientgen tool already uses a default client-side SOAP message handler that creates the conversational StartHeader header element. Unfortunately, currently clientgen output does not support callback operations and therefore the default handler does not create the required callbackLocation sub-element. We therefore have to ensure that our own message handler overrides the default conversational handler so as to manage the extra task of creating the callbackLocation header. We do this by ensuring that our handler is created within the same package as the default handler, namely weblogic.webservice.core.handler, and that it has the exact same class name as ClientConversationHandler. Note that this technique involves overriding the existing conversation message handler and therefore is not guaranteed to be forward-compatible. This example, however, does show how a message handler could be used for the creation of the callback headers and this concept can be used with any other web services framework supporting SOAP message handlers.

An alternative to the technique of using SOAP message handlers along with proxy classes is to use the JAXM API to directly create the SOAP request messages and send them to the web service. This is the second technique used by our example client and is implemented within the makeCallsUsingJAXM() and callMethodFromFile() methods of the StockNotificationClient class:

public void makeCallsUsingJAXM() {

      callMethodFromFile("C:\\registerForNotifSOAP.xml");

      ...

      callMethodFromFile("C:\\endAllNotifSOAP.xml");

}



private void callMethodFromFile(String fileName) {



    try {

      // Create a SOAP connection object

      SOAPConnectionFactory scf = SOAPConnectionFactory.newInstance();

      SOAPConnection conn = scf.createConnection();



      // Create the SOAP request message from the specified file

      MessageFactory mf = MessageFactory.newInstance();

      SOAPMessage msg = mf.createMessage();

      SOAPPart sp = msg.getSOAPPart();



      // Read the file content into a string and replace the conversation ID with

      // the current one.

      String fileContent = readFileIntoString(fileName);

      fileContent = fileContent.replaceFirst(CONV_ID_HOLDER,

                                             ConvIDGenerator.getInstance().

                                             getConversationID());



      StreamSource prepMsg = new StreamSource(new StringBufferInputStream(

          fileContent));

      sp.setContent(prepMsg);

      msg.saveChanges();



      // Make the actual call

      conn.call(msg, TARGET_SERVICE_URI);

      conn.close();

    }

    catch (Exception ex) {

      throw new RuntimeException(ex);

    }

}   

The callMethodFromFile() method reads the SOAP message from the specified file, replaces the message’s conversation ID with the client’s current conversation ID, and finally makes the actual call. When using the JAXM approach, the Java to XML mapping of operation parameters and return values must be implemented by the client. We have avoided this task by using pre-generated SOAP messages (that already contain the required parameter values) from the file. Although this approach serves as a good conceptual illustration, it is most likely not a feasible solution for clients containing a reasonable level of complexity. Such clients must create the messages programmatically from scratch using JAXM along with a Java to XML mapping API.

Now that we have reviewed the areas of our client responsible for creating the web service operations, let's turn to the implementation of the client’s callback endpoint and the processing of the callback messages coming from the web service. Since our callback endpoint needs to be able to handle the incoming HTTP requests, a servlet could be a logical choice. Further, it would be highly desirable to have a servlet that is able to extract the SOAP messages included within the incoming HTTP requests and provide it to us in an easy-to-use form. The ReqRespListener interface and the JAXMServlet class of the javax.xml.messaging package provide us with this functionality. Once properly deployed, servlets that use this interface and class will have all targeted HTTP post requests automatically converted to SOAPMessage instances passed to us through the onMessage() method. The following code shows the onMessage() method implementation for our example client.

public class CallBackHandlerServlet

    extends JAXMServlet

    implements ReqRespListener {



  public SOAPMessage onMessage(SOAPMessage message) {

    try {

      // We have just received a SOAP message at the callback

      // endpoint. In this example we will just assume that

      // the message received is in fact the onThresholdPassed

      // callback request. However, a servlet handling

      // multiple callbacks cannot make this assumption and

      // should process the SOAP message to see what callback

      // it relates to and do as appropriate for each.

      String stockTicker = extractOnThresholdPassedArgument(message);



        System.out.println("[DEV2DEV CALLBACK EXAMPLE]: Received treshold notification 

                                        for: " + stockTicker);



      // Now we have to create a proper response to the callback

      // request. Returning this response as part of the onMessage()

      // method will ensure that the SOAP response gets back to the server.

      SOAPMessage response = createThresholdPassedResponse();



      return response;

    }

    catch (Exception e) {

      throw new RuntimeException(e);

    }

  }

  ...

}

Once a callback request message is received by the onMessage() method, our client then extracts the stock ticker argument, prints it to the standard output, and creates the correlated response message that is returned to the web service by our servlet. Clients that use web services that contain more than a single callback operation will also need to dispatch the incoming request to an appropriate area of client code based on the corresponding callback operation.

Although our servlet contains the appropriate code for the processing of incoming onThresholdPassed() callback messages from the web services, it needs to be deployed within a servlet container listening on the callback URI to complete the implementation of the callback endpoint. The Jetty open source project contains a lightweight servlet container that can be embedded within our client. The deployment of the CallBackHandlerServlet servlet within an embedded Jetty container allows us to have the callback endpoint reside within the client Java process. The StockNotificationCallbackProcessor class is a utility class used by our client to start the Jetty server and deploy the CallBackHandlerServlet servlet:

public class StockNotificationCallbackProcessor {



  public static final String CALLBACK_SERVER_PORT = ":8181";

  public static final String CALLBACK_SERVLET_CLASS = 

       "asynchwsconsumer.CallBackHandlerServlet";

  public static final String CALLBACK_SERVLET_NAME = "CallBackHandlerServlet";



  private HttpServer server;



  public StockNotificationCallbackProcessor() {}



  public void startListening() {



    try {

      // Configure the server so that we listen on the callback URI

      server = new Server();

      server.addListener(CALLBACK_SERVER_PORT);

      ServletHttpContext context = (ServletHttpContext) server.getContext("/");

      context.addServlet(CALLBACK_SERVLET_NAME, "/*",

                         CALLBACK_SERVLET_CLASS);



      // Start the http server

      server.start();

    }

    catch (Exception e) {

      throw new RuntimeException(e);

    }

  }



  public void stopListening() {

    try {

      server.stop();

    }

    catch (Exception e) {

      throw new RuntimeException(e);

    }

  }

}

The startListening() method deploys the CallBackHandlerServlet on port 8181 and configures the embedded servlet container so that all HTTP requests are directed to this port. This method also starts the embedded servlet container.

Now that we have examined both the creation of the web service operations and the processing of the callback operations, let's see how the main() method of our example client uses these elements:

public static void main(String[] args) {



    StockNotificationClient snClient = new StockNotificationClient();



    snClient.snCallbackProcessor.startListening();



    snClient.makeCallUsingBEAProxy();



    ConvIDGenerator.getInstance().resetConversationID();



    snClient.makeCallsUsingJAXM();



    snClient.snCallbackProcessor.stopListening();

}

As you can see, the method starts by setting up the callback endpoint (which, as we saw before, involves starting the embedded servlet container and deploying the callback processing servlet to it). The method then uses the BEA clientgen proxy to make calls on the stock notification web service. Given that this interaction with the web services completes the conversation by calling the endAllNotifications() end operation, the client needs to change the conversation ID to start a new conversation. Our client uses the ConvIDGenerator singleton class to manage conversation IDs, and that class uses the java.rmi.dgc.VMID class to create properly formatted unique conversation IDs.

Once the conversation ID has been changed, the client then uses JAXM to once again make calls on the stock notification web service. Finally, the client stops the embedded servlet container and the process ends.

We have now completed our analysis of the example client that can be downloaded as part of this article. When you run this client against the stock notification web services, both the client and the web service will display messages to the standard output describing the various phases of the conversation including the processing of the callback operations.

Conclusion

This article shows the steps involved in the creation of a stand-alone client process for web services containing callback operations. Such a client needs to manage the conversational aspects of the web service calls and provide an endpoint that can be used by the web service for callback operations.

References

Reza Shafii works for BEA Systems professional services.