A reader recently asked:
I would love to see a snippet of how to eval the JSON coming from RequestBuilder into the OverlayTypes. What is the mapping like? I used OverlayTypes to read in static data that I render into the head section of the hosted page, which is pretty easy and fast, but I don't know how to do this "reading" dynamically at runtime.
If you're not familiar with GWT's Overlay Types (added in 1.5), see Getting to really know GWT, Part 2: JavaScript Overlay Types. In our project, we're using Overlay Types to simplify JSON parsing and make our application lean-and-mean as possible.
First of all, we have a JSOModel class that acts as our overlay type:
import java.util.HashSet;
import java.util.Set;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.core.client.JsArray;
import com.google.gwt.core.client.JsArrayString;
/**
* Java overlay of a JavaScriptObject.
*/
public abstract class JSOModel extends JavaScriptObject {
// Overlay types always have protected, zero-arg constructors
protected JSOModel() {
}
/**
* Create an empty instance.
*
* @return new Object
*/
public static native JSOModel create() /*-{
return new Object();
}-*/;
/**
* Convert a JSON encoded string into a JSOModel instance.
* <p/>
* Expects a JSON string structured like '{"foo":"bar","number":123}'
*
* @return a populated JSOModel object
*/
public static native JSOModel fromJson(String jsonString) /*-{
return eval('(' + jsonString + ')');
}-*/;
/**
* Convert a JSON encoded string into an array of JSOModel instance.
* <p/>
* Expects a JSON string structured like '[{"foo":"bar","number":123}, {...}]'
*
* @return a populated JsArray
*/
public static native JsArray<JSOModel> arrayFromJson(String jsonString) /*-{
return eval('(' + jsonString + ')');
}-*/;
public final native boolean hasKey(String key) /*-{
return this[key] != undefined;
}-*/;
public final native JsArrayString keys() /*-{
var a = new Array();
for (var p in this) { a.push(p); }
return a;
}-*/;
@Deprecated
public final Set<String> keySet() {
JsArrayString array = keys();
Set<String> set = new HashSet<String>();
for (int i = 0; i < array.length(); i++) {
set.add(array.get(i));
}
return set;
}
public final native String get(String key) /*-{
return "" + this[key];
}-*/;
public final native String get(String key, String defaultValue) /*-{
return this[key] ? ("" + this[key]) : defaultValue;
}-*/;
public final native void set(String key, String value) /*-{
this[key] = value;
}-*/;
public final int getInt(String key) {
return Integer.parseInt(get(key));
}
public final boolean getBoolean(String key) {
return Boolean.parseBoolean(get(key));
}
public final native JSOModel getObject(String key) /*-{
return this[key];
}-*/;
public final native JsArray<JSOModel> getArray(String key) /*-{
return this[key] ? this[key] : new Array();
}-*/;
}
This class alone allows you to easily parse JSON returned in a callback. For example, here's an example of parsing Twitter's User Timeline in my OAuth with GWT application.
private class TwitterApiCallback implements RequestCallback {
public void onResponseReceived(Request request, Response response) {
if (response.getStatusCode() == 200) {
JsArray<JSOModel> data = JSOModel.arrayFromJson(response.getText());
List<JSOModel> statuses = new ArrayList<JSOModel>();
for (int i = 0; i < data.length(); i++) {
statuses.add(data.get(i));
}
// populate textarea with returned statuses
for (JSOModel status : statuses) {
payload.setValue(payload.getValue() + status.get("text") + "\n\n");
}
Label success = new Label("API call successful!");
success.setStyleName("success");
form.add(success);
} else {
onError(request, new RequestException(response.getText()));
}
}
public void onError(Request request, Throwable throwable) {
Window.alert("Calling API failed. " + OAuthPage.STANDARD_ERROR + "\n\n" + throwable.getMessage());
}
}
To simply things even more, we created a BaseModel class that can be extended.
import java.util.Map;
import java.util.HashMap;
import com.google.gwt.core.client.JsArrayString;
import com.google.gwt.user.client.Element;
import com.google.gwt.user.client.DOM;
public abstract class BaseModel {
protected JSOModel data;
public BaseModel(JSOModel data) {
this.data = data;
}
public String get(String field) {
String val = this.data.get(field);
if (val != null && "null".equals(val) || "undefined".equals(val)) {
return null;
} else {
return escapeHtml(val);
}
}
public Map<String, String> getFields() {
Map<String, String> fieldMap = new HashMap<String, String>();
if (data != null) {
JsArrayString array = data.keys();
for (int i = 0; i < array.length(); i++) {
fieldMap.put(array.get(i), data.get(array.get(i)));
}
}
return fieldMap;
}
private static String escapeHtml(String maybeHtml) {
final Element div = DOM.createDiv();
DOM.setInnerText(div, maybeHtml);
return DOM.getInnerHTML(div);
}
}
You can extend this class and create model objects that represent a more Java-like view of your data. For example, I could create a Status class with the following code:
public class Status extends BaseModel {
public Status(JSOModel data) {
super(data);
}
public String getText() {
return get("text");
}
}
Then I could change my JSON parsing in TwitterApiCallback to be:
private class TwitterApiCallback implements RequestCallback {
public void onResponseReceived(Request request, Response response) {
if (response.getStatusCode() == 200) {
JsArray<JSOModel> data = JSOModel.arrayFromJson(response.getText());
List<Status> statuses = new ArrayList<Status>();
for (int i = 0; i < data.length(); i++) {
Status s = new Status(data.get(i));
statuses.add(s);
}
// populate textarea with returned statuses
for (Status status : statuses) {
payload.setValue(payload.getValue() + status.getText() + "\n\n");
}
Label success = new Label("API call successful!");
success.setStyleName("success");
form.add(success);
} else {
onError(request, new RequestException(response.getText()));
}
}
public void onError(Request request, Throwable throwable) {
Window.alert("Calling API failed. " + OAuthPage.STANDARD_ERROR + "\n\n" + throwable.getMessage());
}
}
That's how we're doing lightweight JSON parsing with GWT. I've updated my GWT with OAuth demo with this code. You can also download the source. Please let me know if you have any questions.
Update October 20, 2009: I recently had to enhance the JSOModel and BaseModel classes in my project to handle nested objects and arrays. In my project, I have a Conversation object that has a Channel and a List of Task objects. These objects are available in the JSOModel of my BaseModel, I just needed to grab them a bit differently.
public Channel getChannel() {
return new Channel(data.getObject("channel"));
}
public List<Task> getTasks() {
JsArray<JSOModel> array = data.getArray("tasks");
List<Task> tasks = new ArrayList<Task>(array.length());
for (int i = 0; i < array.length(); i++) {
Task task = new Task(array.get(i));
tasks.add(task);
}
return tasks;
}
To set a Channel, it's as simple as:
data.set("channel", channel.toJson().toString());
To allow setting Lists, I had to enhance JSOModel by adding the following two methods:
public final void set(String key, List<JSOModel> values) {
JsArray<JSOModel> array = JavaScriptObject.createArray().cast();
for (int i=0; i < values.size(); i++) {
array.set(i, values.get(i));
}
setArray(key, array);
}
protected final native void setArray(String key, JsArray<JSOModel> values) /*-{
this[key] = values;
}-*/;
After making this change, I was able to convert my List to List and set it on the underlying JSOModel.
public void setTasks(List<Task> tasks) {
List<JSOModel> values = new ArrayList<JSOModel>();
for (Task task : tasks) {
values.add(task.getModel());
}
data.set("tasks", values);
}
To allow the task.getModel() method to work, I added a getter to BaseModel to allow retrieving the underlying JSOModel. Currently, I'm using a homegrown JSON.java class to produce JSON from my BaseModel objects. It all seems to work great and I'm pumped I can receive and send all my JSON using overlay types.