A RESTful Service as a @WebServiceProvider

JAX-WS includes APIs for RESTful and SOAP-based web services, although JAX-WS seems to be used mostly for the latter. The reference implementation is Metro, which is part of the GlassFish project. Although JAX-WS technically belongs to enterprise rather than core Java, the core Java JDK (1.6 or greater) includes enough of the Metro distribution to compile and publish RESTful and SOAP-based services. JAX-RS and Restlet are state-of-the-art, high-level APIs for developing RESTful services; by contrast, the JAX-WS API for RESTful services is low-level. Nonetheless, JAX-WS support for RESTful services deserves a look, and the JAX-WS API for SOAP-based services will be the centerpiece in Chapter 4 and Chapter 5.

The JAX-WS stack reflects the view that SOAP-based services over HTTP are refinements of RESTful services. The JAX-WS API has two main annotations. A POJO class annotated as a @WebService delivers a SOAP-based service, whereas a POJO class annotated as a @WebServiceProvider usually delivers a RESTful one; however, a class annotated as a @WebServiceProvider can deliver a SOAP-based service as well. Yet another revision of the adages RESTful service, adages3, introduces the JAX-WS API for RESTful services.

In the revised adages3 service, the Adage and Adages classes are mostly unchanged from the adages2 version. One small change is that the package name goes from adages2 to adages3; another change is that the Adage list is returned as array, which then is serialized into XML. The toPlain method in the Adages class could be dropped because the revised service deals only in application/xml and not in text/plain HTTP payloads. The AdagesProvider class (see Example 2-19) supports the four CRUD operations against the RESTful service.

Example 2-19. The AdagesProvider class that supports the CRUD operations

package adages3;

import java.beans.XMLEncoder;
import java.io.ByteArrayOutputStream;
import java.io.ByteArrayInputStream;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Arrays;
import javax.annotation.Resource;
import javax.xml.transform.Source;
import javax.xml.transform.stream.StreamSource;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.stream.StreamResult;
import javax.xml.ws.handler.MessageContext;
import javax.xml.ws.WebServiceContext;
import javax.xml.ws.WebServiceProvider;
import javax.xml.ws.Provider;
import javax.xml.ws.BindingType;
import javax.xml.ws.http.HTTPBinding;
import javax.xml.ws.ServiceMode;
import javax.xml.ws.http.HTTPException;
import javax.xml.xpath.XPathFactory;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import org.xml.sax.InputSource;

@WebServiceProvider                             // generic service provider       1
@ServiceMode(javax.xml.ws.Service.Mode.MESSAGE) // entire message available       2
@BindingType(HTTPBinding.HTTP_BINDING)          // versus SOAP binding            3
public class AdagesProvider implements Provider<Source> {
    @Resource
    protected WebServiceContext wctx;  // dependency injection

    public AdagesProvider() { }
    // Implement the Provider interface by defining invoke, which expects an XML
    // source (perhaps null) and returns an XML source (perhaps null).
    public Source invoke(Source request) {                                        4
        if (wctx == null) throw new RuntimeException("Injection failed on wctx.");
        // Grab the message context and extract the request verb.
        MessageContext mctx = wctx.getMessageContext();                           5
        String httpVerb = (String) mctx.get(MessageContext.HTTP_REQUEST_METHOD);
        httpVerb = httpVerb.trim().toUpperCase();
        // Dispatch on verb to the handler method. POST and PUT have non-null
        // requests so only these two get the Source request.
        if      (httpVerb.equals("GET"))    return doGet(mctx);                   6
        else if (httpVerb.equals("POST"))   return doPost(request);
        else if (httpVerb.equals("PUT"))    return doPut(request);
        else if (httpVerb.equals("DELETE")) return doDelete(mctx);
        else throw new HTTPException(405);  // bad verb
    }
    private Source doGet(MessageContext mctx) {
        // Parse the query string.
        String qs = (String) mctx.get(MessageContext.QUERY_STRING);
        // Get all Adages.
        if (qs == null) return adages2Xml();
        // Get a specified Adage.
        else {
            int id = getId(qs);
            if (id < 0) throw new HTTPException(400); // bad request
            Adage adage = Adages.find(id);
            if (adage == null) throw new HTTPException(404); // not found
            return adage2Xml(adage);
        }
    }
    private Source doPost(Source request) {
        if (request == null) throw new HTTPException(400); // bad request
        InputSource in = toInputSource(request);
        String pattern = "//words/text()"; // find the Adage's "words"
        String words = findElement(pattern, in);
        if (words == null) throw new HTTPException(400); // bad request
        Adages.add(words);
        String msg = "The adage '" + words + "' has been created.";
        return toSource(toXml(msg));
    }
    private Source doPut(Source request) {
        if (request == null) throw new HTTPException(400); // bad request
        InputSource in = toInputSource(request);
        String pattern = "//words/text()";  // find the Adage's "words"
        String words = findElement(pattern, in);
        if (words == null) throw new HTTPException(400); // bad request
        // Format in XML is: <words>!<id>
        String[ ] parts = words.split("!");
        if (parts[0].length() < 1 || parts[1].length() < 1)
            throw new HTTPException(400); // bad request
        int id = -1;
        try {
            id = Integer.parseInt(parts[1].trim());
        }
        catch(Exception e) { throw new HTTPException(400); } // bad request
        // Find and edit.
        Adage adage = Adages.find(id);
        if (adage == null) throw new HTTPException(404); // not found
        adage.setWords(parts[0]);
        String msg = "Adage " + adage.getId() + " has been updated.";
        return toSource(toXml(msg));
    }
    private Source doDelete(MessageContext mctx) {
        String qs = (String) mctx.get(MessageContext.QUERY_STRING);
        // Disallow the deletion of all teams at once.
        if (qs == null) throw new HTTPException(403); // illegal operation
        else {
            int id = getId(qs);
            if (id < 0) throw new HTTPException(400); // bad request
            Adage adage = Adages.find(id);
            if (adage == null) throw new HTTPException(404); // not found
            Adages.remove(adage);
            String msg = "Adage " + id + " removed.";
            return toSource(toXml(msg));
        }
    }
    private int getId(String qs) {
        int badId = -1; // bad ID
        String[ ] parts = qs.split("=");
        if (!parts[0].toLowerCase().trim().equals("id")) return badId;
        int goodId = badId; // for now
        try {
            goodId = Integer.parseInt(parts[1].trim());
        }
        catch(Exception e) { return badId; }
        return goodId;
    }
    private StreamSource adages2Xml() {
        String str = toXml(Adages.getListAsArray());
        return toSource(str);
    }
    private StreamSource adage2Xml(Adage adage) {
        String str = toXml(adage);
        return toSource(str);
    }
    private String toXml(Object obj) {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        XMLEncoder enc = new XMLEncoder(out);
        enc.writeObject(obj);
        enc.close();
        return out.toString();
    }
    private StreamSource toSource(String str) {
        return new StreamSource(new StringReader(str));
    }
    private InputSource toInputSource(Source source) {
        InputSource input = null;
        try {
            Transformer trans = TransformerFactory.newInstance().newTransformer();
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            StreamResult result = new StreamResult(bos);
            trans.transform(source, result);
            input = new InputSource(new ByteArrayInputStream(bos.toByteArray()));
        }
        catch(Exception e) { throw new HTTPException(500); } // internal server error
        return input;
    }
    private String findElement(String expression, InputSource source) {
        XPath xpath = XPathFactory.newInstance().newXPath();
        String retval = null;
        try {
            retval =
                (String) xpath.evaluate(expression, source, XPathConstants.STRING);
        }
        catch(Exception e) { throw new HTTPException(400); } // bad request
        return retval;
    }
}

Even a glance at the AdagesProvider code looks low-level. Much of this code transforms one type to another, for example, a Source to an InputSource or an Adage to a StreamSource. The Source types are sources of XML. The JAX-P (Java API for XML-Processing) packages, used in the adages3 service, support transforms that convert a source into a result (see Figure 2-1).

A JAX-P transform

Figure 2-1. A JAX-P transform

For example, a generic Source might be transformed into a specific type such as StreamResult. The need for such transformations in the adages3 service is explained shortly. First, however, it will be helpful to consider the overall structure of the AdagesProvider.

Three annotations adorn the AdagesProvider class:

This overview should help in the more detailed analysis that follows. In the adages3 service, the doGet method needs to handle two cases:

The doPost method is:

private Source doPost(Source request) {
    if (request == null) throw new HTTPException(400); // bad request
       InputSource in = toInputSource(request);                         1
       String pattern = "//words/text()"; // find the Adage's "words"   2
       String words = findElement(pattern, in);
       if (words == null) throw new HTTPException(400); // bad request
       Adages.add(words);
       String msg = "The adage '" + words + "' has been created.";
       return toSource(toXml(msg));
}

This method relies on utility methods, in particular on the tricky toInputSource method (line 1) that transforms a Source request, which is likely but not necessarily a StreamSource, into an InputSource. The reason is that, for convenience, the doPost method uses an XPath instance to search the incoming XML document for the words in the Adage to be created. For example, the XML document might look like this in a POST request:

<ns1:foo xmlns:ns1='http://sample.org'>
   <words>This is the way the world ends.</words>
</ns1:foo>

An XPath search requires a pattern, in this example (line 2):

//words/text()

The two opening slashes mean anywhere in the document and the specific search term is the literal words. The text() at the end signals XPath to return the text node in the XML document that contains the new adage; in this case, the phrase:

This is the way the world ends.

The search is flexible in that the XML tag words could be anywhere in the document, in this example nested inside the root element named ns1:foo. Now let me get back to the point about needing to transform a Source into an InputSource. The XPath method evaluate searches an XML document for a pattern such as //words but requires, as a second argument, an InputSource; hence, the transformation of the incoming but generic Source to an InputSource sets up the XPath search. There are other ways in which the XPath search might be supported but any of these would require a transformation of some kind.

The doPut method in the AdagesProvider class is similar in structure to the doPost method because, of course, creating a new Adage (POST) and updating an existing one (PUT) are similar operations. However, the doPut implementation allows only the words of the Adage to be changed; the id property, which the Adages class manages, cannot be changed through a PUT operation.

The JAX-WS @WebServiceProvider is a low-level, XML-centric API. Java is well known for providing options, and this API is among the Java options for delivering REST-style services. Chapter 7 introduces the client-side API, based on the Dispatch interface, for RESTful services implemented with @WebServiceProvider.