In a current project, I've got a typical architecture: DB Store, API, Server talking to a variety of clients, including Mobile, and Web (GWT).
I wanted very much to use a single POJO model for my API and at least my web app, potentially my Android app as well. I made the following technology choices, running on Google App Engine:
- Objectify for ORM - this maps POJOs directly to the GAE datastore. Light, fast, nice.
- Restlet for API and client-server communication - well-supported versions for GAE, GWT, and Android
My hope was that this stack would meet my requirements, and not paint me into any corners with respect to other platforms (such as iOS) later. I wanted my API to be able to use multiple transport protocols (e.g., GWT RPC, JSON, XML, etc.).
Eventually I ran into issues with Jackson (the default OOTB JSON serialization framework that comes with Restlet) not being able to serialize Objectify key objects. This was due to the Objectify Key class having a self-referencing loop on the root property. Since this is a dynamically generated property, it is safe to remove it from the serialization rules used by Jackson. It took me a fair amount of research to figure out how to do that, and it's my hope that this post will help someone else figure out how to get by this issue.
The basic idea is that you need to replace the OOTB JacksonCoverter with your own custom converter that makes use of the concept of Jackson Mixins to ignore the getRoot method on the Objectify Key class. Mixins are a very cool Jackson concept that allow you to, in effect, inject annotations into a class that is handled by the Jackson serialization framework. Normally, you would need to get the source for the Key class, add annotations to the root property, and then recompile. Mixins allow you to do the equivalent without access to the original source.
You will need to create three classes. You can pretty much just copy them as I have them here. If you're using Restlet, you will also need to register your new ConverterHelper with the Restlet infrastructure. I did this in my Restlet Application class in the createInboundRoot method:
RestletApi.java
/**
* Creates a root Restlet that will receive all incoming calls.
*/
@Override
public Restlet createInboundRoot() {
Router router = new Router(getContext());
// Defines only one route
router.attach("/foo", FooServerResource.class);
router.setRoutingMode(Router.MODE_BEST_MATCH);
// add the GWT mapping in the metadata service
getMetadataService().setDefaultMediaType(MediaType.APPLICATION_JSON);
getMetadataService().addExtension("gwt", MediaType.APPLICATION_JAVA_OBJECT_GWT);
// replace the OOTB JacksonConverter with the a converter that will work with the objectify Key class
replaceConverter(JacksonConverter.class, new FixedJacksonConverter());
return router;
}
/**
* Registers a new converter with the Restlet engine, after removing the first
* registered converter of the given class.
*
* Taken from:
* http://restlet.tigris.org/ds/viewMessage.do?dsForumId=4447&dsMessageId
* =2716118
*/
static void replaceConverter(
Class<? extends ConverterHelper> converterClass,
ConverterHelper newConverter) {
ConverterHelper oldConverter = null;
List<ConverterHelper> converters = Engine.getInstance().getRegisteredConverters();
for (ConverterHelper converter : converters) {
if (converter.getClass().equals(converterClass)) {
converters.remove(converter);
oldConverter = converter;
break;
}
}
converters.add(newConverter);
if (oldConverter == null) {
System.err.println("Added Converter to Restlet Engine: " + newConverter.getClass().getName());
} else {
System.err.println("Replaced Converter "+oldConverter.getClass().getName()+" with "+
newConverter.getClass().getName()+" in Restlet Engine");
}
}
FixedJacksonConverter.java
import java.util.List;
import org.restlet.data.MediaType;
import org.restlet.engine.resource.VariantInfo;
import org.restlet.ext.jackson.JacksonConverter;
import org.restlet.ext.jackson.JacksonRepresentation;
import org.restlet.representation.Representation;
import org.restlet.representation.Variant;
public class FixedJacksonConverter extends JacksonConverter {
private static final VariantInfo VARIANT_JSON = new VariantInfo(MediaType.APPLICATION_JSON);
@Override
protected <T> JacksonRepresentation<T> create(MediaType mediaType, T source) {
return new FixedJacksonRepresentation<T>(mediaType, source);
}
@Override
protected <T> JacksonRepresentation<T> create(Representation source, Class<T> objectClass) {
return new FixedJacksonRepresentation<T>(source, objectClass);
}
@Override
public List<Class<?>> getObjectClasses(Variant source) {
List<Class<?>> result = null;
if(VARIANT_JSON.isCompatible(source)){
result=addObjectClass(result, Object.class);
result=addObjectClass(result, FixedJacksonRepresentation.class);
}
return result;
}
}
FixedJacksonRepresentation.java
import org.codehaus.jackson.map.ObjectMapper;
import org.restlet.data.MediaType;
import org.restlet.ext.jackson.JacksonRepresentation;
import org.restlet.representation.Representation;
import com.googlecode.objectify.Key;
public class FixedJacksonRepresentation<T> extends JacksonRepresentation<T> {
public FixedJacksonRepresentation(MediaType mediaType, T object) {
super(mediaType, object);
}
public FixedJacksonRepresentation(T object) {
super(object);
}
public FixedJacksonRepresentation(Representation representation, Class<T> objectClass) {
super(representation, objectClass);
}
@Override
protected ObjectMapper createObjectMapper() {
ObjectMapper ret = super.createObjectMapper();
// inject the mixin that will allow us to properly serialize Objectify Key objects...
ret.getSerializationConfig().addMixInAnnotations(Key.class, JacksonMixIn.class);
ret.getDeserializationConfig().addMixInAnnotations(Key.class, JacksonMixIn.class);
return ret;
}
}
JacksonMixin.java
import org.codehaus.jackson.annotate.JsonIgnore;
import com.googlecode.objectify.Key;
public interface JacksonMixIn {
@JsonIgnore <V> Key<V> getRoot();
}
|