Friday, 13 February 2015

Writing a Custom HTTP Message Converter in Spring

The Spring Webservices course got so big that we had to cut a few minor topics, and I promised on the video that I would write some blog posts covering them. Here's the first of them, how to write a "Custom Message Converter".

You probably don't need to do this very often - I've never had to do this "in real life". But it is a useful exercise to get a better understanding of what those message converters are doing.

Recall that in Spring, a MessageConverter is a class that is capable of converting a regular Java domain object to a REST representation (and back again). Spring has a small set of default converters already built in, but the two main ones are for JSON (most common representation used in REST) and XML.

For this exercise, let's assume that for some reason, our REST application needs to support YAML as well. YAML is Yet Another Markup Language (literally) that aims to be simpler than XML. It's used a lot in Rails.

As a starting point, I've fired up the REST project that we built on the training course. I've also started up the standard Spring REST shell:

baseUri mywebapp
get /customers

< 200 OK
< Server: Apache-Coyote/1.1
< Content-Type: application/json;charset=UTF-8
< Transfer-Encoding: chunked
< Date: Thu, 12 Feb 2015 17:56:33 GMT
<
{
  "customers" : [ {
    "customerId" : "100029",
    "companyName" : "Acme",
    "email" : null,
    "telephone" : null,
    "notes" : "No Notes",
    "calls" : null,
    "version" : 1,
    "links" : [ {
      "rel" : "self",
      "href" : "http://localhost:8080/mywebapp/customers/customer/100029?fullDet

As on the course, if the client wants XML instead, they can change the accept headers:

headers set --name accept --value application/xml

And now we repeat the get request....

get /customers
> accept: application/xml

< 200 OK
< Server: Apache-Coyote/1.1
< Content-Type: application/xml
< Transfer-Encoding: chunked
< Date: Thu, 12 Feb 2015 18:04:06 GMT
<
<<customers><customer><companyName>Acme</companyName
><customerId>100029</customerId> ... lots of XML snipped

But there is no YAML message converter installed by default in Spring....

headers set --name accept --value application/yaml
get customers

> accept: application/yaml

<406 NOT_ACCEPTABLE

So let's write a YAML Message Converter!

Step 1: Add the JAR file for YAML

One Java YAML parser is called SnakeYAML (code.google.com/p/snakeyaml). You can download the JAR from there, but if you have done our course, I actually supplied this JAR in the "Additional JARs" folder. So pull it from there and add it to your build path.

This library is very easy to use. If you want to try it out, you can easily convert an object into YAML (and back again) in a test harness.

public class TestYaml 
{
 public static void main(String[] args)
 {
  Customer c = new Customer("10012", "Acme","Notes");
  
  Yaml yaml = new Yaml();
  System.out.println(yaml.dump(c));
 }
}

This gives an output like this:

!!com.virtualpairprogrammers.domain.Customer
calls: []
companyName: Acme
customerId: '10012'
email: null
notes: Notes
telephone: null
version: 0

Step 2: Write the converter

This is the bulk of the work. To write a message converter, extend the Spring AbstractHttpMessageConverter, and override the three methods as below.

  • readInternal() describes how Spring should convert the data (YAML) into a Java object.
  • writeInternal() is the opposite - it generates a YAML String from a Java object (this will be done in a similar way to our test above).
  • The supports() method is used to determine whether the converter actually supports conversion to and from the type of object in question. You might decide that you're not going to support collections for example. We'll simply return true and support any object.

In the constructor, we call the superclass constructor, which requires a MediaType object to denote what the HTTP media type is. We're supporting application/yaml.

The implementations of the read and write methods are fairly routine, we're just using the SnakeYaml library. It takes a bit of fiddling with the API of the HttpInputMessage and HttpOutputMessage classes to get what you need. In the read method, the getBody() method returns a standard Java InputStream, which luckily SnakeYaml can accept. In the write() method, we have to convert the YAML String into a byte array so we can send it to the write() method of the HttpOuputMessage. It's all a bit fiddly but straightforward in the end.


public class YamlMessageConverter<T> extends AbstractHttpMessageConverter<T>
{
 public YamlMessageConverter()
 {
        super(new MediaType("application","yaml"));
 }
 
 @Override
 protected T readInternal(Class<? extends T> arg0, HttpInputMessage arg1)
   throws IOException, HttpMessageNotReadableException 
 {
   Yaml yaml = new Yaml(new Constructor(arg0));
   T object = (T)yaml.load(arg1.getBody());
   return object;
 }

 @Override
 protected boolean supports(Class<?> arg0) {
  return true;
 }
 
 @Override
 protected void writeInternal(T arg0, HttpOutputMessage arg1)
   throws IOException, HttpMessageNotWritableException 
 {
  Yaml yaml = new Yaml();
  String result = yaml.dump(arg0);  
  arg1.getBody().write(result.getBytes());
 }
}

Step 3: Register the converter

The magic that makes the default message converters automatically happen is the tag in your Spring configuration.

We can add our new YAML Converter into the this tag:

<!-- This will automatically switch on the default httpmessageconverters -->
 <mvc:annotation-driven content-negotiation-manager="contentNegotiationManager">
  <mvc:message-converters register-defaults="true">
   <bean class="com.virtualpairprogrammers.messageconverters.YamlMessageConverter"/>
  </mvc:message-converters>
 </mvc:annotation-driven>

Note: the "register-defaults=true" is needed - without it, the default converters will not be registered and you will end up with only the YAML one.

And that's it. We can now deploy the application and test:

headers set --name accept --value application/yaml
get customer/100029

< 200 OK
< Server: Apache-Coyote/1.1
< Content-Type: application/yaml
< Transfer-Encoding: chunked
< Date: Fri, 13 Feb 2015 12:55:47 GMT
<
!!com.virtualpairprogrammers.domain.Customer
calls: []
companyName: Acme
customerId: '100030'
email: null
notes: No Notes
telephone: null
version: 1

Our representation is now in YAML.

I hope this exercise may prove useful to someone - to be honest I'm not really interested in YAML, the main point of the exercise is to get an understanding of what those mysterious HttpMessageConverters are doing!

1 comment: