- Chapter 3: Simple Object Access Protocol (SOAP)
- Simple Object Access Protocol (SOAP)
- Doing Business with SkatesTown
- Inventory Check Web Service
- SOAP Envelope Framework
- Taking Advantage of SOAP Extensibility
- SOAP Intermediaries
- Error Handling in SOAP
- SOAP Data Encoding
- Architecting Distributed Systems with Web Services
- Purchase Order Submission Web Service
- SOAP Protocol Bindings
- Summary
- The Road Ahead
- Resources
Taking Advantage of SOAP Extensibility
Let's take a look at how SkatesTown can use SOAP extensibility to its benefit. It turns out that SkatesTown's partners are demanding some type of proof that certain items are in SkatesTown's inventory. In particular, partners would like to have an e-mail record of any inventory checks they have performed.
Al Rosen got the idea to use SOAP extensibility in a way that allows the existing inventory check service implementation to be reused with no changes. SOAP inventory check requests will include a header whose element name is EMail belonging to the http://www.skatestown.com/ns/email namespace. The value of the header will be a simple string containing the e-mail address to which the inventory check confirmation should be sent.
Service Requestor View
Service requestors will have to modify their clients to build a custom SOAP envelope that includes the EMail header. Listing 3.5 shows the necessary changes. The e-mail to send confirmations to is provided in the constructor.
Listing 3.5 Updated Inventory Check Client
package ch3.ex3; import org.apache.axis.client.ServiceClient; import org.apache.axis.message.SOAPEnvelope; import org.apache.axis.message.SOAPHeader; import org.apache.axis.message.RPCElement; import org.apache.axis.message.RPCParam; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import org.w3c.dom.Document; import org.w3c.dom.Element; /* * Inventory check web service client */ public class InventoryCheckClient { /** * Service URL */ String url; /** * Email address to send confirmations to */ String email; /** * Point a client at a given service URL */ public InventoryCheckClient(String url, String email) { this.url = url; this.email = email; } /** * Invoke the inventory check web service */ public boolean doCheck(String sku, int quantity) throws Exception { // Build the email header DOM element DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); DocumentBuilder builder = factory.newDocumentBuilder(); Document doc = builder.newDocument(); Element emailElem = doc.createElementNS( "http://www.skatestown.com/", "EMail"); emailElem.appendChild(doc.createTextNode(email)); // Build the RPC request SOAP message SOAPEnvelope reqEnv = new SOAPEnvelope(); reqEnv.addHeader(new SOAPHeader(emailElem)); Object[] params = new Object[]{ sku, new Integer(quantity), }; reqEnv.addBodyElement(new RPCElement("", "doCheck", params)); // Invoke the inventory check web service ServiceClient call = new ServiceClient(url); SOAPEnvelope respEnv = call.invoke(reqEnv); // Retrieve the response RPCElement respRPC = (RPCElement)respEnv.getFirstBody(); RPCParam result = (RPCParam)respRPC.getParams().get(0); return ((Boolean)result.getValue()).booleanValue(); } }
To set a header in Axis, you first need to build the DOM representation for the header. The code in the beginning of doCheck() does this. Then you need to manually construct the SOAP message that will be sent. This involves starting with a new SOAPEnvelope object, adding a SOAPHeader with the DOM element constructed earlier, and, finally, adding an RPCElement as the body of the message. At this point, you can use ServiceClient.invoke() to send the message.
When the call is made with a custom-built SOAP envelope, the return value of invoke() is also a SOAPEnvelope object. You need to pull the relevant data out of that envelope by getting the body of the response, which will be an RPCElement. The result of the operation will be the first RPCParam inside the RPC response. Knowing that doCheck() returns a boolean, you can get the value of the parameter and safely cast it to Boolean.
As you can see, the code is not trivial, but Axis does provide a number of convenience objects that make working with custom-built SOAP messages straightforward. Figure 3.5 shows a UML diagram with some of the key Axis objects related to SOAP messages.
Figure 3.5 Axis SOAP message objects.
Service Provider View
The situation on the side of the Axis-based service provider is a little more complicated because we can no longer use a simple JWS file for the service. JWS files are best used for simple and straightforward service implementations. Currently, it is not possible to indicate from a JWS file that a certain header (in this case the e-mail header) should be processed. Al Rosen implements three changes to enable this more sophisticated type of service:
He moves the service implementation from the JWS file to a simple Java class.
He writes a handler for the EMail header.
He extends the Axis service deployment descriptor with information about the service implementation and the header handler.
Moving the service implementation is as simple as saving InventoryCheck.jws as InventoryCheck.java in /WEB-INF/classes/com/skatestown/services. No further changes to the service implementation are necessary.
Building a handler for the EMail header is relatively simple, as Listing 3.6 shows. When the handler is invoked by Axis, it needs to find the SOAP message and lookup the EMail header using its namespace and name. If the header is present in the request message, the handler sends a confirmation e-mail of the inventory check. The implementation is complex because to produce a meaningful e-mail confirmation, the handler needs to see both the request data (SKU and quantity) and the result of the inventory check. The basic process involves the following steps:
-
Get the request or the response message using getRequestMessage() or getResponseMessage() on the Axis MessageContext object.
-
Get the SOAP envelope by calling getAsSOAPEnvelope().
-
Retrieve the first body of the envelope and cast it to an RPCElement because the body represents either an RPC request or an RPC response.
-
Get the parameters of the RPC element using getParams().
-
Extract parameters by their position and cast them to their appropriate type. As seen earlier in Listing 3.5, the response of an RPC is the first parameter in the response message body.
Listing 3.6 E-mail Header Handler
package com.skatestown.services; import java.util.Vector; import org.apache.axis.* ; import org.apache.axis.message.*; import org.apache.axis.handlers.BasicHandler; import org.apache.axis.encoding.SOAPTypeMappingRegistry; import bws.BookUtil; import com.skatestown.backend.EmailConfirmation; /** * EMail header handler */ public class EMailHandler extends BasicHandler { /** * Utility method to retrieve RPC parameters * from a SOAP message. */ private Object getParam(Vector params, int index) { return ((RPCParam)params.get(index)).getValue(); } /** * Looks for the EMail header and sends an email * confirmation message based on the inventory check * request and the result of the inventory check */ public void invoke(MessageContext msgContext) throws AxisFault { try { // Attempt to retrieve EMail header Message reqMsg = msgContext.getRequestMessage(); SOAPEnvelope reqEnv = reqMsg.getAsSOAPEnvelope(); SOAPHeader header = reqEnv.getHeaderByName( "http://www.skatestown.com/", "EMail" ); if (header != null) { // Mark the header as having been processed header.setProcessed(true); // Get email address in header String email = (String)header.getValueAsType( SOAPTypeMappingRegistry.XSD_STRING); // Retrieve request parameters: SKU & quantity RPCElement reqRPC = (RPCElement)reqEnv.getFirstBody(); Vector params = reqRPC.getParams(); String sku = (String)getParam(params, 0); Integer quantity = (Integer)getParam(params, 0); // Retrieve inventory check result Message respMsg = msgContext.getResponseMessage(); SOAPEnvelope respEnv = respMsg.getAsSOAPEnvelope(); RPCElement respRPC = (RPCElement)respEnv.getFirstBody(); Boolean result = (Boolean)getParam( respRPC.getParams(), 0); // Send confirmation email EmailConfirmation ec = new EmailConfirmation( BookUtil.getResourcePath(msgContext, "/resources/email.log")); ec.send(email, sku, quantity.intValue(), result.booleanValue()); } } catch(Exception e) { throw new AxisFault(e); } } /** * Required method of handlers. No-op in this case */ public void undo(MessageContext msgContext) { } }
It's simple code, but it does take a few lines because several layers need to be unwrapped to get to theRPC parameters. When all data has been retrieved, the handler calls the e-mail confirmation backend, which, in this example, logs e-mails "sent" to /resources/email.log.
Finally, adding deployment information about the new header handler and the inventory check service involves making a small change to the Axis Web services deployment descriptor. The book example deployment descriptor is in /resources/deploy.xml. Working with Axis deployment descriptors will be described in detail in Chapter 4.
Listing 3.7 shows the five lines of XML that need to be added. First, the e-mail handler is registered by associating a handler name with its Java class name. Following that is the description of the inventory check service. The service options identify the Java class name for the service and the method that implements the service functionality. The service element has two attributes. Pivot is an Axis term that specifies the type of service. In this case, the value is RPCDispatcher, which implies that InventoryCheck is an RPC service. The output attribute specifies the name of a handler that will be called after the service is invoked. Because the book examples don't rely on an e-mail server being present, instead of sending confirmation this class writes messages to a log file in /resources/email.log.
Listing 3.7 Deployment Descriptor for Inventory Check Service
<!-- Chapter 3 example 3 services --> <handler name="Email" class="com.skatestown.services.EMailHandler"/> <service name="InventoryCheck" pivot="RPCDispatcher" response="Email"> <option name="className" value="com.skatestown.services.InventoryCheck"/> <option name="methodName" value="doCheck"/> </service>
Putting the Service to the Test
With all these changes in place, we are ready to test the improved inventory check service. There is a simple JSP test harness in ch3/ex3/index.jsp that is modeled after the JSP test harness we used for the JWS-based inventory check service (see Figure 3.6).
Figure 3.6 Putting the enhanced inventory check Web service to the test.
SOAP on the Wire
With the help of TCPMon, we can see what SOAP messages are passing between the client and the Axis engine. We are only interested in seeing the request message because the response message will be identical to the one before the EMail header was added.
Here is the SOAP request message with the EMail header present:
POST /bws/services/InventoryCheck HTTP/1.0 Content-Length: 482 Host: localhost Content-Type: text/xml; charset=utf-8 SOAPAction: "/doCheck" <?xml version="1.0" encoding="UTF-8"?> <SOAP-ENV:Envelope SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <SOAP-ENV:Header> <e:EMail xmlns:e="http://www.skatestown.com/ns/email"> confirm@partners.com </e:EMail> </SOAP-ENV:Header> <SOAP-ENV:Body> <ns1:doCheck xmlns:ns1="AvailabilityCheck"> <arg0 xsi:type="xsd:string">947-TI</arg0> <arg1 xsi:type="xsd:int">1</arg1> </ns1:doCheck> </SOAP-ENV:Body> </SOAP-ENV:Envelope>
There are no surprises in the SOAP message. However, a couple of things have changed in the HTTP message. First, the target URL is /bws/services/InventoryCheck. This is a combination of two parts: the URL of the Axis servlet that listens for SOAP requests over HTTP (/bws/services) and the name of the service we want to invoke (InventoryCheck). Also, the SOAPAction header, which was previously empty, now contains the name of the method we want to invoke. The service name on the URL and the method name in SOAPAction are both hints to Axis about the service we want to invoke.
That's all there is to taking advantage of SOAP custom headers. The key message is one of simple yet flexible extensibility. Remember, the inventory check service implementation did not change at all!