Many of my Restlet resources are designed to support both JSON and HTML representations with resource interfaces that look like this:
public interface PersonResource {
@Get Person getPerson();
}
where Person is a POJO that might look like this:
public class Person {
public String getName() { ...}
public List<Address> getAddresses() { ... }
... other bean stuff: setters, fields ...
}
and the implementation of the resource looks like this:
public class PersonServerResource
extends ServerResource implements PersonResource {
@Override public Person getPerson() {
return ... construct Person from domain data ...
}
}
I register custom ConverterHelpers with the Restlet Engine. One of them knows how to use Jackson to convert Person into JSON, one knows how to obtain a Freemarker HTML template associated with the Person type and render it with the Person as its data model.
It works like magic. When I use a browser to point to the resource, it returns the HTML generated with Freemarker. When I ask for it with, say, jQuery, requesting JSON, I get ... JSON. When I get a Restlet ClientResource proxy for PersonResource at that URI, I get a Person object back when I call getPerson() on the client side; the value returned by getPerson() on the server is serialized to JSON, sent over the wire, and deserialized back into a Person object.
The only problem I had was for PUT and POST method handlers, because there was no automatic way to convert form data to my domain types. I had to create an additional method to handle forms as parameters:
public interface PeopleResource {
@Post("json:json") Person addPerson(Person person);
@Post("form:html") Person addPersonForm(Form person);
}
public class PeopleServerResource
extends ServerResource implements PeopleResource {
@Override public Person addPerson(Person person) {
...
}
@Override Person addPersonForm(Form personForm) {
Person person = new Person();
// Extract values from personForm and set
// them to person.
return addPerson(person);
}
}
I suffered this for a long time, and then I began to think that the application/www-form-urlencoded media type was, at least for simple types, not far from JSON. Could I translate form data to JSON and then use Jackson to deserialize that? Of course I could. In fact, it wasn't too hard to define some conventions whereby sequences (lists/arrays) and nested structures could be modeled. An encoded form for Person as produced by an HTML page might look like this, in part:
name=Tim&address.1.street=Main&address.1.city=Anytown...
...&address.2.street=Elm&address.2.city=Smallton...
and the corresponding JSON would be:
{
"name":"Tim",
"address":[{
"street":"Main",
"city":"Anytown",
...
},{
"street":"Elm",
"city":"Smallton",
...
},
...
]
}
I wrote FormDeserializer to do this. It takes a Jackson ObjectMapper to do the heavy lifting. Here's the Javadoc for the deserialization method:
publicT deserialize(Form source, Class targetType)
- Converts a Form to a Java object of the target type, using each
name=value
pair to set the corresponding property on the target object.
Compound names, using period (.) as the delimiter, are treated as pseudo-dereferences (a la JavaScript or Groovy) to set properties of sub-objects, e.g.,a.b=c
for bean targets is treated like a call totarget.getA().setB(c)
.
Numeric components of compound names are treated as indices into a sequence named by the preceding components, e.g.,a.1=c
is treated astarget.getA()[1] = c
(ortarget.getA().set(1, c)
, if the "a" property of the target is a list rather than an array). Unless an element with index 0 is set, the indices are origin 1.
Sequences are also created by names with multiple values, e.g.,a=x&a=y
is equivalent toa.1=x&a.2=y
, with the value of the "a" property in the target being a sequence of two values.
When a name appears both indexed and non-indexed, the last assignment wins:a=x&a.1=a1&a.2=a2
will set the "a" property to a sequence of values[a1, a2]
, buta.1=a1&a.2=a2&a=x
will set the "a" property tox
.
If the top level consists only of integer indices, the subobjects will be interpreted as elements of a sequence (and T must be a List or List subtype instead).
The values are deserialized using the Jackson ObjectMapper that was used to construct this FormDeserializer. Any type that can be deserialized from a string can be used. While it is possible for Jackson to deserialize object graphs that have internal references, it is not possible for the form values to refer outside of themselves.
Runtime exceptions from Jackson conversion are propagated without exception translation. This could be considered a bug.-
- Parameters:
source
- the Restlet Form object to be deserializedtargetType
- the type of the target object into which the form is to be deserialized.- Returns:
- the deserialized object of the target type
The converter class that uses this machinery is FormConverter. It uses a marker annotation, FormDeserializable, that signals when a class is suitable for deserialization from a form.
The upshot is that I can now write:
public interface PeopleResource {
@Post Person addPerson(Person person);
}
// And no need for additional method in implementation!
Big reduction in the amount of boilerplate I have to write, and the code is easier to read.
Here are the links to the code in one place:
I have @Inject tags on some constructors, but you can ignore them unless you want to use them for dependency injection.
The implementation makes heavy use of Guava. If you don't or can't use Guava, then you'll have to roll your own machinery. (Good luck!)