martes, octubre 11, 2011

You're so sexy sex sex sexy. Feel me now and stop the conversation. No, no, no don't stop the desire no, No, no, no, no! (Sexy - French Affair)





INTRODUCTION

In current post I am going to explain how to implement and register a custom Spring MVC HttpMessageConverter object. Specifically a converter that binds objects to Yaml protocol. As starting point I am going to use the Rest application I implemented in previous post. That application is a simple Restful application where XML and JSON (Spring MVC already support them) are used. Because Spring MVC does not implement YamlMessageConverter, I am going to explain how to transform previous application from supporting XML and JSON to support Yaml.

Yaml is a human-readable data serialization format that takes concepts from programming languages such as C, Perl, and Python, and ideas from XML and the data format of electronic mail (RFC 2822).

SnakeYAML  is a YAML parser and emitter for the Java programming language, and will be used to implement our message converter.

DESIGN

Let's start with a UML class diagram of HttpMessageConverter that are going to be implemented.



HttpMessageConverter is a base interface that must be implemented. It is a strategy interface that specifies methods to convert objects from and to HTTP requests and responses. AbstractHttpMessageConverter is the abstract base class for most HttpMessageConverter implementation (both provided by springframework), and is our base class.

First developed class is an abstract class called AbstractYamlHttpMessageConverter. This class is responsible of generic operations that "should" be required by all Yaml parsers/emitters. In my case it deals with charset options, and transforms HttpInputMessage and HttpOutputMessage to java.io.InputStreamWriter and java.io.OutputStreamWriter. In fact it acts as a Template Pattern to read and write operations (readInternal and writeInternal methods).

Next abstract class is AbstractSnakeYamlHttpMessageConverter. This class is a base class for HttpMessageConverters that use SnakeYaml as Yaml binder. This class gets an instance of Yaml class (central class of SnakeYaml project).

And finally JavaBeanSnakeYamlHttpMessageConverter. This class uses SnakeYaml JavaBeans features for converting from object to Yaml and viceversa. SnakeYaml does not support annotations like Jackson (JSON) or Jaxb (XML), but if some day this feature is implemented, we should create a new class extending from AbstractSnakeYamlHttpMessageConverter with required change.

CODE

First of all is adding a new dependency to pom. In this case SnakeYaml.

<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>1.9</version>
</dependency>
view raw pom.xml hosted with ❤ by GitHub

Then three classes previously exposed should be developed.

First class is a generic Yaml converter where we are setting accepted media type, in this case application/yaml, and creating a Reader and Writer with required Charset. We are leaving to children classes the responsibility of implementing read and write code.

public abstract class AbstractYamlHttpMessageConverter<T> extends
AbstractHttpMessageConverter<T> {
public static final Charset DEFAULT_CHARSET = Charset.forName("ISO-8859-1");
private final List<Charset> availableCharsets;
private boolean writeAcceptCharset = true;
protected AbstractYamlHttpMessageConverter() {
super(new MediaType("application", "yaml"));
this.availableCharsets = new ArrayList<Charset>(Charset.availableCharsets().values());
}
public void setWriteAcceptCharset(boolean writeAcceptCharset) {
this.writeAcceptCharset = writeAcceptCharset;
}
@Override
protected T readInternal(Class<? extends T> clazz,
HttpInputMessage inputMessage) throws IOException,
HttpMessageNotReadableException {
return readFromSource(clazz, inputMessage.getHeaders(), new InputStreamReader(inputMessage.getBody(), getCharset(inputMessage.getHeaders())));
}
@Override
protected void writeInternal(T t, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
if (writeAcceptCharset) {
outputMessage.getHeaders().setAcceptCharset(getAcceptedCharsets());
}
writeToResult(t, outputMessage.getHeaders(), new OutputStreamWriter(outputMessage.getBody(), getCharset(outputMessage.getHeaders())));
}
protected List<Charset> getAcceptedCharsets() {
return this.availableCharsets;
}
private Charset getCharset(HttpHeaders headers) {
MediaType contentType = headers.getContentType();
return contentType.getCharSet() != null ? contentType.getCharSet() : DEFAULT_CHARSET;
}
protected abstract T readFromSource(Class<? extends T> clazz, HttpHeaders headers, Reader source)
throws IOException;
protected abstract void writeToResult(T t, HttpHeaders headers, Writer result)
throws IOException;
}

Next class is specific of API that will be used to bind classes to messages, in this case SnakeYaml. This class will be responsible of creating an instance of Yaml class (from SnakeYaml). As is warned in http://snakeyamlrepo.appspot.com/releases/1.9/site/apidocs/index.html each thread must have its own instance; for this reason a ThreadLocal is used to carry out this restriction.

public abstract class AbstractSnakeYamlHttpMessageConverter<T> extends
AbstractYamlHttpMessageConverter<T> {
//SnakeYaml requires one instance for each Thread so ThreadLocal is used.
private ThreadLocal<Yaml> yamlInterfaces = new ThreadLocal<Yaml>();
public AbstractSnakeYamlHttpMessageConverter() {
super();
}
protected final Yaml getSnakeYamlInterface() {
Yaml yaml = this.yamlInterfaces.get();
if(yaml == null) {
yaml = new Yaml();
yamlInterfaces.set(yaml);
}
return yaml;
}
}

Final class is an implementation of read/write methods using SnakeYaml. As I have explained previously this class has a meaning because allows us changing SnakeYaml binding strategy (for example to annotation approach) and only worries to rewrite read/write operations.

public class JavaBeanSnakeYamlHttpMessageConverter extends
AbstractSnakeYamlHttpMessageConverter<Object> {
public JavaBeanSnakeYamlHttpMessageConverter() {
super();
}
@Override
protected Object readFromSource(Class<?> clazz, HttpHeaders headers,
Reader source) throws IOException {
Yaml beanLoader = getSnakeYamlInterface();
return beanLoader.loadAs(source, clazz);
}
@Override
protected void writeToResult(Object t, HttpHeaders headers, Writer result)
throws IOException {
Yaml beanDumpper = getSnakeYamlInterface();
String yamlMessage = beanDumpper.dumpAsMap(t);
FileCopyUtils.copy(yamlMessage, result);
}
@Override
public boolean canRead(Class<?> clazz, MediaType mediaType) {
return canRead(mediaType);
}
@Override
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
return canWrite(mediaType);
}
@Override
protected boolean supports(Class<?> clazz) {
// should not be called, since we override canRead/Write
throw new UnsupportedOperationException();
}
}

Now it is time to register created message converter to AnnotationMethodHandlerAdapter. First thing you should do is not to use <mvc:annotation-driven>. This annotation registers default message converters and you are not able to modify them. So first step is comment or remove annotation-driven. Next step is declare DefaultAnnotationHandlerMapping bean and AnnotationMethodHandlerAdapter which registers http message converters. In our case only Yaml http message converter is added.

<!-- Enables the Spring MVC @Controller programming model -->
<!-- <annotation-driven /> -->
<beans:bean id="handlerMapping" class="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping" />
<beans:bean
class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
<beans:property name="messageConverters">
<util:list id="beanList">
<beans:bean class="org.springframework.rest.JavaBeanSnakeYamlHttpMessageConverter"></beans:bean>
</util:list>
</beans:property>
</beans:bean>

RUNNING

And now you can try application. Deploy it on a server, and using for example Rest Client, try http://localhost:8080/RestServer/characters/1 and you will receive a response like:



If you want you can use POST instead of GET to insert a new Character to our Map.

@Controller
public class HomeController {
private static final Map<Integer, Character> characters = new HashMap<Integer, Character>();
static {
characters.put(1, new Character(1, "Totoro", false));
characters.put(2, new Character(2, "Satsuki Kusakabe", true));
characters.put(3, new Character(3, "Therru", false));
}
@RequestMapping(value = "/characters/{characterId}", method = RequestMethod.POST)
@ResponseBody
public Character addCharacter(@RequestBody Character character, @PathVariable int characterId) {
characters.put(characterId, character);
return character;
}
@RequestMapping(value = "/characters/{characterId}", method = RequestMethod.GET)
@ResponseBody
public Character findCharacter(@PathVariable int characterId) {
return characters.get(characterId);
}
}

As you can see developing a Spring MVC HTTP Message Converter is so easy, in fact you must implement two basic operations, when a resource can be read or written and resource conversion.

Hope you found this post useful.

0 comentarios: