Handlers and Faults in the predictionsSOAP Service

This section ports the various REST-style versions of the predictions web service to a SOAP-based version. The new version is predictionsSOAP, whose structure can be summarized as follows:

The architecture of the predictionsSOAP service

Figure 5-5. The architecture of the predictionsSOAP service

The predictionsSOAP service mirrors, in its structure, the SOAP-based version of Amazon’s E-Commerce service. In each case, a client-side handler modifies an outgoing SOAP message by inserting a security credential; a service-side handler then verifies the credential before dealing with the request itself.

The PredictionsSOAP class (see Example 5-3), the main class for the predictionsSOAP service, has five service operations: getAll, getOne, create, edit, and delete.

Example 5-3. The PredictionsSOAP class with two methods that throw SOAP faults

package predictions;

import javax.annotation.Resource;
import javax.jws.WebService;
import javax.jws.WebMethod;
import javax.jws.HandlerChain;
import javax.xml.ws.WebServiceContext;
import javax.xml.ws.handler.MessageContext;
import java.util.List;
import javax.servlet.ServletContext;


@WebService
@HandlerChain(file = "../../../WEB-INF/serviceHandler.xml")
public class PredictionsSOAP {
    @Resource
    private WebServiceContext wsCtx;
    private ServletContext sCtx;
    private static final Predictions predictions= new Predictions();
    private static final int maxLength = 16;
    @WebMethod
    public List<Prediction> getAll() {
        init();
        return predictions.getPredictions();
    }
    @WebMethod
    public Prediction getOne(int id) {
        init();
        return predictions.getPrediction(id);
    }
    @WebMethod
    public String create(String who, String what) throws VerbosityException {      1
        int count = wordCount(what);
        if (count > maxLength)
            throw new VerbosityException(count + " is too verbose!",
                                         "Max words: " + maxLength);
        init();
        Prediction p = new Prediction();
        p.setWho(who);
        p.setWhat(what);
        int id = predictions.addPrediction(p);
        String msg = "Prediction " + id + " created.";
        return msg;
    }
    @WebMethod
    public String edit(int id, String who, String what) throws VerbosityException {2
        int count = wordCount(what);
        if (count > maxLength)
            throw new VerbosityException(count + " is too verbose!",
                                         "Max words: " + maxLength);
        init();
        String msg = "Prediction " + id + " not found.";
        Prediction p = predictions.getPrediction(id);
        if (p != null) {
            if (who != null) p.setWho(who);
            if (what != null) p.setWhat(what);
            msg = "Prediction " + id + " updated.";
        }
        return msg;
    }
    @WebMethod
    public String delete(int id) {
        init();
        String msg = "Prediction " + id + " not found.";
        Prediction p = predictions.getPrediction(id);
        if (p != null) {
            predictions.getMap().remove(id);
            msg = "Prediction " + id + " removed.";
        }
        return msg;
    }
    private void init() {
        if (wsCtx == null) throw new RuntimeException("DI failed on wsCtx!");
        if (sCtx == null) { // ServletContext not yet set?
            MessageContext mCtx = wsCtx.getMessageContext();
            sCtx = (ServletContext) mCtx.get(MessageContext.SERVLET_CONTEXT);
            predictions.setServletContext(sCtx);
        }
    }
    private int wordCount(String words) {
        if (words == null) return -1;
        return words.trim().split("\\s+").length;
    }
}

Two of the implementing methods, create and edit (lines 1 and 2), throw an exception named VerbosityException if the creation of a new Prediction or the editing of an existing one results in a candidate prediction that exceeds the maximum length, currently set to 16 words. Each of the methods create and edit is annotated as a @WebMethod and each throws a VerbosityException, which becomes a fault at the SOAP level. As a result, the service’s WSDL now contains an extra message in the portType section for the create and edit operations. Here is a WSDL segment that includes the portion for create and, for contrast, delete:

<portType name="PredictionsSOAP">
  <operation name="delete">
    <input wsam:Action="http://predictions/PredictionsSOAP/deleteRequest" 1
        message="tns:delete" />
    <output wsam:Action="http://predictions/PredictionsSOAP/deleteResponse"
        message="tns:deleteResponse" />
  </operation>
  <operation name="create">
    <input wsam:Action="http://predictions/PredictionsSOAP/createRequest"
        message="tns:create" />
    <output wsam:Action="http://predictions/PredictionsSOAP/createResponse"
        message="tns:createResponse" />
    <fault message="tns:VerbosityException"                               2
        name="VerbosityException"
        wsam:Action=
        "http://predictions/PredictionsSOAP/create/Fault/VerbosityException"/>
   </operation>
   ...

The delete operation has the usual input and output messages (line 1), whereas the create operation now has, in addition to the usual two, a fault message as well (line 2); the fault message, like all of the other messages, is defined in the XML Schema.

A VerbosityException is thrown at the application rather than at the handler level. Accordingly, the underlying SOAP libraries handle the details of converting a Java Exception into a SOAP fault message. The VerbosityException class is standard Java:

package predictions;
public class VerbosityException extends Exception {
    private String details;
    public VerbosityException(String reason, String details) {
        super(reason);                                      1
        this.details = details;
    }
    public String getFaultInfo() { return this.details; }   2
}

A VerbosityException has a reason (line 1) to explain why the fault occurred together with details (line 2) that provide additional information. Both the reason and the details become part of the SOAP fault message.

Generating a VerbosityException is standard Java—a throw clause is used to generate an exception. The bodies of the create and edit methods begin in the same way—with a check of whether the submitted Prediction (the parameter name is what) is too long:

int count = wordCount(what);
if (count > maxLength)
   throw new VerbosityException(count + " is too verbose!",  1
                                "Max words: " + maxLength);

If a candidate Prediction exceeds the maximum length, a VerbosityException is thrown (line 1) with the regular Java syntax. Generating a SOAP fault at the application level requires just two conditions:

For the fault message returned from the PredictionsSOAP service when a submitted prediction is 18 words in length, see Example 5-4.

Example 5-4. A fault generated from a verbose prediction

<S:Envelope xmlns:S="http://schemas.xmlsoap.org/soap/envelope/">
  <S:Header/>
  <S:Body>
    <S:Fault xmlns:ns4="http://www.w3.org/2003/05/soap-envelope">
      <faultcode>S:Server</faultcode>
      <faultstring>18 is too verbose!</faultstring>
      <detail>
        <ns2:VerbosityException xmlns:ns2="http://predictions/">
          <faultInfo>Max words: 16</faultInfo>
          <message>18 is too verbose!</message>
        </ns2:VerbosityException>
      </detail>
    </S:Fault>
  </S:Body>
</S:Envelope>

The PredictionsSOAP class has backend classes in support, in particular Prediction (see Example 5-5) and Predictions (see Example 5-6). Neither the PredictionsSOAP class nor any of the backend classes does any explicit XML processing, of course, because the underlying SOAP libraries handle the serialization and deserialization automatically.

The Prediction class implements Comparable and, therefore, defines the compareTo method so that a client against the predictionsSOAP service can get a sorted list of Predictions on a getAll request. Otherwise, the Prediction class is a POJO class with three properties: id, which identifies a Prediction; who, which names the author of the Prediction; and what, which consists of the actual words in the Prediction.

Example 5-6. The Predictions supporting class

package predictions;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.util.Arrays;
import java.util.List;
import java.util.ArrayList;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import javax.servlet.ServletContext;

public class Predictions {
    private ConcurrentMap<Integer, Prediction> predictions;           1
    private ServletContext sctx;
    private AtomicInteger mapKey;

    public Predictions() {
        predictions = new ConcurrentHashMap<Integer, Prediction>();
        mapKey = new AtomicInteger();
    }
    public void setServletContext(ServletContext sctx) {
        this.sctx = sctx;
    }
    public ServletContext getServletContext() { return this.sctx; }
    public void setMap(ConcurrentMap<String, Prediction> predictions) { }
    public ConcurrentMap<Integer, Prediction> getMap() {
        if (good2Go()) return this.predictions;
        else return null;
    }
    public int addPrediction(Prediction p) {                          2
        int id = mapKey.incrementAndGet();
        p.setId(id);
        predictions.put(id, p);
        return id;
    }
    public Prediction getPrediction(int id) {
        return predictions.get(id);
    }
    public List<Prediction> getPredictions() {                        3
        List<Prediction> list;
        if (good2Go()) {
            Object[] preds = predictions.values().toArray();
            Arrays.sort(preds);
            list = new ArrayList<Prediction>();
            for (Object obj : preds) list.add((Prediction) obj);
            return list;
        }
        else
            return null;
    }
    private boolean good2Go() {
        if (getServletContext() == null) return false;
        if (predictions.size() < 1) populate();
        return true;
    }
    private void populate() {
        String filename = "/WEB-INF/data/predictions.db";
        InputStream in = sctx.getResourceAsStream(filename);
        // Read the data into the array of Predictions.
        if (in != null) {
            try {
                InputStreamReader isr = new InputStreamReader(in);
                BufferedReader reader = new BufferedReader(isr);
                int i = 0;
                String record = null;
                while ((record = reader.readLine()) != null) {
                    String[] parts = record.split("!");
                    Prediction p = new Prediction();
                    p.setWho(parts[0]);
                    p.setWhat(parts[1]);
                    addPrediction(p);
                }
            }
            catch (IOException e) { }
        }
    }
}

The Predictions class provides the supporting data structures, in particular a thread-safe ConcurrentMap (line 1), together with convenience methods such as the method getPredictions (line 3), which returns a sorted List<Prediction>, and the method addPrediction (line 2), which adds a newly created Prediction to the existing collection. The predictionsSOAP service invokes these methods as needed. As in the earlier versions, the service initializes the Prediction collection from the predictions.db file in the deployed WAR file.

There remains one more service-side class to discuss, the ServiceHashHandler. Perhaps the best way to clarify this handler, however, is to switch first to the client side. The reason is that the service-side handler extracts and verifies a credential that a client-side handler needs to inject into every SOAP request message. One motivation behind the predictionsSOAP example is to mimic the authentication scheme used in Amazon’s E-Commerce service.

The PredictionsClient class (see Example 5-7) is a client against the predictionsSOAP service.

Example 5-7. The PredictionsClient against the predictionsSOAP service

import clientSOAP.PredictionsSOAP;
import clientSOAP.PredictionsSOAPService;
import clientSOAP.Prediction;
import clientSOAP.ClientHandlerResolver;
import java.util.List;

public class PredictionsClient {
    public static void main(String[ ] args) {
        if (args.length < 2) {
            System.err.println("Usage: PredictionsClient <name> <key>");   1
            return;
        }
        new PredictionsClient().runTests(args[0], args[1]);
    }
    private void runTests(String name, String key) {
        PredictionsSOAPService service = new PredictionsSOAPService();
        service.setHandlerResolver(new ClientHandlerResolver(name, key));
        PredictionsSOAP port = service.getPredictionsSOAPPort();

        getTests(port);
        postTest(port);
        getAllTest(port);     // confirm the POST
        deleteTest(port, 33); // delete the just POSTed prediction
        getAllTest(port);     // confirm the POST
        putTest(port);
    }
    private void getTests(PredictionsSOAP port) {
        getAllTest(port);
        getOneTest(port);
    }
    private void getAllTest(PredictionsSOAP port) {
        msg("getAll");
        List<Prediction> preds = port.getAll();
        for (Prediction pred : preds)
            System.out.println(String.format("%2d: ", pred.getId()) +
                               pred.getWho() + " predicts: " + pred.getWhat());
    }
    private void getOneTest(PredictionsSOAP port) {
        msg("getOne (31)");
        System.out.println(port.getOne(31).getWhat());
    }
    private void postTest(PredictionsSOAP port) {
        msg("postTest");
        String who = "Freddy";
        String what = "Something bad may happen.";
        String res = port.create(who, what);
        System.out.println(res);
    }
    private void putTest(PredictionsSOAP port) {
        msg("putTest -- here's the record to be edited");
        getOneTest(port);
        msg("putTest results");
        String who = "FooBar";
        String what = null;  // shouldn't change
        int id = 31;
        String res = port.edit(id, who, what);
        System.out.println(res);
        System.out.println("Confirming:");
        Prediction p = port.getOne(31);
        System.out.println(p.getWho());
        System.out.println(p.getWhat());
    }
    private void deleteTest(PredictionsSOAP port, int id) {
        msg("deleteTest");
        String res = port.delete(id);
        System.out.println(res);
    }
    private void msg(String s) {
        System.out.println("\n" + s + "\n");
    }
}

As usual, the PredictionsClient uses wsimport-generated artifacts, which are in the clientSOAP package. This client, together with dependencies, is packaged in the executable JAR file PredictionsClient.jar:

% java -jar PredictionsClient.jar
Usage: PredictionsClient <name> <key>

The client expects two command-line arguments (line 1): a name (in Amazon E-Commerce, the accessId) and a key (in Amazon E-Commerce, the secretKey). The predictionsSOAP service includes a DataStore class that mimics a database with a map with names as the lookup keys and secret keys as their values. Accordingly, the command:

% java -jar PredictionsClient.jar moe MoeMoeMoe

provides the required pair of command-line arguments, with moe as the name and MoeMoeMoe as the key.

The PredictionsClient dynamically sets the client-side handler whose job is to turn the command-line arguments into a credential that the service-side handler can verify. Here is the relevant code segment:

PredictionsSOAPService service = new PredictionsSOAPService();
service.setHandlerResolver(new ClientHandlerResolver(name, key)); 1
PredictionsSOAP port = service.getPredictionsSOAPPort();

In line 1, name and key are the two command-line arguments. After setting the handler, the PredictionsClient runs the expected tests against the CRUD operations that the predictionsSOAP service implements: getAll, getOne, create, edit, and delete. It should be noted that the PredictionsClient, like the predictionsSOAP service, does absolutely no XML processing but instead works exclusively with Java data structures such as List<Prediction>.

The ClientHandlerResolver class (see Example 5-8) registers an instance of the class ClientHashHandler with the runtime system. Before digging into the details, it may be helpful to do a before/after comparison with respect to the handler.

Example 5-8. The ClientHandlerResolver and ClientHashHandler classes

package clientSOAP;

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
import java.util.Set;
import java.util.TimeZone;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.xml.namespace.QName;
import javax.xml.soap.SOAPMessage;
import javax.xml.soap.SOAPEnvelope;
import javax.xml.soap.SOAPHeader;
import javax.xml.ws.handler.Handler;
import javax.xml.ws.handler.HandlerResolver;
import javax.xml.ws.handler.MessageContext;
import javax.xml.ws.handler.PortInfo;
import javax.xml.ws.handler.soap.SOAPHandler;
import javax.xml.ws.handler.soap.SOAPMessageContext;
import org.apache.commons.codec.binary.Base64;
import org.w3c.dom.Element;
import org.w3c.dom.Node;

public class ClientHandlerResolver implements HandlerResolver {
    private String name;
    private String key;

    public ClientHandlerResolver(String name, String key) {
        this.name = name;
        this.key = key;
    }
    public List<Handler> getHandlerChain(PortInfo portInfo) {
        List<Handler> handlerChain = new ArrayList<Handler>();
        handlerChain.add(new ClientHashHandler(this.name, this.key));
        return handlerChain;
    }
}

class ClientHashHandler implements SOAPHandler<SOAPMessageContext> {
    private byte[ ] secretBytes;
    private String name;

    public ClientHashHandler(String name, String key) {
        this.name = name;
        this.secretBytes = getBytes(key);
    }
    public void close(MessageContext mCtx) { }                                  1
    public Set<QName> getHeaders() { return null; }                             2
    public boolean handleFault(SOAPMessageContext mCtx) {                       3
        try {
            SOAPMessage msg = mCtx.getMessage();
            msg.writeTo(System.err);
        }
        catch(Exception e) { throw new RuntimeException(e); }
        return true;
    }
    public boolean handleMessage(SOAPMessageContext mCtx) {                     4
        Boolean outbound =
            (Boolean) mCtx.get(MessageContext.MESSAGE_OUTBOUND_PROPERTY);
        if (outbound) {                                                         5
            try {
                SOAPMessage soapMessage = mCtx.getMessage();
                SOAPEnvelope envelope = soapMessage.getSOAPPart().getEnvelope();
                // Ensure there is a header and add a 'wrapper' element.
                if (envelope.getHeader() == null) envelope.addHeader();         6
                SOAPHeader header = envelope.getHeader();
                QName qn = new QName("http://predictionsSOAP", "credentials");
                header.addHeaderElement(qn);                                    7
                // Now insert credentials into the header.
                String timeStamp = getTimestamp();
                String signature = getSignature(this.name,
                                                timeStamp,
                                                this.secretBytes);
                Node firstChild = header.getFirstChild();
                append(firstChild, "Name",      this.name);                     8
                append(firstChild, "Signature", signature);                     9
                append(firstChild, "Timestamp", timeStamp);                     10
                soapMessage.saveChanges();
            }
            catch(Exception e) {
                throw new RuntimeException("SOAPException thrown.", e);
            }
        }
        return true; // continue down the handler chain
    }
    private String getSignature(String name, String timestamp, byte[ ] secretBytes) {
        try {
            System.out.println("Name ==      " + name);
            System.out.println("Timestamp == " + timestamp);
            String toSign = name + timestamp;
            byte[] toSignBytes = getBytes(toSign);
            Mac signer = Mac.getInstance("HmacSHA256");
            SecretKeySpec keySpec = new SecretKeySpec(secretBytes, "HmacSHA256");
            signer.init(keySpec);
            signer.update(toSignBytes);
            byte[] signBytes = signer.doFinal();
            String signature = new String(Base64.encodeBase64(signBytes));
            return signature;
        }
        catch(Exception e) {
            throw new RuntimeException("NoSuchAlgorithmException thrown.", e);
        }
    }
    private String getTimestamp() {
        Calendar calendar = Calendar.getInstance();
        SimpleDateFormat dateFormat =
            new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
        dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
        return dateFormat.format(calendar.getTime());
    }
    private void append(Node node, String elementName, String elementText) {
        Element element = node.getOwnerDocument().createElement(elementName);
        element.setTextContent(elementText);
        node.appendChild(element);
    }
    private byte[ ] getBytes(String str) {
        try {
            return str.getBytes("UTF-8");
        }
        catch(Exception e) { throw new RuntimeException(e); }
    }
}

As an example, consider the first test that the PredictionsClient runs: the client invokes getAll on the service to get a list of all of the predictions. Here is what happens:

A SOAP header together with a credentials element is added; the credentials element has three subelements tagged Name, Signature, and Timestamp in that lexicographical order. The Name is the command-line argument moe and the Signature is an HmacSHA256 hash encoded in base64, the same kind of hash used in Amazon’s E-Commerce service. The Signature hash is generated from the provided key value MoeMoeMoe, but this secretKey cannot be recovered from the hash. (Chapter 6, on security, explains why.) Accordingly, Moe’s secret key is not in jeopardy of being hijacked when the SOAP request is sent over the wire.

The ClientHashHandler class implements the SOAPHandler interface and, therefore, defines the four methods getHeaders, close, handleFault, and handleMessage (lines 1 through 4 in the code listing). Only handleFault and handleMessage are of interest here. Recall that handlers are inherently bidirectional; that is, they handle incoming and outgoing messages alike, and only one of these methods is invoked in either case: if there is a fault, the runtime invokes handleFault; otherwise, the runtime invokes handleMessage.

The handleMessage method has work to do only on outgoing messages or requests; hence, this method checks the direction of the message (line 5). If the message is indeed outgoing, the handler does the following:

  • Checks whether there is a SOAP header and, if not, adds one (line 6).
  • Adds, as the first child of the SOAP header, an element tagged credentials (line 7).
  • Adds, as children of the credentials element, three elements tagged Name (with a value such as moe), Signature (whose value is an HmacSHA256 hash generated with, in this case, Moe’s secret key), and Timestamp (whose value is a properly formatted timestamp).

The outgoing SOAP message, a request, is now properly structured. The SOAP body names the operation of interest (for instance, getOne) and includes any required arguments (in this example, the integer identifier of the Prediction to get). The SOAP header contains the requester’s name, a hash value that serves as a signature, and a timestamp.

The handler method handleFault does not check the message direction because a fault would arrive, in any case, as a response from the predictionsSOAP service. This service generates a SOAP fault as a VerbosityException if a candidate Prediction is excessively wordy. At present, handleFault simply prints the SOAP fault to the standard error; in a production environment, more elaborate logic might be brought into play—for instance, the fault might be saved in a data store for later analysis.

Handlers, especially SOAP handlers, are powerful in that they can amend the SOAP message created at the application level. In this example, a SOAP rather than a Logical handler is needed because the handler needs access to the SOAP header. On the service side, the handler also needs to be a SOAP handler.

The ServiceHashHandler (see Example 5-9) is a service-side SOAP handler. On any incoming message, this handler checks for the following:

  • Does the message include a SOAP header? If not, generate a SOAP fault (line 1).
  • Are there at least three children of the element tagged credentials? If not, generate a SOAP fault (line 2).
  • Are the Name, Signature, and Timestamp values all non-null? If not, generate a SOAP fault (line 3).
  • Does the Name, used as a lookup key in the service-side data store, have a value? (The value should be the user’s secret key.) If not, generate a SOAP fault (line 4).
  • Does the Signature generated on the service side match the Signature sent in the client request? If not, generate a SOAP fault (line 5).

Example 5-9. The service-side ServiceHashHandler, which verifies the credentials in a request

package predictions;

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
import java.util.Set;
import java.util.Map;
import java.util.HashMap;
import java.util.TimeZone;
import java.util.Iterator;
import java.util.Arrays;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.xml.namespace.QName;
import javax.xml.soap.SOAPHeader;
import javax.xml.soap.SOAPBody;
import javax.xml.soap.SOAPFault;
import javax.xml.soap.SOAPMessage;
import javax.xml.ws.soap.SOAPFaultException;
import javax.xml.soap.SOAPException;
import javax.xml.ws.handler.Handler;
import javax.xml.ws.handler.MessageContext;
import javax.xml.ws.handler.soap.SOAPHandler;
import javax.xml.ws.handler.soap.SOAPMessageContext;
import org.apache.commons.codec.binary.Base64;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

public class ServiceHashHandler implements SOAPHandler<SOAPMessageContext> {
    private byte[ ] secretBytes;

    public ServiceHashHandler() { }
    public void close(MessageContext mCtx) { }
    public Set<QName> getHeaders() { return null; }
    public boolean handleFault(SOAPMessageContext mCtx) {
        return true;
    }
    public boolean handleMessage(SOAPMessageContext mCtx) {
        Boolean outbound =
            (Boolean) mCtx.get(MessageContext.MESSAGE_OUTBOUND_PROPERTY);
        if (!outbound) {
            try {
                SOAPMessage msg = mCtx.getMessage();
                SOAPHeader soapHeader = msg.getSOAPHeader();
                if (soapHeader == null)                                           1
                    generateFault(msg, "No header!");
                Node node = soapHeader.getFirstChild();   // credentials
                NodeList nodeList = node.getChildNodes(); // Name, Timestamp, Sig.
                if (nodeList.getLength() < 3)                                     2
                    generateFault(msg, "Too few header nodes!");
                // Extract the required attributes.
                String name = nodeList.item(0).getFirstChild().getNodeValue();
                String signature = nodeList.item(1).getFirstChild().getNodeValue();
                String timestamp = nodeList.item(2).getFirstChild().getNodeValue();
                if (name == null || timestamp == null || signature == null)       3
                    generateFault(msg, "Missing header key/value pairs!");
                // Generate comparison signature and compare against what's sent.
                String secret = DataStore.get(name);
                if (secret == null)
                    generateFault(msg, name + " not registered!");                4
                byte[ ] secretBytes = getBytes(secret);
                String localSignature = getSignature(name, timestamp, secretBytes);
                if (!verify(signature, localSignature))
                    generateFault(msg, "HMAC signatures do not match.");          5
            }
            catch(Exception e) {
                throw new RuntimeException("SOAPException thrown.", e);
            }
        }
        return true; // continue down the handler chain
    }
    private boolean verify(String sig1, String sig2) {
        return Arrays.equals(sig1.getBytes(), sig2.getBytes());
    }
    private String getSignature(String name, String timestamp, byte[ ] secretBytes) {
        try {
            System.err.println("Name ==      " + name);
            System.err.println("Timestamp == " + timestamp);
            String toSign = name + timestamp;
            byte[] toSignBytes = getBytes(toSign);
            Mac signer = Mac.getInstance("HmacSHA256");
            SecretKeySpec keySpec = new SecretKeySpec(secretBytes, "HmacSHA256");
            signer.init(keySpec);
            signer.update(toSignBytes);
            byte[] signBytes = signer.doFinal();
            String signature = new String(Base64.encodeBase64(signBytes));
            return signature;
        }
        catch(Exception e) {
            throw new RuntimeException("NoSuchAlgorithmException thrown.", e);
        }
    }
    private String getTimestamp() {
        Calendar calendar = Calendar.getInstance();
        SimpleDateFormat dateFormat =
            new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
        dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
        return dateFormat.format(calendar.getTime());
    }
    private byte[ ] getBytes(String str) {
        try {
            return str.getBytes("UTF-8");
        }
        catch(Exception e) { throw new RuntimeException(e); }
    }
    private void generateFault(SOAPMessage msg, String reason) {
        try {
            SOAPBody body = msg.getSOAPBody();
            SOAPFault fault = body.addFault();
            fault.setFaultString(reason);
            throw new SOAPFaultException(fault);
        }
        catch(SOAPException e) { }
    }
}

The predictionsSOAP service does signature verification in basically the same way that Amazon does. To make a request against the predictionsSOAP service, a client needs a key. How this is distributed to the client is ignored in this example. In the Amazon case, the secretKey is provided when a user registers with Amazon, and, of course, Amazon maintains a copy of the secretKey. In the predictionsSOAP example, the service-side DataStore has a map whose lookup keys are usernames (for instance, Moe) and whose values are the users’ secret keys (in this case, MoeMoeMoe). On an incoming message, the ServiceHashHandler recomputes the hash value—generated on the client side with the user’s key—and then does a byte-by-byte comparison of the sent signature and the signature computed on the service side. The code is in the verify utility method:

private boolean verify(String sig1, String sig2) {
   return Arrays.equals(sig1.getBytes(), sig2.getBytes());
}

The argument sig1 is the sent signature, and the argument sig2 is the signature computed on the service side.

The API for generating a SOAP fault at the handler level differs significantly from the API for generating a SOAP fault at the application level. At the application level, the regular Java syntax of:

throw new VerbosityException(...);

suffices; at the handler level, by contrast, the SOAP fault needs to be constructed and then thrown. Here again is the generateFault method in the ServiceHashHandler:

private void generateFault(SOAPMessage msg, String reason) {
   try {
      SOAPBody body = msg.getSOAPBody();   1
      SOAPFault fault = body.addFault();   2
      fault.setFaultString(reason);        3
      throw new SOAPFaultException(fault); 4
   }
   catch(SOAPException e) { }
}

The generateFault method uses the incoming SOAP message (msg is the reference) to get the SOAP body (line 1). A SOAP fault is then added to body (line 2); the reason for the fault is given; and a SOAPFaultException, initialized with the fault information (line 3), is thrown—which in turn causes a SOAP fault message to be sent back to the requester (line 4). If desired, additional Detail could be added to the SOAPFault, which has an addDetail method.

The service-side handler ServiceHashHandler needs to be linked to the service itself, whose @WebService-annotated class is PredictionsSOAP. On the client side, the linking is dynamic. Here, for review, are the two critical lines of code in the PredictionsClient:

PredictionsSOAPService service = new PredictionsSOAPService();    1
service.setHandlerResolver(new ClientHandlerResolver(name, key)); 2

Line 2 in the listing performs the dynamic linking. With Tomcat deployment, this option is not available. Instead, the predictionsSOAP service and the ServiceHashHandler are linked through an XML configuration file encapsulated in the deployed WAR file:

<handler-chains xmlns="http://java.sun.com/xml/ns/javaee">
  <handler-chain>                                                      1
    <handler>
      <handler-name>predictions.ServiceHashHandler</handler-name>      2
      <handler-class>predictions.ServiceHashHandler</handler-class>
    </handler>
  </handler-chain>
</handler-chains>

The name of the configuration file is arbitrary. A handler-chain (line 1) can include arbitrarily many handler instances but, in this case, there is but one handler in the handler-chain, the handler ServiceHashHandler (line 2). This configuration file then is referenced with a @HandlerChain annotation in the PredictionsSOAP class:

@WebService
@HandlerChain(file = "../../../WEB-INF/serviceHandler.xml")
public class PredictionsSOAP {
...

The serviceHandler.xml file winds up in the WEB-INF directory of the deployed WAR file because the usual Ant script is used to deploy the predictionsSOAP service to Tomcat.

The configuration document serviceHandler.xml shown above indicates, with its handler-chain tag, that multiple handlers might be in play on either the service or the client side. Figure 5-6 depicts the structure of such a chain. For an outgoing message, logical handlers come into play first. This is appropriate because such handlers have limited scope; that is, they have access only to the payload in the SOAP body. The (SOAP) message handlers then come into play and these handlers, as noted earlier, have access to the entire SOAP message. For an incoming message, the order is reversed: the (SOAP) message handlers have first access and the logical handlers have last access. Message handlers are sufficient for any handler logic precisely because they have access to the entire SOAP message, but logical handlers are convenient in that the runtime makes available only the SOAP body’s payload.

The next section adds a second handler, in this case a LogicalHandler, to the client side of the predictionsSOAP service. The result is a true chain of handlers.