Tuesday, November 9, 2010

Evolve your REST representation with a MessageBodyWriter

THIS PAGE IS UNDER CONSTRUCTION... this is here to get the right scripts and layout in place. Thanks....

A less known component of the JAX-RS spec is the MessageBodyWriter. It’s a pity because it’s one of the most important building blocks if you plan on evolving your representation of your REST resources. A MessageBodyWriter will also keep the representation logic out of your annotated web-resource classes. 

Build in support

A JAX-RS implementation already supports a number of conversions from types to different representation. The classes responsible for the conversions are all implementent as MessageBodyWriters and Readers. Here are a few: 

  • byte[]
  • java.lang.String
  • javax.xml.transform.Source
  • javax.xml.bind.JAXBElement
  • MultivaluedMap
  • ...

It’s already quite a rich set of types that are supported by default. On simple REST interfaces this will probably suffice. The support for JAXB is especially popular for creating XML representations. An DTO/JAXB implementation could look like this:

@GET
@Path("companies/{companyId}")
@Produces("application/xml")
public XmlCompany getCompany(@PathParam("companyId") id ) {
  return XmlAdapter.toXml(companies.getId(companyId));
}

@POST
@Path("companies")
@Consumes("application/xml")
@Produces("application/xml")
public Response createCompany(@Context UriInfo uriInfo,XmlCompany jaxbCompany) {
  Company company = XMLAdapter.fromXML(jaxbCompany);
  company = service.create(company);
  ResponseBuilder responseBuilder = Response.created(uriInfo.getAbsolutePath().resolve(company.getID));
  return responseBuilder.entity(xmlProfile).build();
}

But if your interface evolves, so will your representation. At some point in time you will even need to support more then one representation, because you have customers still rely on the old.

The REST style

 Before looking at the implementation we should have a mechanism to make a distinction between the different representations. This can be done by the putting our version number in the MIME type. MIME types allow for a vendor namespace where we can create all our private representations. This is how the MIME type could look like:

application/vnd.vanboxel.labs.app.v1.company+xml

You are free to define the structure between the application/vnd. and the +xml. Here I’ve created my private namespace vanboxel.labs for my app application, version v1 and an entity of type company. You can be even more fine grained and version your individual entities as well, but this adds to the complexity. And finally don’t forget to finish your MIME type with the real format, this could be +xml, +json or something else.

A reflex is now to add new methods to handle the new conversions, and add the MIME type to the @Consumers and @Produces annotation, in your resource class. But remember that by doing this you will be multiplying the methods by the number of representations. This will just add clutter to the classes. You should keep the representation logic out of the resource classes. Let them handle the path’s, caching, e-tag’s, etc…

MessageBodyWriter/Reader

What you could do is write a MessageBodyWriter that handles the conversion to different representations (be it format or version). Writing one is easy, you need to implement 3 methods of the interface with the same name. The writeTo method is obvious, this method will be called with the object returned by the resource class, and here you do the actual conversion. The getSize is called before the writeTo method, to determine the size upfront. If you don't know the size before the conversion, just return -1.

Now the most interesting method is isWriteable. Here you write the code, to detect if this is the correct <em>writer</em> for the object. The JAX-RS implementation will iterate over each known MessageBodyWriter and call the isWritable method to determine if the class is capable of converting the object.

@Provider
public class XMLMessageBodyWriter implements MessageBodyWriter<Object> {
    
 @Override
 public long getSize(Object arg0, Class<?> arg1, Type arg2, Annotation[] arg3, MediaType arg4) {
   return -1;
 }
    
 @Override
 public boolean isWriteable(Class<?> clazz, Type type, 
       Annotation[] annotations, MediaType mediaType) {
   if ("be.vanboxel.labs.app.dto".equals(clazz.getPackage().getName())
       && mediaType.getType().equals("application")
       && mediaType.getSubtype()
           .matches("vnd\\.vanboxel\\.labs\\.app\\.v1\\..*\\+xml")
   ) {
     return true;
   }
   return false;
 }

 @Override
 public void writeTo(Object object, Class<?> clazz, Type type, 
         Annotation[] annotation, MediaType mediaType, 
         MultivaluedMap<String, Object> map, 
         OutputStream out) throws IOException, WebApplicationException {
   if (object instanceof Company) {
     marshal(out, XMLAdapter.toXML((Company) object));
   }
   else if (object instanceof Customer) {
     marshal(out, XMLAdapter.toXML((Customer) object));
   }
 }
}

In this example we only use 2 parameters of the supplied information to decide if the class is capable of the conversion. The first is the class of the object. We are only interested in converting our own DTO classes. They are all packaged together so the package name is verified. The other parameter we check is the MIME type. Here we check if it's the private namespace witch includes our version number.

Now the only thing left is to remove the conversion code from the methods in the resource class and add the @Produces annotation, with the MIME type.

@GET
@Path("companies/{companyId}")
@Produces("application/vnd.vanboxel.labs.app.v1.company+xml")
public XmlCompany getCompany(@PathParam("companyId") id ) {
  return companies.getId(companyId);
}

Say the REST interface evolved a newer and ritcher representation that is not compatible with the current and adds a JSON representation on top. We only need to write the two new MessageBodyWriters (and MessageBodyReaders) for the new XML representation and JSON. Once the classes are written, you only need to add the @Produces and @Consumer MIME types to the resource classes.

@GET
@Path("companies/{companyId}")
@Produces("application/vnd.vanboxel.labs.app.v1.company+xml")
@Produces("application/vnd.vanboxel.labs.app.v2.company+xml")
@Produces("application/vnd.vanboxel.labs.app.v2.company+json")
public XmlCompany getCompany(@PathParam("companyId") id ) {

If you already have a written a bunch of adapters to convert your dto's to your XML representation you could consider making them MessageBodyWriter/Readers by implementing the interfaces there.

0 comments:

Post a Comment