Red Hat
Oct 27, 2011
by Heiko Rupp

[update] In the earlier version I had if (mediaType==MediaType.xxx), which is wrong. MediaType is no enum, so we need to compare via .equals()[/update]

 

So in RHQ we've introduced the REST api and one of the nice things is the ease of use like this:

  @GET
@Path("/customer/{id}")
public Customer getCustomer(@PathParam("id") int custId) ;

where the implementation code e.g. looks up a Customer object in  the database and then returns it. Clean expressive API.

Now things start getting more interesting when you consider returning XML, JSON and HTML versions of the document. The first naive way goes like this:

  @GET
@Path("/customer/{id}")
public Customer getCustomer(@PathParam("id") int custId) ;

Which fails big time for HTML as (at least RESTEasy) does not know how to serialize the Customer object into HTML. So the next version of the API could look like this:

  @GET
@Path("/customer/{id}")
@Produces({"application/json","application/xml"})
public Customer getCustomer(@PathParam("id") int custId) ;

and

 @GET
@Path("/customer/{id}")
@Produces("text/html")
public String getCustomerHtml(@PathParam("id") int custId) ;

Where both methods get the customer from the backend. The first one then returns the object directly (as above) whereas the second one renders the object with the help of Freemarker templates into a HTML string and returns this one. Just returning String in both cases is also not working as one may think. So this two methods that internally use almost the same code (actually the *Html one calls the other to retrieve the Customer object).

This works quite nicely, but now I want to start adding explicit return codes (and also caching information). JAX-RS offers the Response class for this, where you can e.g. say Response.ok() or Response.notFound(). So the interface changes to

  @GET
@Path("/customer/{id}")
@Produces({"application/json","application/xml"})
public Response getCustomer(@PathParam("id") int custId) ;

and

 @GET
@Path("/customer/{id}")
@Produces("text/html")
public Response getCustomerHtml(@PathParam("id") int custId);

Internally we have the same situation as before, but can now explicitly return the result codes we want. This is still not optimal. Luckily JAX-RS allows to inject the Request and the HttpHeaders into the called Java-method, so that we can now write

  @GET   
@Path("/customer/{id}")   
@Produces({"application/json","application/xml","text/html"})   
Response getCustomer(@PathParam("id") int custId,
@Context HttpHeaders headers);

and then in the implementation do the following:

  MediaType mediaType = headers.getAcceptableMediaTypes().get(0);
  ResponseBuilder builder = ...
  if (mediaType.equals(MediaType.TEXT_HTML_TYPE)) {
String html = renderTemplate("customer", customer);
     builder = Response.ok(html, mediaType);
}
else {
builder = Response.ok(customer);
}

So now we have one method doing all the work for us without code duplication and which can then use the same logic for caching and e.g. paging or linking.

Support for caching is now only one more step away:

  @GET   
@Path("/customer/{id}")   
@Produces({"application/json","application/xml","text/html"})   
Response getCustomer(@PathParam("id") int custId,
@Context Request request,
@Context HttpHeaders headers);

and in the implementation

  // Check for conditional get
  String tagString = Integer.toString(customer.hashCode());
EntityTag eTag = new EntityTag(tagString);       
Date lastModifiedInDb = new Date(customer.getMtime();

Response.ResponseBuilder builder = request.
evaluatePreconditions(lastModifiedInDb,eTag);
  if (builder==null ) { 
    // we need to send the full resource
if (mediaType.equals(MediaType.TEXT_HTML_TYPE)) {
String html = renderTemplate("customer", customer);
     builder = Response.ok(html, mediaType);
} else {
builder = Response.ok(customer);
}
  builder.tag(eTag); // Set ETag on response

There is of course still a lot to do, but we have now achieved :

  • uniform interface independent of the media type
  • support for conditional sending and thus caching and re-validation by ETag and time stamp

The last step is to add some hints to the client how long it may cache data without the need to go out to the network to do any caching at all. Again JAX-RS has already support for that:

  // Create a cache control       
CacheControl cc = new CacheControl();       
cc.setMaxAge(300); // Customer objects are valid for 5 mins       
cc.setPrivate(false); // Proxies may cache this
builder.cacheControl(cc);
 
This gives the client a hint, that they can consider the customer object valid for 300s = 5min. Proxies on the way are also allowed to cache the returned object. In practice one may make the maxAge depending on e.g. some average update frequencies and also set the "you need to always verify" flag via cc.setMustRevalidate(true).

I am sure, this is not yet the last version of the interface, but you can see how it can evolve over time and add new features like support for conditional get. And the best part is that so far the clients don't even have to change a single line of code.

In the future we may want to introduce our own media types like appliation/vnd.rhq-customer+json that newer clients then can make use of. The server can dispatch as seen above the media type and return the appropriate representation. The existing clients, that do not know the new media type can still be serviced by the server just sending this "old" version of it.