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)



Interesting GWT Libraries


Google Web Toolkit (GWT) is a fantastic platform for developing rich internet applications (RIA).

I've been looking for different libraries to extend the functionality of GWT and have run into several interesting libraries hosted on Google Project. The following libraries seem to have interesting functionality, high development activity and good user feedback (+1).

GWT Query (GWTQuery):

    GwtQuery a.k.a. GQuery is a jQuery-like API written in GWT, which allows GWT to be used in progressive enhancement scenarios where perhaps GWT widgets are too heavyweight. It can also be used to find and improve your GWT widgets.

    GwtQuery is easy to learn for those using jQuery as they share the same api, aditionally gquery adds nice features like type-safe css, compile time optimisations, etc.


GWT Platform (GWTP)

A complete model-view-presenter framework to simplify your next GWT project.

GWTP was presented at Google I/O and you can find some slides here: Link

Google GIN (GIN)
For those who love Guice for dependency injection; this is the GWT version.

GIN (GWT INjection) brings automatic dependency injection to Google Web Toolkit client-side code. GIN is built on top of Guice and uses (a subset of) Guice's binding language. (See GuiceCompatibility for details.) By using GWT's compile-time Generator support, GIN has little-to-no runtime overhead compared to manual DI.

GWT Upload (GWTupload)

GwtUpload & JsUpload: File Upload Progress with pure javascript (Ajax)

GWT Test Utils

a framework to test GWT client side code very easily

GWT Test utils aims to replace the VERY slow GWTTestCase construct for unit testing GWT Widget code.

Spiffy UI

Spiffy UI framework: GWT made simple

Spiffy UI aims to help with the plumbing and the framework required to setup GWT project. See also the main website for Spiffy UI