Apr

9

GWT: using JSON as history token?

By Olivier Gérardin

As you might know, GWT provides a handy and elegant mechanism to manage back and forward buttons (and bookmarking) through the use of the “anchor” part of the URL, that is the part after the # sign. Basically, whatever you put after the # will not trigger a page reload, but can be intercepted by JavaScript.

Using it in GWT is very straightforward:

  • register a listener to be notified when the context changes (user pressed back or forward or jumped in history) with History.addHistoryListener(listener)
  • when you want to create a browser history savepoint, call History.newItem(token)

The only thing is, GWT will not generate the token for you, because it has no way of knowing what must be included in the so-called application state. So when you want to manage history with GWT, you should always ask yourself the following questions:

  1. What must be saved? (just the active window? the selected item? etc.)
  2. How will you encode this in a String?

Depending on how complex is your answer to question 1, the answer to question 2 can be very trivial. For example, if all you want to save is the active tab in a multi-tab app, then the contents of the history token can be just an identifier of this tab. Generating it is easy, and so is parsing.

But what if you have more complex data, such as a list of items you want to restore, plus the currently selected item, plus the active view, etc.? I’ve been looking for a generic way to build a history token in such a case for a while. At first I considered a simple list of key/value pairs, such as:

key0=value0; key1=value1

This will certainly work in simple cases; you could write a parser and serializer that takes or generates a Map<String, String> without too much trouble. But then inevitably will come the need to embed another map (or a list) in the map… then the simple case is not so simple anymore.

Anyway, you could imagine handling recursivity with a format like :

key0=(item00, item01), key1={key10=value10, key12=(item120, item121)}

which would be parsed as:

  • map
    • key0 -> list
      • “item00″
      • “item01″
    • key1 -> map
      • key10 -> “value10″
      • key12 -> list
        • “item120″
        • “item121″

you get the picture.

I was set to write a parser for this type of format, and not very happy to have to do so, when it struck me how close this format was to JSON… what if a map was a JSON object, a list a JSON array ? My previous example would then generate the following JSON string:

{“key0″:[“item00″,” item01″], “key1″:{“key10″:”value10″, “key12″:[“item120″,” item121″]}

Given that GWT comes with a JSON parser/serializer, if I can represent my application state as a Map<String, Object> where objects are instances of String, List<Object> or Map<String, Object>, it would be very easy to build a JSON representation of it. Would that work?

The short answer: yes, but there are caveats.

First, since most of the special characters are URL-encoded, it’s not pretty. Actually it’s really ugly. This is what it looks like on an example:

MyPage.html#%7B%22active%22:%222d22aa%22,%20%22selected%22:%222dff8%22,%20%22workFolder%22:%5B%222be34%22,%222dff8%22,%22414a4%22%5D%7D

Scary, isn’t it? But, unless you believe every user pays attention to what’s going on in the address bar, it doesn’t really matter.

Second, it’s pretty verbose. The URL can’t grow in length indefinitely, and this burns a lot of precious characters.

Third, it’s not secure, if you use GWT’s native JSON parser that is, because as stated in the Javadoc: “For efficiency, this method [parse] is implemented using the JavaScript eval() function, which can execute arbitrary script. DO NOT pass an untrusted string into this method.”.

The last one is a showstopper for public web sites, unless you reimplement your own JSON parser in GWT. So yes, it works, but no, don’t use it. And if you do anyway, don’t blame me.

If you want some detail here’s how I have done it. I’m in the context of a HMVC app; each controller has a getState() method which is supposed to return a Map representing its state to be saved, and a restoreState() method that takes a Map of the state to be restored. By default getState() returns an empty map, and restoreMap() does nothing, so subclasses are free to override those methods to provide something to save/restore (which might recursively include the state of subcontrollers).

The topmost controller is responsible for handling save state requests; it uses getState() to obtain the global application state, then converts it to a JSON representation, and calls History.newItem().

Conversely, when a history change is detected, the token is parsed as JSON and then converted to a Map. The result is passed to restoreState(), which takes what it needs from the map, does what is needed to restore the state, and recursively calls restoreState() on subcontrollers. How neat is that?

Here’s the code I use to convert my object graph that represents the app state to and from JSON:

    /**
     * Serialize the specified object as a JSONValue. The object to serialize must be an instance of:
     * - String
     * - Map
     * - List
     * 

* where O has the same constraints. */ public static JSONValue serializeAsJson(Object object) { if (object instanceof Map) { Map map = (Map) object; JSONObject jsonObject = new JSONObject(); for (Map.Entry entry : map.entrySet()) { String key = entry.getKey(); Object value = entry.getValue(); JSONValue convertedValue = serializeAsJson(value); jsonObject.put(key, convertedValue); } return jsonObject; } else if (object instanceof List) { List list = (List) object; JSONArray jsonArray = new JSONArray(); for (int i = 0; i < list.size(); i++) { Object entry = list.get(i); JSONValue convertedValue = serializeAsJson(entry); jsonArray.set(i, convertedValue); } return jsonArray; } else if (object instanceof String) { return new JSONString((String) object); } else { throw new RuntimeException("Unsupported state type: " + object.getClass()); } } /** * Parse a JSONValue as follows: * -if the value is a JSONObject, parse into a Map * -if the value is a JSONArray, parse into a List * -if the value is a JSONString, parse into a String * -otherwise fail */ public static Object parseObject(JSONValue jsonValue) { if (jsonValue.isObject() != null) { return parseMap(jsonValue.isObject()); } else if (jsonValue.isArray() != null) { return parseList(jsonValue.isArray()); } else if (jsonValue.isString() != null) { return jsonValue.isString().stringValue(); } else { throw new RuntimeException("Failed to parse JSON: " + jsonValue.toString()); } } /** * Parse the specified JSONObject into a Map. The JSONValue associated to * a key is parsed recursively using {@link #parseObject(JSONValue)} */ public static Map parseMap(JSONObject jsonObject) { Map result = new HashMap(); for (String key : jsonObject.keySet()) { JSONValue jsonValue = jsonObject.get(key); Object convertedValue = parseObject(jsonValue); result.put(key, convertedValue); } return result; } /** * Parse the specified JSONArray into a List. The JSONValues contained in the array are * parsed recursively using {@link #parseObject(JSONValue)} */ public static Object parseList(JSONArray array) { List result = new ArrayList(); for (int i = 0; i < array.size(); i++) { JSONValue jsonValue = array.get(i); Object convertedValue = parseObject(jsonValue); result.add(convertedValue); } return result; }