[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.
Hello,
ReplyDeleteHave you tried to use a MessageBodyWriter matching the returned mediatype + entity type to render the HTML ?
Something like this:
@Provider
@Produces("text/html")
public class CustomerHTMLWriter implements MessageBodyWriter {
public long getSize(Customer c, Class type, Type genericType,
Annotation[] annotations, MediaType mediaType) {
return -1;
}
public boolean isWriteable(Class type, Type genericType,
Annotation[] annotations, MediaType mediaType) {
return true;
}
public void writeTo(Customer customer, Class type,
Type genericType, Annotation[] annotations, MediaType mediaType,
MultivaluedMap httpHeaders,
OutputStream entityStream) throws IOException,
WebApplicationException {
OutputStreamWriter writer = new OutputStreamWriter(entityStream,
"UTF-8");
String html = renderTemplate("customer", customer);
writer.write(html);
writer.close();
}
}
Xavier, not yet, but I like the idea - I guess the tick to do that in a generic (transparent way) would be to evaluate the object passed in into writeTo() and to use this in conjunction with he media type to select the template for rendering?
ReplyDeleteIn a scenario with many types of resource I'd end up encoding either a huge "switch" statement for template selection? Or even creating many MessageBodyWriter(s)?
Do you have some experience with such a generic version?
Thanks
Heiko
Hi Heiko,
ReplyDeleteThe isWritable method is called before the writeTo() method (if isWritable() returns false, the writeTo() one is not called).
Also, the MessageBodyWriter is called if the returned mediatype matches the @Provides annotation, so this should limit the amount of switches/test in your writeTo() method.
Bu indeed, you'd probably end up with having to code as many MessageBodyWriters as you have Entity types..
Best regards,
Xavier