Wednesday, November 9, 2011

GWT, JSON and AutoBean

On my current project, we're using Google Web Toolkit (GWT) for a rich internet application (RIA).

Starting with GWT version 2.1.x, GWT is offering a new framework called AutoBean for handling bean-like objects. The framework provides automatic binding to editors and saves the developers the hassle of writing a lot of boilerplate code.

AutoBean also provides serialization of beans to JSON which can then be used for communication with the server. The documentation for this framework is, let's just call it, not very clear. In addition, some of the error handling in the framework generate very cryptic error messages.

Recently, we ran into NullPointerException when we serialize Long values and java.util.Date values. This turns out to be previously reported as a bug in the system. Issue 6331. Below you can see a common cryptic stack trace.

The problem is that the AutoBean JSON library is using non-standard format for long values; it looks for them to be quoted like strings, instead of unquoted like regular numbers.

On my current project, we're using the Jackson library for JSON serialization (Jackson JSON). Jackson is a great library for JSON but is also lacking when it comes to documentation.

In the code below, I show how we override the default Jackson behavior in order to write Long and java.util.Date as strings. This works nicely and if you're willing to change your JSON library for GWT then you're in good shape.

In our project, we wanted to make sure that we can also interact with non-GWT clients. We extended our REST library (Jersey) to look at the HTTP header for the type. We request the media type of GWT-JSON for the customized JSON and regular (application_json) for other clients.

Here is the code for modifying Jackson:

import java.io.IOException;
import java.util.Date;

import org.codehaus.jackson.JsonGenerator;
import org.codehaus.jackson.JsonParser;
import org.codehaus.jackson.JsonProcessingException;
import org.codehaus.jackson.Version;
import org.codehaus.jackson.map.DeserializationContext;
import org.codehaus.jackson.map.JsonDeserializer;
import org.codehaus.jackson.map.JsonSerializer;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.SerializerProvider;
import org.codehaus.jackson.map.module.SimpleModule;

public class JsonUtils
{
    public static String getString(Object object)
    {
        try
        {
            ObjectMapper mapper = createMapper();
            String json = mapper.writeValueAsString(object);
            return json;
        }
        catch (IOException e)
        {
            throw new RuntimeException("Failed to convert JSON to object", e);
        }
    }
   
    public static T fromString(String json, Class clazz)
    {
        try
        {
            ObjectMapper mapper = createMapper();
            T result = mapper.readValue(json, clazz);
            return result;
        }
        catch (IOException e)
        {
            throw new RuntimeException("Failed to convert JSON to object:\n" + json, e);
        }
    }
   
    public static ObjectMapper createMapper()
    {
        ObjectMapper mapper = new ObjectMapper();
        SimpleModule module = new SimpleModule("Ekotrope", new Version(1, 0, 0, null));
        module.addSerializer(Date.class, new DateSerializer());
        module.addDeserializer(Date.class, new DateDeserializer());
        module.addSerializer(Long.class, new LongSerializer());
        module.addDeserializer(Long.class, new LongDeserializer());
        mapper.registerModule(module);
        return mapper;
    }
   
    private static class DateSerializer extends JsonSerializer
    {
        @Override
        public void serialize(Date value, JsonGenerator jgen, SerializerProvider provider)
              throws IOException, JsonProcessingException
        {
            String str = "" + value.getTime();
            jgen.writeString(str);
        }
    }
   
    private static class DateDeserializer extends JsonDeserializer
    {
        @Override
        public Date deserialize(JsonParser jp, DeserializationContext ctxt)
            throws IOException, JsonProcessingException
        {
            String str = jp.getText();
            Long value = Long.parseLong(str);
            Date date = new Date(value);
            return date;
        }
    }
   
    private static class LongSerializer extends JsonSerializer
    {
        @Override
        public void serialize(Long value, JsonGenerator jgen, SerializerProvider provider)
              throws IOException, JsonProcessingException
        {
            String str = "" + value.toString();
            jgen.writeString(str);
        }
    }
   
    private static class LongDeserializer extends JsonDeserializer
    {
        @Override
        public Long deserialize(JsonParser jp, DeserializationContext ctxt)
            throws IOException, JsonProcessingException
        {
            String str = jp.getText();
            Long value = Long.parseLong(str);
            return value;
        }
    }
}


Cryptic Stacktrace from the bug report:
[ERROR] Uncaught Exception:
java.lang.NullPointerException
 at com.google.web.bindery.autobean.shared.impl.StringQuoter.tryParseDate(StringQuoter.java:85)
 at com.google.web.bindery.autobean.shared.ValueCodex$Type$6.decode(ValueCodex.java:106)
 at com.google.web.bindery.autobean.shared.ValueCodex$Type$6.decode(ValueCodex.java:1)
 at com.google.web.bindery.autobean.shared.ValueCodex.decode(ValueCodex.java:288)
 at com.google.web.bindery.autobean.shared.impl.AutoBeanCodexImpl$ValueCoder.decode(AutoBeanCodexImpl.java:492)
 at com.google.web.bindery.autobean.shared.impl.AbstractAutoBean.getOrReify(AbstractAutoBean.java:241)
 at com.activegrade.shared.data.user.UserAutoBean.access$5(UserAutoBean.java:1)
 at com.activegrade.shared.data.user.UserAutoBean$2.getAccessExpirationDate(UserAutoBean.java:89)
 at com.activegrade.shared.data.user.UserAutoBean$1.getAccessExpirationDate(UserAutoBean.java:26)
 at com.activegrade.shared.data.user.UserAutoBean.traverseProperties(UserAutoBean.java:161)
 at com.google.web.bindery.autobean.shared.impl.AbstractAutoBean.traverse(AbstractAutoBean.java:166)
 at com.google.web.bindery.autobean.shared.impl.AbstractAutoBean.accept(AbstractAutoBean.java:101)
 at com.google.web.bindery.autobean.shared.impl.AutoBeanCodexImpl.doCoderFor(AutoBeanCodexImpl.java:521)
 at com.google.web.bindery.autobean.shared.impl.AbstractAutoBean.getOrReify(AbstractAutoBean.java:239)
 at com.activegrade.shared.data.user.UserAutoBean.access$5(UserAutoBean.java:1)
 at com.activegrade.shared.data.user.UserAutoBean$2.getPrimaryEmailAddress(UserAutoBean.java:86)
 at com.activegrade.shared.data.user.UserAutoBean$1.getPrimaryEmailAddress(UserAutoBean.java:22)
 at com.activegrade.shared.data.user.UserAutoBean.traverseProperties(UserAutoBean.java:152)
 at com.google.web.bindery.autobean.shared.impl.AbstractAutoBean.traverse(AbstractAutoBean.java:166)
 at com.google.web.bindery.autobean.shared.impl.AbstractAutoBean.accept(AbstractAutoBean.java:101)
 at com.google.web.bindery.autobean.shared.impl.AutoBeanCodexImpl.doCoderFor(AutoBeanCodexImpl.java:521)
 at com.google.web.bindery.autobean.shared.impl.AbstractAutoBean.getOrReify(AbstractAutoBean.java:239)
 at com.activegrade.shared.data.user.UserAutoBean.access$5(UserAutoBean.java:1)
 at com.activegrade.shared.data.user.UserAutoBean$2.getDefaultRootOrgId(UserAutoBean.java:74)
 at com.activegrade.shared.data.user.UserAutoBean$1.getDefaultRootOrgId(UserAutoBean.java:6)
 at com.activegrade.shared.data.user.UserAutoBean.traverseProperties(UserAutoBean.java:116)
 at com.google.web.bindery.autobean.shared.impl.AbstractAutoBean.traverse(AbstractAutoBean.java:166)
 at com.google.web.bindery.autobean.shared.impl.AbstractAutoBean.accept(AbstractAutoBean.java:101)
 at com.google.web.bindery.autobean.shared.impl.AutoBeanCodexImpl.doCoderFor(AutoBeanCodexImpl.java:521)
 at com.google.web.bindery.autobean.shared.impl.AbstractAutoBean.getOrReify(AbstractAutoBean.java:239)
 at com.activegrade.shared.data.user.UserAutoBean.access$5(UserAutoBean.java:1)
 at com.activegrade.shared.data.user.UserAutoBean$2.getId(UserAutoBean.java:77)
 at com.activegrade.shared.data.user.UserAutoBean$1.getId(UserAutoBean.java:10)
 at com.activegrade.client.AppState.setLoggedInUser(AppState.java:100)



No comments:

Post a Comment