A RESTful Web Service as a JAX-RS Resource

The servlet API, the grizzled workhorse for producing Java websites, is still nimble enough to support RESTful web services as well. There are more recent APIs, among them JAX-RS (Java API for XML-RESTful Services). JAX-RS relies upon Java annotations to advertise the RESTful role that a class and its encapsulated methods play. Jersey is the reference implementation (RI) of JAX-RS. RESTEasy, a JBoss project; Apache Wink; and Apache CXF are other implementations. JAX-RS has APIs for programming RESTful services and clients against such services; the two APIs can be used independently. This section focuses on the service-side API. The first JAX-RS example supports only GET requests, but the second JAX-RS example supports all of the CRUD operations.

JAX-RS web services are resources that can be published with the Tomcat and Jetty web servers. The first example has one resource, the class Adages, and two supporting Java classes: the deployment class RestfulApplication and the POJO class Adage. Exactly how these three classes interact is covered next.

The RestfulAdage class (see Example 2-5) extends the JAX-RS Application class (line 2), which implements a getClasses method that enumerates the individual resources deployed in the WAR file (line 3). In this example, there is but one such resource, Adages, but there could be arbitrarily many (line 4).

Recall that any website or web service deployed under Tomcat has a URI that begins with the name of the deployed WAR file. In the RestfulAdage class, the annotation ApplicationPath (line 1) spells out how the URI continues. For example, assuming that the deployed WAR file is named adages.war, the ApplicationPath annotation indicates that the URI part of the URL continues with resourcesA:

http://localhost:8080/adages/resourcesA

The next part is tricky, so the low-level details are explained in a sidebar. At issue is how the programmer-defined RestfulAdage class interacts with the Jersey JAX-RS implementation under a Tomcat deployment. For context, recall that the getClasses method (line 3), a callback invoked when the RestfulAdage instance is loaded into the servlet container, specifies the JAX-RS resources available in the WAR file. Once again, there is but a single resource, Adages (see Example 2-7), in the example. The RestfulAdage class is a Jersey Application because the programmer-defined RestfulAdage class extends the JAX-RS Application class. If multiple JAX-RS resources were to be made available in the deployed WAR file, then the class name of each would occur in a set.add call in RestfulAdage. In the current example, there is only:

set.add(Adages.class);

because Adages is the only resource.

Publishing JAX-RS Resources with Tomcat explains how a JAX-RS resource can be published with a production-grade web server such as Tomcat; the section also explains how the JAX-RS libraries can be downloaded. For now, the point of interest is that the Jersey implementation of JAX-RS offers other ways to publish, which may be better suited for development. Here is a standalone Java application that publishes the adages service:

package adages;

import java.net.InetSocketAddress;
import javax.ws.rs.ext.RuntimeDelegate;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;

public class AdagesPublisher{
    private static final int port = 9876;                                  1
    private static final String uri = "/resourcesA/";                      2
    private static final String url = "http://localhost:" + port + uri;
    public static void main(String[ ] args) {
        new AdagesPublisher().publish();
    }
    private void publish() {
        HttpServer server = getServer();
        HttpHandler requestHandler =
            RuntimeDelegate.getInstance().createEndpoint(new RestfulAdage(),
                                                         HttpHandler.class);
        server.createContext(uri, requestHandler);
        server.start();
        msg(server);
    }
    private HttpServer getServer() {
        HttpServer server = null;
        int backlog = 8;
        try {
            server =
              HttpServer.create(new InetSocketAddress("localhost", port),
                                backlog);
        }
        catch(Exception e) { throw new RuntimeException(e); }
        return server;
    }

    private void msg(HttpServer server) {
        String out = "Publishing RestfulAdage on " + url +
                                                 ". Hit any key to stop.";
        System.out.println(out);
        try {
            System.in.read();
        } catch(Exception e) { }
        server.stop(0); // normal termination
    }
}

For convenience, this AdagesPublisher class is in the adages package together with Adage, Adages, and RestfulAdage. To compile, the JAR file jersey-core.jar must be on the classpath; to run, that file and jersey-server.jar must be on the classpath. The ZIP that contains the sample code has an executable JAR file AdagesPublish.jar that includes all of the dependencies. The JAR can be executed from the command line:

% java -jar AdagesPublish.jar

The AdagesPublisher awaits connections on port 9876 (line 1), and the URI (line 2) is /resourcesA. Accordingly, the base URL is:

http://localhost:9876/resourcesA/

The JAX-RS utility publisher uses classes such as HttpServer and HttpHandler, which come with core Java. Later examples will put these and related classes to use. The point for now is that there are options for publishing JAX-RS services, including a very lightweight option. The adages web service performs the same way regardless of how it is published. The Jersey implementation does a nice job of cleanly separating JAX-RS services from their publication.

The JAX-RS and Jersey packages do not come with the core Java JDK; instead, the relevant JAR files can be found at jersey.java.net. There is a Maven repository from which a Maven script can install Jersey and its dependencies, but the standalone JAR files are available as well. The Maven approach deliberately hides the deployment details to make life easier for the developer. The goal here, however, is to understand how things work under the hood. In any case, working directly with the JARs is straightforward.

JAX-RS resources can be published as usual with the Ant build.xml script. For example, the command to deploy a JAX-RS resource in the WAR file named adages is:

% ant -Dwar.name=adages deploy

As usual, the relevant files would be in a src directory. In this example, the three .java files are in the src/adages subdirectory. The remaining files, including four Jersey JARs, are in src. The relevant JAR files, with approximate sizes, are:

asm.jar             ;;  43K bytes
jersey-core.jar     ;; 206K bytes
jersey-server.jar   ;; 595K bytes
jersey-servlet.jar  ;; 125K bytes

The last three JARs are available, for convenience, in a jersey-bundle.jar.

There are different ways to make these four JARs accessible to Tomcat. The JAR files could be copied to TOMCAT_HOME/lib and thereby be made available to any WAR file deployed under Tomcat. (Recall that Tomcat must be restarted after files are copied to its lib directory in contrast to its webapps directory.) The problem with this approach is version control. Should new versions of the JARs be installed as they come out? If so, will these new versions break already deployed web services? A more conservative approach is to freeze a deployed WAR file by packing the four JARs within the WAR file. This approach also makes it easier to port the WAR from one web server to another, for instance, from Tomcat on one machine to Tomcat on another machine, or from Tomcat to Jetty, and so on. The one downside to packing the JARs inside the WAR is, of course, that the WAR file becomes larger. My preference is to include the required JARs within the WAR file. With this approach, the contents of deployed WAR file adages.war are:

WEB-INF/web.xml
WEB-INF/classes/adages/Adage.class
WEB-INF/classes/adages/Adages.class
WEB-INF/classes/adages/RestfulAdage.class
WEB-INF/lib/asm.jar
WEB-INF/lib/jackson-annotations.jar
WEB-INF/lib/jackson-core.jar
WEB-INF/lib/jackson-databind.jar
WEB-INF/lib/jersey-core.jar
WEB-INF/lib/jersey-server.jar
WEB-INF/lib/jersey-servlet.jar

The three JAR files that begin with jackson handle the generation of JSON documents. Jackson is a collection of Java packages for producing and consuming JSON documents. The main text explains how Jackson works with the rest of the service.

The class adages.RestfulAdage (see Example 2-5) encapsulates a getClasses method, whose role can be clarified with reference to the deployment file web.xml. A JAX-RS service deployed under Tomcat needs a minimalist web.xml to set up communication between the servlet container and the service. Here is an example that can be used with any Jersey JAX-RS service published with Tomcat (or Jetty):

<?xml version="1.0" encoding="UTF-8"?>
<web-app>
  <servlet>
    <servlet-name>jersey</servlet-name>
    <servlet-class>
       com.sun.jersey.spi.container.servlet.ServletContainer
    </servlet-class>
    <load-on-startup>1</load-on-startup>
  </servlet>
</web-app>

The load-on-startup element prompts Tomcat to instantiate and load an instance of the Jersey ServletContainer during the WAR bootstrap process; the critical role of the ServletContainer is to scan the deployed WAR file for Jersey Application classes. Here is a slice of Tomcat’s catalina.out logfile, edited for readability:

INFO: Deploying web application archive adages.war
INFO: Registering Jersey servlet application, named adages.RestfulAdage,  1
      at the servlet mapping, /resources/*, with the Application class
      of the same name
INFO: Scanning for root resource in the Web app resource paths:
INFO: Root resource classes found: class adages.Adages                    2
INFO: Instantiated the Application class adages.RestfulAdage

The upshot of this log segment is that the Jersey ServletContainer finds the class RestfulAdage (line 1), which in turn identifies the JAX-RS resources in the WAR file (line 2). In this case, there is only one such resource: Adages. By the way, if multiple JAX-RS services are deployed to a servlet container, then each service should have a unique name for the class that extends Application. In this first example, the class is named RestfulAdage; in a later example, the name is RestfulPrediction to avoid conflict.

The JAX-RS service in the deployed WAR file, adages.war, is now ready to accept requests such as:

% curl http://localhost:8080/adages/resourcesA/

The Adage class (see Example 2-6) has an import for the JAX-B annotation XmlRootElement. The term binding refers, in this context, to linking a Java data type such as String to an XML type, in this case xsd:string.

The @XmlRootElement annotation (line 1) signals that an Adage object can be transformed into an XML document whose document or root (that is, outermost) element is named adage. For example, the XML document:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<adage>
  <wordCount>7</wordCount>
  <words>What can be shown cannot be said.</words>
</adage>

results from the JAX-B transformation of an in-memory Adage object.

The Adages class (see Example 2-7) is a JAX-RS resource that accepts RESTful requests, in this case only GET requests, and responds with payloads of these three MIME types: text/plain, application/json, and application/xml.

Example 2-7. The Adages class as a JAX-RS resource

package adages;

import javax.xml.bind.annotation.XmlElementDecl;
import javax.xml.bind.JAXBElement;
import javax.xml.namespace.QName;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import java.util.Random;
import com.fasterxml.jackson.databind.ObjectMapper;

@Path("/")
public class Adages {
    // Add aphorisms to taste...
    private String[ ] aphorisms =
       {"What can be shown cannot be said.",
        "If a lion could talk, we could not understand him.",
        "Philosophy is a battle against the bewitchment of " +
        "our intelligence by means of language.",
        "Ambition is the death of thought.",
        "The limits of my language mean the limits of my world."};
    public Adages() { }
    @GET
    @Produces({MediaType.APPLICATION_XML}) // could use "application/xml"
    public JAXBElement<Adage> getXml() {
        return toXml(createAdage());
    }
    @GET
    @Produces({MediaType.APPLICATION_JSON})
    @Path("/json")
    public String getJson() {
        return toJson(createAdage());
    }
    @GET
    @Produces({MediaType.TEXT_PLAIN})
    @Path("/plain")
    public String getPlain() {
        return createAdage().toString() + "\n";
    }
    // Create an Adage and set the words property, which
    // likewise sets the wordCount property. The adage is
    // randomly selected from the array, aphorisms.
    private Adage createAdage() {
        Adage adage = new Adage();
        adage.setWords(aphorisms[new Random().nextInt(aphorisms.length)]);
        return adage;
    }
    // Java Adage --> XML document
    @XmlElementDecl(namespace = "http://aphorism.adage", name = "adage")
    private JAXBElement<Adage> toXml(Adage adage) {
        return new JAXBElement<Adage>(new QName("adage"), Adage.class, adage);
    }
    // Java Adage --> JSON document
    // Jersey provides automatic conversion to JSON using the Jackson
    // libraries. In this example, the conversion is done manually
    // with the Jackson libraries just to indicate how straightforward it is.
    private String toJson(Adage adage) {
        String json = "If you see this, there's a problem.";
        try {
            json = new ObjectMapper().writeValueAsString(adage);
        }
        catch(Exception e) { }
        return json;
    }
}

Perhaps the best way to clarify how the three Java classes interact is through sample client calls. To begin, consider the request:

% curl localhost:8080/adages/resourcesA/plain

On a sample run, the output was:

What can be shown cannot be said. -- 7 words

The RESTful routing of the client’s request works as follows:

The RESTful routing idioms used in JAX-RS follow the spirit, if not the exact syntax, of those from the Rails framework. These idioms support clear, terse URIs such as:

/adages/resourcesA/plain

and:

adages/resourcesA/json

The interaction between the JAX-RS resource class Adages and the POJO class Adage needs clarification. Recall that class Adage begins:

@XmlRootElement(name = "adage")
public class Adage {
...

and that the annotation @XmlRootElement allows an Adage instance to be serialized into an XML document with <adage> as its document-level start tag. In the language of JAX-RS, the Adage class is a provider of XML. (See the How JAX-B Can Transform a Java Object into an XML Document for details about how JAX-B uses an XML Schema to generate the XML.) Adage is likewise a POJO class with the familiar get/set methods for two properties: words and wordCount. The only unusual detail is that the setWords method also sets the wordCount for the adage:

public void setWords(String words) {
   this.words = words;
   this.wordCount = words.trim().split("\\s+").length; // word count
}

because this is a convenient way to do so.

The Adages resource has three methods that define the web service operations: getJson, getPlain, and getXml. The operation names are arbitrary. The important routing information for each operation comes from the annotations that describe the HTTP verb (in this case, only GET) and the @Path. The getXml operation has no @Path annotation, which means that the path for the resource, the Adages class, is the path for this operation; the path is /adages/resourcesA/. In effect, getXml is the default operation.

The getJson and getXml operations could be combined into a single operation:

@GET
@Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON})
...

because Jersey can coordinate directly with the Jackson libraries to process JSON. My implementation uses Jackson explicitly to show just how simple the API is. Further, if the two operations were combined into one, then a client would have to disambiguate the request by adding the HTTP header:

Accept: application/json

to the HTTP request. It seems cleaner to use two different URIs: /adages/resourcesA/ maps to the default getXml operation, whereas /adages/resourcesA/json maps to the getJson operation. Here for review is the utility method that getJson calls to produce the JSON:

private String toJson(Adage adage) {
   String json = "If you see this, there's a problem.";
   try {
      json = new ObjectMapper().writeValueAsString(adage); 1
   } catch(Exception e) { }
   return json;
}

The Jackson ObjectMapper encapsulates the method writeValueAsString (line 1), which serializes an Adage into a JSON document. The response for a sample request against the toJson operation, formatted for readability, would look like this:

{"words":    "The limits of my language mean the limits of my world.",
 "wordCount":11
}

Similar serialization occurs with respect to an Adage converted into an XML document. The default operation getXml:

@GET
@Produces({MediaType.APPLICATION_XML}) // could use "application/xml" instead
public JAXBElement<Adage> getXml() {
   return toXml(createAdage());
}

returns a JAXBElement<Adage>, an XML document that represents an Adage. Under the hood the JAX-B processor converts an Adage instance into an XML document. On a sample run the output was:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<adage>
  <wordCount>10</wordCount>
  <words>If a lion could talk, we could not understand him.</words>
</adage>

The POJO class Adage currently has but one annotation, @XmlRootElement. A variety of others could be used to refine the XML output. Here is a sample refinement:

package adages;
...
@XmlRootElement(name = "adage")
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(propOrder = {"words", "wordCount"})  1
public class Adage {
    @XmlElement(required = true)
    protected String words;
    @XmlElement(required = true)
    protected int wordCount;
    ...

The @XmlType (line 1) is particularly useful if the order of elements in the generated XML document matters. In the current implementation, the wordCount element precedes the words element, but this order could be reversed through the propOrder attribute in the @XmlType annotation (line 1).

This first JAX-RS example illustrates the style of implementing a RESTful web service as a JAX-RS resource. The deployment under Tomcat is uncomplicated, and the adages.war file also can be deployed, as is, under Jetty. The adages service supports only GET requests. The forthcoming adages2 service, implemented as a set of Restlet resources, supports all of the CRUD operations. The next section also shows, with a different example, how JAX-RS nicely supports all of the CRUD operations.

The servlet-based predictions2 service supports the four CRUD operations; hence, the port from the servlet/JSP implementations to JAX-RS is an opportunity to show the @POST, @PUT, and @DELETE annotations and to illustrate parametrized versions of the @GET and @DELETE operations. This revision highlights again the JAX-RS idioms for RESTful URIs. The revised service is called predictions3.

The JAX-RS predictions3 service has four Java classes:

The RestfulPrediction class (see Example 2-8) is the JAX-RS Application class. To ensure that the adages JAX-RS service and this JAX-RS service can coexist in the same servlet container, the names of the two Application classes must differ: in the case of adages, the Application class is RestfulAdage; in this case, the Application class is RestfulPrediction.

The backend support for the PredictionsRS source consists of two POJO classes: Prediction (see Example 2-9) and PredictionsList (see Example 2-10). The class Prediction is mostly unchanged from the predictions2 version except for the added @XmlRootElement annotation, which means that the runtime can automatically convert a Prediction instance into an XML document. Details follow shortly.

The PredictionsList POJO class (see Example 2-10) in the predictions3 service is simpler overall than the Predictions from class of predictions2 because methods such as populate have moved into the core JAX-RS class. In any case, the PredictionsList class has a find method to search for a particular Prediction, and the data structure used to store the predictions is now a thread-safe CopyOnWriteArrayList.

The PredictionsRS class (see Example 2-11) is the JAX-RS resource with annotations that define the CRUD operations. The class is long enough that inspecting the code in chunks may be helpful.

Example 2-11. The JAX-RS resource PredictionsRS

package predictions3;

import java.io.InputStream;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.DELETE;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.FormParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Context;
import javax.servlet.ServletContext;
import com.fasterxml.jackson.databind.ObjectMapper;

@Path("/")
public class PredictionsRS {
    @Context
    private ServletContext sctx;          // dependency injection
    private static PredictionsList plist; // set in populate()

    public PredictionsRS() { }

    @GET
    @Path("/xml")
    @Produces({MediaType.APPLICATION_XML})
    public Response getXml() {
        checkContext();
        return Response.ok(plist, "application/xml").build();
    }
    @GET
    @Path("/xml/{id: \\d+}")
    @Produces({MediaType.APPLICATION_XML}) // could use "application/xml" instead
    public Response getXml(@PathParam("id") int id) {
        checkContext();
        return toRequestedType(id, "application/xml");
    }
    @GET
    @Produces({MediaType.APPLICATION_JSON})
    @Path("/json")
    public Response getJson() {
        checkContext();
        return Response.ok(toJson(plist), "application/json").build();
    }
    @GET
    @Produces({MediaType.APPLICATION_JSON})
    @Path("/json/{id: \\d+}")
    public Response getJson(@PathParam("id") int id) {
        checkContext();
        return toRequestedType(id, "application/json");
    }
    @GET
    @Path("/plain")
    @Produces({MediaType.TEXT_PLAIN})
    public String getPlain() {
        checkContext();
        return plist.toString();
    }
    @POST
    @Produces({MediaType.TEXT_PLAIN})
    @Path("/create")
    public Response create(@FormParam("who") String who,
                           @FormParam("what") String what) {
        checkContext();
        String msg = null;
        // Require both properties to create.
        if (who == null || what == null) {
            msg = "Property 'who' or 'what' is missing.\n";
            return Response.status(Response.Status.BAD_REQUEST).
                                                   entity(msg).
                                                   type(MediaType.TEXT_PLAIN).
                                                   build();
        }
        // Otherwise, create the Prediction and add it to the collection.
        int id = addPrediction(who, what);
        msg = "Prediction " + id +
              " created: (who = " + who + " what = " + what + ").\n";
        return Response.ok(msg, "text/plain").build();
    }
    @PUT
    @Produces({MediaType.TEXT_PLAIN})
    @Path("/update")
    public Response update(@FormParam("id") int id,
                           @FormParam("who") String who,
                           @FormParam("what") String what) {
        checkContext();
        // Check that sufficient data is present to do an edit.
        String msg = null;
        if (who == null && what == null)
            msg = "Neither who nor what is given: nothing to edit.\n";
        Prediction p = plist.find(id);
        if (p == null)
            msg = "There is no prediction with ID " + id + "\n";

        if (msg != null)
            return Response.status(Response.Status.BAD_REQUEST).
                                                   entity(msg).
                                                   type(MediaType.TEXT_PLAIN).
                                                   build();
        // Update.
        if (who != null) p.setWho(who);
        if (what != null) p.setWhat(what);
        msg = "Prediction " + id + " has been updated.\n";
        return Response.ok(msg, "text/plain").build();
    }
    @DELETE
    @Produces({MediaType.TEXT_PLAIN})
    @Path("/delete/{id: \\d+}")
    public Response delete(@PathParam("id") int id) {
        checkContext();
        String msg = null;
        Prediction p = plist.find(id);
        if (p == null) {
            msg = "There is no prediction with ID " + id + ". Cannot delete.\n";
            return Response.status(Response.Status.BAD_REQUEST).
                                                   entity(msg).
                                                   type(MediaType.TEXT_PLAIN).
                                                   build();
        }
        plist.getPredictions().remove(p);
        msg = "Prediction " + id + " deleted.\n";
        return Response.ok(msg, "text/plain").build();
    }
    private void checkContext() {
        if (plist == null) populate();
    }
    private void populate() {
        plist = new PredictionsList();
        String filename = "/WEB-INF/data/predictions.db";
        InputStream in = sctx.getResourceAsStream(filename);
        // Read the data into the array of Predictions.
        if (in != null) {
            try {
                BufferedReader reader =
                    new BufferedReader(new InputStreamReader(in));
                int i = 0;
                String record = null;
                while ((record = reader.readLine()) != null) {
                    String[] parts = record.split("!");
                    addPrediction(parts[0], parts[1]);
                }
            }
            catch (Exception e) {
                throw new RuntimeException("I/O failed!");
            }
        }
    }
    private int addPrediction(String who, String what) {
        int id = plist.add(who, what);
        return id;
    }
    // Prediction --> JSON document
    private String toJson(Prediction prediction) {
        String json = "If you see this, there's a problem.";
        try {
            json = new ObjectMapper().writeValueAsString(prediction);
        }
        catch(Exception e) { }
        return json;
    }
    // PredictionsList --> JSON document
    private String toJson(PredictionsList plist) {
        String json = "If you see this, there's a problem.";
        try {
            json = new ObjectMapper().writeValueAsString(plist);
        }
        catch(Exception e) { }
        return json;
    }
    // Generate an HTTP error response or typed OK response.
    private Response toRequestedType(int id, String type) {
        Prediction pred = plist.find(id);
        if (pred == null) {
            String msg = id + " is a bad ID.\n";
            return Response.status(Response.Status.BAD_REQUEST).
                                                   entity(msg).
                                                   type(MediaType.TEXT_PLAIN).
                                                   build();
        }
        else if (type.contains("json"))
            return Response.ok(toJson(pred), type).build();
        else
            return Response.ok(pred, type).build(); // toXml is automatic
    }
}

A summary of the major parts of this class follows:

The predictions3 service still has a toJson utility method to convert one Prediction or a collection of these into JSON. This is a design decision, not a necessity. The JAX-RS runtime also generates JSON automatically if the relevant Jackson libraries are included and if the HTTP request contains the header element Accept: application/json. The conversion to JSON is simple enough that predictions3 does it manually, thereby sparing the client the responsibility of adding a specific header element to the HTTP request.

The request pattern in the predictions3 service is uniform as there is no default URI—that is, a URI consisting solely of the slash (/). A request for an XML document ends with /xml for all predictions in XML or, for instance, /xml/7 to get prediction 7 in XML; a request for JSON ends with /json or, for example, /json/13; and a request for plain text ends with /plain. The JAX-RS patterns for URIs can adhere to the Rails URI patterns, now widely imitated, as closely as the programmer likes.



[1] In core Java 8, the functionality of the schemagen utility will give way to general annotation processing through javac.