Examples for Chapter 10 - JAX-RS Workbook - RESTful Java with JAX-RS 2.0 (2013)

RESTful Java with JAX-RS 2.0 (2013)

Part II. JAX-RS Workbook

Chapter 24. Examples for Chapter 10

In Chapter 10, you learned about many of the concepts of HATEOAS and how to use JAX-RS to add these principles to your RESTful web services. In this chapter, you’ll look through two different examples. The first shows you how to introduce Atom links into your XML documents. The second uses Link headers to publish state transitions within a RESTful web service application.

Example ex10_1: Atom Links

This example is a slight modification of the ex06_1 example introduced in Chapter 21. It expands the CustomerResource RESTful web service so that a client can fetch subsets of the customer database. If a client does a GET /customers request in our RESTful application, it will receive a subset list of customers in XML. Two Atom links are embedded in this document that allow you to view the next or previous sets of customer data. Example output would be:

<customers>
   <customer id="3">
     ...
   </customer>
   <customer id="4">
      ...
   </customer>
   <link rel="next"
         href="http://example.com/customers?start=5&size=2"
         type="application/xml"/>
   <link rel="previous"
         href="http://example.com/customers?start=1&size=2"
         type="application/xml"/>
</customers>

The next and previous links are URLs pointing to the same /customers URL, but they contain URI query parameters indexing into the customer database.

The Server Code

The first bit of code is a JAXB class that maps to the <customers> element. It must be capable of holding an arbitrary number of Customer instances as well as the Atom links for our next and previous link relationships. We can use the javax.ws.rs.core.Link class with its JAXB adapter to represent these links:

src/main/java/com/restfully/shop/domain/Customers.java

import javax.ws.rs.core.Link;
...
 
@XmlRootElement(name = "customers")
public class Customers
{
   protected Collection<Customer> customers;
   protected List<Link> links;
 
   @XmlElementRef
   public Collection<Customer> getCustomers()
   {
      return customers;
   }
 
   public void setCustomers(Collection<Customer> customers)
   {
      this.customers = customers;
   }
 
   @XmlElement(name="link")
   @XmlJavaTypeAdapter(Link.JaxbAdapter.class)
   public List<Link> getLinks()
   {
      return links;
   }
 
   public void setLinks(List<Link> links)
   {
      this.links = links;
   }
 
   @XmlTransient
   public URI getNext()
   {
      if (links == null) return null;
      for (Link link : links)
      {
         if ("next".equals(link.getRel())) return link.getUri();
      }
      return null;
   }
 
   @XmlTransient
   public URI getPrevious()
   {
      if (links == null) return null;
      for (Link link : links)
      {
         if ("previous".equals(link.getRel())) return link.getUri();
      }
      return null;
   }
 
}

There is no nice way to define a map in JAXB, so all the Atom links are stuffed within a collection property in Customers. The convenience methods getPrevious() and getNext() iterate through this collection to find the next and previous Atom links embedded within the document if they exist.

The final difference from the ex06_1 example is the implementation of GET /customers handling:

src/main/java/com/restfully/shop/services/CustomerResource.java

@Path("/customers")
public class CustomerResource
{
   @GET
   @Produces("application/xml")
   @Formatted
   public Customers getCustomers(@QueryParam("start") int start,
                   @QueryParam("size") @DefaultValue("2") int size,
                   @Context UriInfo uriInfo)
   {

The @org.jboss.resteasy.annotations.providers.jaxb.Formatted annotation is a RESTEasy-specific plug-in that formats the XML output returned to the client to include indentations and new lines so that the text is easier to read.

The query parameters for the getCustomers() method, start and size, are optional. They represent an index into the customer database and how many customers you want returned by the invocation. The @DefaultValue annotation is used to define a default page size of 2.

The UriInfo instance injected with @Context is used to build the URLs that define next and previous link relationships.

      UriBuilder builder = uriInfo.getAbsolutePathBuilder();
      builder.queryParam("start", "{start}");
      builder.queryParam("size", "{size}");

Here, the code defines a URI template by using the UriBuilder passed back from UriInfo.getAbsolutePathBuilder(). The start and size query parameters are added to the template. Their values are populated using template parameters later on when the actual links are built.

      ArrayList<Customer> list = new ArrayList<Customer>();
      ArrayList<Link> links = new ArrayList<Link>();
      synchronized (customerDB)
      {
         int i = 0;
         for (Customer customer : customerDB.values())
         {
            if (i >= start && i < start + size)
                                      list.add(customer);
            i++;
         }

The code then gathers up the Customer instances that will be returned to the client based on the start and size parameters. All this code is done within a synchronized block to protect against concurrent access on the customerDB map.

         // next link
         if (start + size < customerDB.size())
         {
            int next = start + size;
            URI nextUri = builder.clone().build(next, size);
            Link nextLink = Link.fromUri(nextUri)
                                .rel("next").type("application/xml").build();
            links.add(nextLink);
         }
         // previous link
         if (start > 0)
         {
            int previous = start - size;
            if (previous < 0) previous = 0;
            URI previousUri = builder.clone().build(previous, size);
             Link previousLink = Link.fromUri(previousUri)
                                     .rel("previous")
                                     .type("application/xml").build();
            links.add(previousLink);
         }

If there are more possible customer instances left to be viewed, a next link relationship is calculated using the UriBuilder template defined earlier. A similar calculation is done to see if a previous link relationship needs to be added to the document.

      }
      Customers customers = new Customers();
      customers.setCustomers(list);
      customers.setLinks(links);
      return customers;
   }

Finally, a Customers instance is created and initialized with the Customer list and link relationships and returned to the client.

The Client Code

The client initially gets the XML document from the /customers URL. It then loops using the next link relationship as the URL to print out all the customers in the database:

public class CustomerResourceTest
{
   @Test
   public void testQueryCustomers() throws Exception
   {
      URI uri = new URI("http://localhost:8080/services/customers");
      while (uri != null)
      {
         WebTarget target = client.target(uri);
         String output = target.request().get(String.class);
         System.out.println("** XML from " + uri.toString());
         System.out.println(output);
 
         Customers customers = target.request().get(Customers.class);
         uri = customers.getNext();
      }
   }
}

An interesting thing to note about this is that the server is guiding the client to make state transitions as it browses the customer database. Once the initial URL is invoked, further queries are solely driven by Atom links.

Build and Run the Example Program

Perform the following steps:

1. Open a command prompt or shell terminal and change to the ex10_1 directory of the workbook example code.

2. Make sure your PATH is set up to include both the JDK and Maven, as described in Chapter 17.

3. Perform the build and run the example by typing maven install.

Example ex10_2: Link Headers

There are two educational goals I want to get across with this example. The first is the use of Link headers within a RESTful application. The second is that if your services provide the appropriate links, you only need one published URL to navigate through your system. When you look at the client code for this example, you’ll see that only one URL is hardcoded to start the whole process of the example.

To illustrate these techniques, a few more additional JAX-RS services were built beyond the simple customer database example that has been repeated so many times throughout this book. Chapter 2 discussed the design of an ecommerce application. This chapter starts the process of implementing this application by introducing an order-entry RESTful service.

The Server Code

The Order and LineItem classes are added to the JAXB domain model. They are used to marshal the XML that represents order entries in the system. They are not that interesting, so I’m not going to get into much detail here.

OrderResource

The OrderResource class is used to create, post, and cancel orders in our ecommerce system. The purge operation is also available to destroy any leftover order entries that have been cancelled but not removed from the order entry database. Let’s look:

src/main/java/com/restfully/shop/services/OrderResource.java

@Path("/orders")
public class OrderResource
{
   private Map<Integer, Order> orderDB =
                                     new Hashtable<Integer, Order>();
   private AtomicInteger idCounter = new AtomicInteger();
 
   @POST
   @Consumes("application/xml")
   public Response createOrder(Order order, @Context UriInfo uriInfo)
   {
      order.setId(idCounter.incrementAndGet());
      orderDB.put(order.getId(), order);
      System.out.println("Created order " + order.getId());
      UriBuilder builder = uriInfo.getAbsolutePathBuilder();
      builder.path(Integer.toString(order.getId()));
      return Response.created(builder.build()).build();
 
   }

The createOrder() method handles POST /orders requests. It generates new Order IDs and adds the posted Order instance into the order database (the map). The UriInfo.getAbsolutePathBuilder() method generates the URL used to initialize the Location header returned by the Response.created() method. You’ll see later that the client uses this URL to further manipulate the created order.

   @GET
   @Path("{id}")
   @Produces("application/xml")
   public Response getOrder(@PathParam("id") int id,
                              @Context UriInfo uriInfo)
   {
      Order order = orderDB.get(id);
      if (order == null)
      {
        throw new WebApplicationException(Response.Status.NOT_FOUND);
      }
      Response.ResponseBuilder builder = Response.ok(order);
      if (!order.isCancelled()) addCancelHeader(uriInfo, builder);
      return builder.build();
   }

The getOrder() method processes GET /orders/{id} requests and retrieves individual orders from the database (the map). If the order has not been cancelled already, a cancel Link header is added to the Response so the client knows if an order can be cancelled and which URL to post a cancel request to:

   protected void addCancelHeader(UriInfo uriInfo,
                                    Response.ResponseBuilder builder)
   {
      UriBuilder absolute = uriInfo.getAbsolutePathBuilder();
      URI cancelUrl = absolute.clone().path("cancel").build();
      builder.links(Link.fromUri(cancelUrl).rel("cancel").build());
   }

The addCancelHeader() method creates a Link object for the cancel relationship using a URL generated from UriInfo.getAbsolutePathBuilder().

   @HEAD
   @Path("{id}")
   @Produces("application/xml")
   public Response getOrderHeaders(@PathParam("id") int id,
                                    @Context UriInfo uriInfo)
   {
      Order order = orderDB.get(id);
      if (order == null)
      {
        throw new WebApplicationException(Response.Status.NOT_FOUND);
      }
      Response.ResponseBuilder builder = Response.ok();
      builder.type("application/xml");
      if (!order.isCancelled()) addCancelHeader(uriInfo, builder);
      return builder.build();
   }

The getOrderHeaders() method processes HTTP HEAD /orders/{id} requests. This is a convenience operation for HTTP clients that want the link relationships published by the resource but don’t want to have to parse an XML document to get this information. Here, thegetOrderHeaders() method returns the cancel Link header with an empty response body:

   @POST
   @Path("{id}/cancel")
   public void cancelOrder(@PathParam("id") int id)
   {
      Order order = orderDB.get(id);
      if (order == null)
      {
         throw new WebApplicationException(Response.Status.NOT_FOUND);
      }
      order.setCancelled(true);
   }

Users can cancel an order by posting an empty message to /orders/{id}/cancel. The cancelOrder() method handles these requests and simply looks up the Order in the database and sets its state to cancelled.

   @GET
   @Produces("application/xml")
   @Formatted
   public Response getOrders(@QueryParam("start") int start,
                     @QueryParam("size") @DefaultValue("2") int size,
                    @Context UriInfo uriInfo)
   {
...
      Orders orders = new Orders();
      orders.setOrders(list);
      orders.setLinks(links);
      Response.ResponseBuilder responseBuilder = Response.ok(orders);
      addPurgeLinkHeader(uriInfo, responseBuilder);
      return responseBuilder.build();
   }

The getOrders() method is similar to the CustomerResource.getCustomers() method discussed in the ex10_1 example, so I won’t go into a lot of details. One thing it does differently, though, is to publish a purge link relationship through a Link header. Posting to this link allows clients to purge the order entry database of any lingering cancelled orders:

   protected void addPurgeLinkHeader(UriInfo uriInfo,
                                     Response.ResponseBuilder builder)
   {
      UriBuilder absolute = uriInfo.getAbsolutePathBuilder();
      URI purgeUri = absolute.clone().path("purge").build();
      builder.links(Link.fromUri(purgeUri).rel("purge").build());
   }

The addPurgeLinkHeader() method creates a Link object for the purge relationship using a URL generated from UriInfo.getAbsolutePathBuilder().

   @HEAD
   @Produces("application/xml")
   public Response getOrdersHeaders(@QueryParam("start") int start,
                     @QueryParam("size") @DefaultValue("2") int size,
                    @Context UriInfo uriInfo)
   {
      Response.ResponseBuilder builder = Response.ok();
      builder.type("application/xml");
      addPurgeLinkHeader(uriInfo, builder);
      return builder.build();
   }

The getOrdersHeaders() method is another convenience method for clients that are interested only in the link relationships provided by the resource:

   @POST
   @Path("purge")
   public void purgeOrders()
   {
      synchronized (orderDB)
      {
         List<Order> orders = new ArrayList<Order>();
         orders.addAll(orderDB.values());
         for (Order order : orders)
         {
            if (order.isCancelled())
            {
               orderDB.remove(order.getId());
            }
         }
      }
   }

Finally, the purgeOrders() method implements the purging of cancelled orders.

StoreResource

One of the things I want to illustrate with this example is that a client needs to be aware of only one URL to navigate through the entire system. The StoreResource class is the base URL of the system and publishes Link headers to the relevant services of the application:

src/main/java/com/restfully/shop/services/StoreResource.java

@Path("/shop")
public class StoreResource
{
   @HEAD
   public Response head(@Context UriInfo uriInfo)
   {
      UriBuilder absolute = uriInfo.getBaseUriBuilder();
      URI customerUrl = absolute.clone().path(CustomerResource.class).build();
      URI orderUrl = absolute.clone().path(OrderResource.class).build();
 
      Response.ResponseBuilder builder = Response.ok();
      Link customers = Link.fromUri(customerUrl)
                           .rel("customers")
                           .type("application/xml").build();
      Link orders = Link.fromUri(orderUrl)
                        .rel("orders")
                        .type("application/xml").build();
      builder.links(customers, orders);
      return builder.build();
   }
}

This class accepts HTTP HEAD /shop requests and publishes the customers and orders link relationships. These links point to the services represented by the CustomerResource and OrderResource classes.

The Client Code

The client code creates a new customer and order. It then cancels the order, purges it, and, finally, relists the order entry database. All URLs are accessed via Link headers or Atom links:

public class OrderResourceTest
{
   @Test
   public void testCreateCancelPurge() throws Exception
   {
      String base = "http://localhost:8080/services/shop";
      Response response = client.target(base).request().head();
 
      Link customers = response.getLink("customers");
      Link orders = response.getLink("orders");
      response.close();

The testCreateCancelPurge() method starts off by doing a HEAD request to /shop to obtain the service links provided by our application. The Response.getLink() method allows you to query for a Link header sent back with the HTTP response.

      System.out.println("** Create a customer through this URL: "
                            + customers.getHref());
 
      Customer customer = new Customer();
      customer.setFirstName("Bill");
      customer.setLastName("Burke");
      customer.setStreet("10 Somewhere Street");
      customer.setCity("Westford");
      customer.setState("MA");
      customer.setZip("01711");
      customer.setCountry("USA");
 
      response = client.target(customers).request().post(Entity.xml(customer));
      Assert.assertEquals(201, response.getStatus());
      response.close();

We create a customer in the customer database by POSTing an XML representation to the URL referenced in the customers link relationship. This relationship is retrieved from our initial HEAD request to /shop.

      Order order = new Order();
      order.setTotal("$199.99");
      order.setCustomer(customer);
      order.setDate(new Date().toString());
      LineItem item = new LineItem();
      item.setCost("$199.99");
      item.setProduct("iPhone");
      order.setLineItems(new ArrayList<LineItem>());
      order.getLineItems().add(item);
 
      System.out.println();
      System.out.println("** Create an order through this URL: "
                                           + orders.getUri().toString());
      response = client.target(orders).request().post(Entity.xml(order));
      Assert.assertEquals(201, response.getStatus());
      URI createdOrderUrl = response.getLocation();
      response.close();

Next, we create an order entry by posting to the orders link relationship. The URL of the created order is extracted from the returned Location header. We will need this later when we want to cancel this order:

      System.out.println();
      System.out.println("** New list of orders");
      response = client.target(orders).request().get();
      String orderList = response.readEntity(String.class);
      System.out.println(orderList);
      Link purge = response.getLink("purge");
      response.close();

A GET /orders request is initiated to show all the orders posted to the system. We extract the purge link returned by this invocation so it can be used later when the client wants to purge cancelled orders:

      response = client.target(createdOrderUrl).request().head();
      Link cancel = response.getLink("cancel");
      response.close();

Next, the client cancels the order that was created earlier. A HEAD request is made to the created order’s URL to obtain the cancel link relationship:

      if (cancel != null)
      {
         System.out.println("** Cancelling the order at URL: "
                                    + cancel.getUri().toString());
         response = client.target(cancel).request().post(null);
         Assert.assertEquals(204, response.getStatus());
         response.close();
      }

If there is a cancel link relationship, the client posts an empty message to this URL to cancel the order:

      System.out.println();
      System.out.println("** New list of orders after cancel: ");
      orderList = client.target(orders).request().get(String.class);
      System.out.println(orderList);

The client does another GET /orders to show that the state of our created order was set to cancelled:

      System.out.println();
      System.out.println("** Purge cancelled orders at URL: "
                                      + purge.getUri().toString());
      response = client.target(purge).request().post(null);
      Assert.assertEquals(204, response.getStatus());
      response.close();
 
      System.out.println();
      System.out.println("** New list of orders after purge: ");
      orderList = client.target(orders).request().get(String.class);
      System.out.println(orderList);
   }

Finally, by posting an empty message to the purge link, the client cleans the order entry database of any cancelled orders.

Build and Run the Example Program

Perform the following steps:

1. Open a command prompt or shell terminal and change to the ex10_2 directory of the workbook example code.

2. Make sure your PATH is set up to include both the JDK and Maven, as described in Chapter 17.

3. Perform the build and run the example by typing maven install.