Revision: 8869
Author: [email protected]
Date: Fri Sep 24 19:41:56 2010
Log: GWT implementation of json2.js parse and stringify, plus removal of
json2.js dependency from RF. This is a slice of a much more ambitious JSON
library that unified client and server JSON APIs into a single shared
library. Thus, it is expected that this API is not final, and will
eventually be moved into another package.
Currently, pure GWT only, does not attempt to delegate to native browser
JSON methods (which are full of browser-version dependant bugs).
Review at http://gwt-code-reviews.appspot.com/922801
http://code.google.com/p/google-web-toolkit/source/detail?r=8869
Added:
/trunk/user/src/com/google/gwt/requestfactory/client/impl/json
/trunk/user/src/com/google/gwt/requestfactory/client/impl/json/ClientJsonUtil.java
/trunk/user/src/com/google/gwt/requestfactory/client/impl/json/JsonArrayContext.java
/trunk/user/src/com/google/gwt/requestfactory/client/impl/json/JsonContext.java
/trunk/user/src/com/google/gwt/requestfactory/client/impl/json/JsonException.java
/trunk/user/src/com/google/gwt/requestfactory/client/impl/json/JsonMap.java
/trunk/user/src/com/google/gwt/requestfactory/client/impl/json/JsonMapContext.java
/trunk/user/src/com/google/gwt/requestfactory/client/impl/json/JsonVisitor.java
/trunk/user/test/com/google/gwt/requestfactory/client/impl/json
/trunk/user/test/com/google/gwt/requestfactory/client/impl/json/ClientJsonUtilTest.java
Modified:
/trunk/user/src/com/google/gwt/requestfactory/client/impl/JsonResults.java
/trunk/user/src/com/google/gwt/requestfactory/client/impl/ProxyJsoImpl.java
/trunk/user/test/com/google/gwt/requestfactory/RequestFactorySuite.java
=======================================
--- /dev/null
+++
/trunk/user/src/com/google/gwt/requestfactory/client/impl/json/ClientJsonUtil.java
Fri Sep 24 19:41:56 2010
@@ -0,0 +1,374 @@
+/*
+ * Copyright 2010 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may
not
+ * use this file except in compliance with the License. You may obtain a
copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
under
+ * the License.
+ */
+package com.google.gwt.requestfactory.client.impl.json;
+
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.JsArray;
+import com.google.gwt.regexp.shared.MatchResult;
+import com.google.gwt.regexp.shared.RegExp;
+import com.google.gwt.requestfactory.client.impl.ProxyJsoImpl;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Direct port of json2.js at http://www.json.org/json2.js to GWT.
+ */
+public class ClientJsonUtil {
+
+ /**
+ * Callback invoked during a RegExp replace for each match. The return
value
+ * is used as a substitution into the matched string.
+ */
+ private interface RegExpReplacer {
+
+ String replace(String match);
+ }
+
+ private static class StringifyJsonVisitor extends JsonVisitor {
+
+ private static final Set<String> skipKeys;
+
+ static {
+ Set<String> toSkip = new HashSet<String>();
+ toSkip.add("$H");
+ toSkip.add("__gwt_ObjectId");
+ toSkip.add(ProxyJsoImpl.REQUEST_FACTORY_FIELD);
+ toSkip.add(ProxyJsoImpl.SCHEMA_FIELD);
+ skipKeys = Collections.unmodifiableSet(toSkip);
+ }
+
+ private String indentLevel;
+
+ private Set<JavaScriptObject> visited;
+
+ private final String indent;
+
+ private final StringBuffer sb;
+
+ private final boolean pretty;
+
+ public StringifyJsonVisitor(String indent, StringBuffer sb,
+ boolean pretty) {
+ this.indent = indent;
+ this.sb = sb;
+ this.pretty = pretty;
+ indentLevel = indent;
+ visited = new HashSet<JavaScriptObject>();
+ }
+
+ @Override
+ public void endVisit(JsArray array, JsonContext ctx) {
+ if (pretty) {
+ indentLevel = indentLevel
+ .substring(0, indentLevel.length() - indent.length());
+ sb.append('\n');
+ sb.append(indentLevel);
+ }
+ sb.append("]");
+ }
+
+ @Override
+ public void endVisit(JsonMap object, JsonContext ctx) {
+ if (pretty) {
+ indentLevel = indentLevel
+ .substring(0, indentLevel.length() - indent.length());
+ sb.append('\n');
+ sb.append(indentLevel);
+ }
+ sb.append("}");
+ }
+
+ @Override
+ public void visit(double number, JsonContext ctx) {
+ sb.append(Double.isInfinite(number) ? "null" : format(number));
+ }
+
+ @Override
+ public void visit(String string, JsonContext ctx) {
+ sb.append(quote(string));
+ }
+
+ @Override
+ public void visit(boolean bool, JsonContext ctx) {
+ sb.append(bool);
+ }
+
+ @Override
+ public boolean visit(JsArray array, JsonContext ctx) {
+ checkCycle(array);
+ sb.append("[");
+ if (pretty) {
+ sb.append('\n');
+ indentLevel += indent;
+ sb.append(indentLevel);
+ }
+ return true;
+ }
+
+ @Override
+ public boolean visit(JsonMap object, JsonContext ctx) {
+ checkCycle(object);
+ sb.append("{");
+ if (pretty) {
+ sb.append('\n');
+ indentLevel += indent;
+ sb.append(indentLevel);
+ }
+ return true;
+ }
+
+ @Override
+ public boolean visitIndex(int index, JsonContext ctx) {
+ commaIfNotFirst(ctx);
+ return true;
+ }
+
+ @Override
+ public boolean visitKey(String key, JsonContext ctx) {
+ if ("".equals(key)) {
+ return true;
+ }
+ // skip properties injected by GWT runtime on JSOs
+ if (skipKeys.contains(key)) {
+ return false;
+ }
+ commaIfNotFirst(ctx);
+ sb.append(quote(key) + ":");
+ if (pretty) {
+ sb.append(' ');
+ }
+ return true;
+ }
+
+ @Override
+ public void visitNull(JsonContext ctx) {
+ sb.append("null");
+ }
+
+ private void checkCycle(JavaScriptObject array) {
+ if (visited.contains(array)) {
+ throw new JsonException("Cycled detected during stringify");
+ } else {
+ visited.add(array);
+ }
+ }
+
+ private void commaIfNotFirst(JsonContext ctx) {
+ if (!ctx.isFirst()) {
+ sb.append(",");
+ if (pretty) {
+ sb.append('\n');
+ sb.append(indentLevel);
+ }
+ }
+ }
+
+ private String format(double number) {
+ String n = String.valueOf(number);
+ if (n.endsWith(".0")) {
+ n = n.substring(0, n.length() - 2);
+ }
+ return n;
+ }
+ }
+
+ /**
+ * Convert special control characters into unicode escape format.
+ */
+ public static String escapeControlChars(String text) {
+ RegExp controlChars = RegExp.compile(
+ "[\\u0000\\u00ad\\u0600-\\u0604\\u070f\\u17b4\\u17b5\\u200c-\\u200f"
+
+ "\\u2028-\\u202f\\u2060-\\u206f\\ufeff\\ufff0-\\uffff]", "g");
+ return replace(controlChars, text, new RegExpReplacer() {
+ public String replace(String match) {
+ return escapeStringAsUnicode(match);
+ }
+ });
+ }
+
+ /**
+ * Safely parse a Json formatted String.
+ *
+ * @param text the json formatted string
+ * @return a JavaScriptObject representing a parsed Json string
+ */
+ public static JavaScriptObject parse(String text) {
+ /*
+ * Parsing happens in three stages. In the first stage, we replace
certain
+ * Unicode characters with escape sequences. JavaScript handles many
characters
+ * incorrectly, either silently deleting them, or treating them as
line endings.
+ */
+ text = escapeControlChars(text);
+ /*
+ *In the second stage, we run the text against regular expressions
that look
+ * for non-JSON patterns. We are especially concerned with '()'
and 'new'
+ * because they can cause invocation, and '=' because it can cause
mutation.
+ * But just to be safe, we want to reject all unexpected forms.
+ */
+ /*
+ * We split the second stage into 4 regexp operations in order to work
around
+ * crippling inefficiencies in IE's and Safari's regexp engines. First
we
+ * replace the JSON backslash pairs with '@' (a non-JSON character).
Second, we
+ * replace all simple value tokens with ']' characters. Third, we
delete all
+ * open brackets that follow a colon or comma or that begin the text.
Finally,
+ * we look to see that the remaining characters are only whitespace
or ']' or
+ * ',' or ':' or '{' or '}'. If that is so, then the text is safe for
eval.
+ */
+ String chktext = text
+ .replaceAll("\\\\(?:[\"\\\\\\/bfnrt]|u[0-9a-fA-F]{4})", "@");
+ chktext = chktext.replaceAll(
+ "\"[^\"\\\\\\n\\r]*\"|true|false|null|
-?\\d+(?:\\.\\d*)?(?:[eE][+\\-]?\\d+)?",
+ "]");
+ chktext = chktext.replaceAll("(?:^|:|,)(?:\\s*\\[)+", "");
+
+ if (chktext.matches("^[\\],:{}\\s]*$")) {
+ /*
+ * In the third stage we use the eval function to compile the text
into a
+ * JavaScript structure. The '{' operator is subject to a syntactic
ambiguity
+ * in JavaScript: it can begin a block or an object literal. We wrap
the text
+ * in parens to eliminate the ambiguity.
+ */
+ return eval('(' + text + ')');
+ }
+ throw new JsonException(
+ "Unable to parse " + text + " illegal JSON format.");
+ }
+
+ /**
+ * Converts a JSO to Json format.
+ *
+ * @param jso javascript object to stringify
+ * @return json formatted string
+ */
+ public static String stringify(JavaScriptObject jso) {
+ return stringify(jso, 0);
+ }
+
+ /**
+ * Converts a JSO to Json format.
+ *
+ * @param jso javascript object to stringify
+ * @param spaces number of spaces to indent in pretty print mode
+ * @return json formatted string
+ */
+ public static String stringify(JavaScriptObject jso, int spaces) {
+ StringBuffer sb = new StringBuffer();
+ for (int i = 0; i < spaces; i++) {
+ sb.append(' ');
+ }
+ return stringify(jso, sb.toString());
+ }
+
+ /**
+ * Converts a JSO to Json format.
+ *
+ * @param jso javascript object to stringify
+ * @param indent optional indention prefix for pretty printing
+ * @return json formatted string
+ */
+ public static String stringify(JavaScriptObject jso, final String
indent) {
+ final StringBuffer sb = new StringBuffer();
+ final boolean isPretty = indent != null && !"".equals(indent);
+
+ new StringifyJsonVisitor(indent, sb, isPretty).accept(jso);
+ boolean isArray = isArray(jso);
+ char openChar = isArray ? '[' : '{';
+ char closeChar = isArray ? ']' : '}';
+ return openChar + (isPretty ? "\n" + indent : "") + sb.toString()
+ + (isPretty ? "\n" : "") + closeChar;
+ }
+
+ static native boolean isArray(JavaScriptObject obj) /*-{
+ return Object.prototype.toString.apply(obj) === '[object Array]';
+ }-*/;
+
+ /**
+ * Safely escape an arbitrary string as a JSON string literal.
+ */
+ static String quote(String value) {
+ RegExp escapeable = RegExp.compile(
+ "[\\\\\\\"\\x00-\\x1f\\x7f-\\x9f\\u00ad\\u0600-\\u0604\\u070f\\u17b4"
+ + "\\u17b5\\u200c-\\u200f\\u2028-\\u202f\\u2060-\\u206f\\ufeff"
+ + "\\ufff0-\\uffff]", "g");
+ if (escapeable.test(value)) {
+ return "\"" + replace(escapeable, value, new RegExpReplacer() {
+ public String replace(String match) {
+ char a = match.charAt(0);
+ switch (a) {
+ case '\b':
+ return "\\b";
+ case '\t':
+ return "\\t";
+ case '\n':
+ return "\\n";
+ case '\f':
+ return "\\f";
+ case '\r':
+ return "\\r";
+ case '"':
+ return "\\\"";
+ case '\\':
+ return "\\\\";
+ default:
+ return escapeStringAsUnicode(match);
+ }
+ }
+ }) + "\"";
+ } else {
+ return "\"" + value + "\"";
+ }
+ }
+
+ /**
+ * Turn a single unicode character into a 32-bit unicode hex literal.
+ */
+ private static String escapeStringAsUnicode(String match) {
+ String hexValue = Integer.toString(match.charAt(0), 16);
+ hexValue = hexValue.length() > 4 ?
hexValue.substring(hexValue.length() - 4)
+ : hexValue;
+ return "\\u0000" + hexValue;
+ }
+
+ private static native JavaScriptObject eval(String text) /*-{
+ return eval(text);
+ }-*/;
+
+ /**
+ * Execute a regular expression and invoke a callback for each match
+ * occurance. The return value of the callback is substituted for the
match.
+ *
+ * @param expression a compiled regular expression
+ * @param text a String on which to perform replacement
+ * @param replacer a callback that maps matched strings into new values
+ */
+ private static String replace(RegExp expression, String text,
+ RegExpReplacer replacer) {
+ expression.setLastIndex(0);
+ MatchResult mresult = expression.exec(text);
+ StringBuffer toReturn = new StringBuffer();
+ int lastIndex = 0;
+ while (mresult != null) {
+ toReturn.append(text.substring(lastIndex, mresult.getIndex()));
+ toReturn.append(replacer.replace(mresult.getGroup(0)));
+ lastIndex = mresult.getIndex() + 1;
+ mresult = expression.exec(text);
+ }
+ toReturn.append(text.substring(lastIndex));
+ return toReturn.toString();
+ }
+}
=======================================
--- /dev/null
+++
/trunk/user/src/com/google/gwt/requestfactory/client/impl/json/JsonArrayContext.java
Fri Sep 24 19:41:56 2010
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2010 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may
not
+ * use this file except in compliance with the License. You may obtain a
copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
under
+ * the License.
+ */
+package com.google.gwt.requestfactory.client.impl.json;
+
+import com.google.gwt.core.client.JavaScriptObject;
+
+/**
+ * A {...@link JsonContext} with integer based location context.
+ */
+class JsonArrayContext extends JsonContext {
+
+ int currentIndex = 0;
+
+ JsonArrayContext(JavaScriptObject jso) {
+ super(jso);
+ }
+
+ public int getCurrentIndex() {
+ return currentIndex;
+ }
+
+ @Override
+ public native void removeMe() /*-{
+ delete
[email protected]::getJso()()[th...@com.google.gwt.requestfactory.client.impl.json.jsonarraycontext::getCurrentIndex()()];
+ }-*/;
+
+ @Override
+ public native void replaceMe(double d) /*-{
+
[email protected]::getJso()()[th...@com.google.gwt.requestfactory.client.impl.json.jsonarraycontext::getCurrentIndex()()]
= d;
+ }-*/;
+
+ @Override
+ public native void replaceMe(String d) /*-{
+
[email protected]::getJso()()[th...@com.google.gwt.requestfactory.client.impl.json.jsonarraycontext::getCurrentIndex()()]
= d;
+ }-*/;
+
+ @Override
+ public native void replaceMe(boolean d) /*-{
+
[email protected]::getJso()()[th...@com.google.gwt.requestfactory.client.impl.json.jsonarraycontext::getCurrentIndex()()]
= d;
+ }-*/;
+
+ @Override
+ public native void replaceMe(JavaScriptObject jso) /*-{
+
[email protected]::getJso()()[th...@com.google.gwt.requestfactory.client.impl.json.jsonarraycontext::getCurrentIndex()()]
= jso;
+ }-*/;
+
+ public void setCurrentIndex(int currentIndex) {
+ this.currentIndex = currentIndex;
+ }
+}
=======================================
--- /dev/null
+++
/trunk/user/src/com/google/gwt/requestfactory/client/impl/json/JsonContext.java
Fri Sep 24 19:41:56 2010
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2010 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may
not
+ * use this file except in compliance with the License. You may obtain a
copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
under
+ * the License.
+ */
+package com.google.gwt.requestfactory.client.impl.json;
+
+import com.google.gwt.core.client.JavaScriptObject;
+
+/**
+ * Represents the current location where a value is stored on a JSO, and
allows
+ * the value's replacement or deletion.
+ */
+abstract class JsonContext {
+
+ private JavaScriptObject jso;
+
+ private boolean isFirst = true;
+
+ JsonContext(JavaScriptObject jso) {
+ this.jso = jso;
+ }
+
+ /**
+ * Return the underlying JavaScriptObject (Array or Object) that backs
the
+ * context.
+ */
+ public JavaScriptObject getJso() {
+ return jso;
+ }
+
+ /**
+ * Whether or not the current context location within the JSO is the
first
+ * key or array index.
+ */
+ public boolean isFirst() {
+ return isFirst;
+ }
+
+ /**
+ * Remove the current array index or key from the underlying JSO.
+ */
+ public abstract void removeMe();
+
+ /**
+ * Replace the current location's value with a double.
+ */
+ public abstract void replaceMe(double d);
+
+ /**
+ * Replace the current location's value with a String.
+ */
+ public abstract void replaceMe(String d);
+
+ /**
+ * Replace the current location's value with a boolean.
+ */
+ public abstract void replaceMe(boolean d);
+
+ /**
+ * Replace the current location's value with a JSO.
+ */
+ public abstract void replaceMe(JavaScriptObject jso);
+
+ void setFirst(boolean first) {
+ isFirst = first;
+ }
+
+ void setJso(JavaScriptObject jso) {
+ this.jso = jso;
+ }
+}
=======================================
--- /dev/null
+++
/trunk/user/src/com/google/gwt/requestfactory/client/impl/json/JsonException.java
Fri Sep 24 19:41:56 2010
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2010 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may
not
+ * use this file except in compliance with the License. You may obtain a
copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
under
+ * the License.
+ */
+package com.google.gwt.requestfactory.client.impl.json;
+
+/**
+ * Exception thrown during JSON operations such as visiting, parsing, or
+ * stringifying.
+ */
+class JsonException extends RuntimeException {
+
+ public JsonException(String s, Throwable throwable) {
+ super(s, throwable);
+ }
+
+ public JsonException(String s) {
+ super(s);
+ }
+}
=======================================
--- /dev/null
+++
/trunk/user/src/com/google/gwt/requestfactory/client/impl/json/JsonMap.java
Fri Sep 24 19:41:56 2010
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2010 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may
not
+ * use this file except in compliance with the License. You may obtain a
copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
under
+ * the License.
+ */
+package com.google.gwt.requestfactory.client.impl.json;
+
+import com.google.gwt.core.client.JavaScriptObject;
+
+/**
+ * Utility JSO for operating on object keys and values.
+ */
+final class JsonMap extends JavaScriptObject {
+
+ protected JsonMap() {
+ }
+
+ public native JavaScriptObject get(String key) /*-{
+ return this[key];
+ }-*/;
+
+ public native boolean getBoolean(String key) /*-{
+ return this[key];
+ }-*/;
+
+ public native double getDouble(String key) /*-{
+ return this[key];
+ }-*/;
+
+ public native void put(String key, JavaScriptObject jso) /*-{
+ this[key] = jso;
+ }-*/;
+}
=======================================
--- /dev/null
+++
/trunk/user/src/com/google/gwt/requestfactory/client/impl/json/JsonMapContext.java
Fri Sep 24 19:41:56 2010
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2010 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may
not
+ * use this file except in compliance with the License. You may obtain a
copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
under
+ * the License.
+ */
+package com.google.gwt.requestfactory.client.impl.json;
+
+import com.google.gwt.core.client.JavaScriptObject;
+
+/**
+ * A {...@link JsonContext} with String based location index.
+ */
+class JsonMapContext extends JsonContext {
+
+ String currentKey;
+
+ JsonMapContext(JavaScriptObject jso) {
+ super(jso);
+ }
+
+ public String getCurrentKey() {
+ return currentKey;
+ }
+
+ @Override
+ public native void removeMe() /*-{
+ delete
[email protected]::getJso()()[th...@com.google.gwt.requestfactory.client.impl.json.jsonmapcontext::getCurrentKey()()];
+ }-*/;
+
+ @Override
+ public native void replaceMe(double d) /*-{
+
[email protected]::getJso()()[th...@com.google.gwt.requestfactory.client.impl.json.jsonmapcontext::getCurrentKey()()]
= d;
+ }-*/;
+
+ @Override
+ public native void replaceMe(String d) /*-{
+
[email protected]::getJso()()[th...@com.google.gwt.requestfactory.client.impl.json.jsonmapcontext::getCurrentKey()()]
= d;
+ }-*/;
+
+ @Override
+ public native void replaceMe(boolean d) /*-{
+
[email protected]::getJso()()[th...@com.google.gwt.requestfactory.client.impl.json.jsonmapcontext::getCurrentKey()()]
= d;
+ }-*/;
+
+ @Override
+ public native void replaceMe(JavaScriptObject jso) /*-{
+
[email protected]::getJso()()[th...@com.google.gwt.requestfactory.client.impl.json.jsonmapcontext::getCurrentKey()()]
= jso;
+ }-*/;
+
+ public void setCurrentKey(String currentKey) {
+ this.currentKey = currentKey;
+ }
+}
=======================================
--- /dev/null
+++
/trunk/user/src/com/google/gwt/requestfactory/client/impl/json/JsonVisitor.java
Fri Sep 24 19:41:56 2010
@@ -0,0 +1,188 @@
+/*
+ * Copyright 2010 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may
not
+ * use this file except in compliance with the License. You may obtain a
copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
under
+ * the License.
+ */
+package com.google.gwt.requestfactory.client.impl.json;
+
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.JsArray;
+
+/**
+ * A visitor for JSON objects. For each unique JSON datatype, a callback is
+ * invoked with a {...@link
com.google.gwt.requestfactory.client.impl.json.JsonContext}
+ * that can be used to replace a value or remove it. For Object and Array
+ * types, the {...@link #visitKey} and {...@link #visitIndex} methods are
invoked
+ * respectively for each contained value to determine if they should be
+ * processed or not. Finally, the visit methods for Object and Array types
+ * returns a boolean that determines whether or not to process its
contained
+ * values.
+ */
+class JsonVisitor {
+
+ /**
+ * Accept a JS array or JS object type and visit its members.
+ * @param jso
+ */
+ public void accept(JavaScriptObject jso) {
+ if (ClientJsonUtil.isArray(jso)) {
+ acceptArray(jso);
+ } else {
+ acceptMap(jso);
+ }
+ }
+
+ /**
+ * Called after every element of array has been visited.
+ */
+ public void endVisit(JsArray array, JsonContext ctx) {
+ }
+
+ /**
+ * Called after every field of an object has been visited.
+ * @param object
+ * @param ctx
+ */
+ public void endVisit(JsonMap object, JsonContext ctx) {
+ }
+
+ /**
+ * Called for JS numbers present in a JSON object.
+ */
+ public void visit(double number, JsonContext ctx) {
+ }
+
+ /**
+ * Called for JS strings present in a JSON object.
+ */
+ public void visit(String string, JsonContext ctx) {
+ }
+
+ /**
+ * Called for JS boolean present in a JSON object.
+ */
+ public void visit(boolean bool, JsonContext ctx) {
+ }
+
+ /**
+ * Called for JS arrays present in a JSON object. Return true if array
+ * elements should be visited.
+ * @param array a JS array
+ * @param ctx a context to replace or delete the array
+ * @return true if the array elements should be visited
+ */
+ public boolean visit(JsArray array, JsonContext ctx) {
+ return true;
+ }
+
+ /**
+ * Called for JS objects present in a JSON object. Return true if object
+ * fields should be visited.
+ * @param object a Json object
+ * @param ctx a context to replace or delete the object
+ * @return true if object fields should be visited
+ */
+ public boolean visit(JsonMap object, JsonContext ctx) {
+ return true;
+ }
+
+ /**
+ * Return true if the value for a given array index should be visited.
+ * @param index an index in a JSON array
+ * @param ctx a context object used to delete or replace values
+ * @return true if the value associated with the index should be visited
+ */
+ public boolean visitIndex(int index, JsonContext ctx) {
+ return true;
+ }
+
+ /**
+ * Return true if the value for a given object key should be visited.
+ * @param key a key in a JSON object
+ * @param ctx a context object used to delete or replace values
+ * @return true if the value associated with the key should be visited
+ */
+ public boolean visitKey(String key, JsonContext ctx) {
+ return true;
+ }
+
+ /**
+ * Called for nulls present in a JSON object.
+ */
+ public void visitNull(JsonContext ctx) {
+ }
+
+ protected void acceptArray(JavaScriptObject jso) {
+ acceptNative(new JsonArrayContext(jso));
+ }
+
+ protected void acceptMap(JavaScriptObject jso) {
+ acceptNative(new JsonMapContext(jso));
+ }
+
+ /*
+ * Implemented purely natively as DevMode would force wrapping /
unwrapping
+ * all return values otherwise.
+ */
+ protected native void acceptNative(JsonContext context) /*-{
+ var thejso =
conte...@com.google.gwt.requestfactory.client.impl.json.jsoncontext::getJso()();
+ var isArray =
@com.google.gwt.requestfactory.client.impl.json.ClientJsonUtil::isArray(Lcom/google/gwt/core/client/JavaScriptObject;)(thejso);
+
+ function dispatch(v, ctx, value) {
+
+ var type = typeof value;
+ if (value == null || type == 'null') {
+
[email protected]::visitNull(Lcom/google/gwt/requestfactory/client/impl/json/JsonContext;)(ctx)
+ } else if (type == 'number') {
+
[email protected]::visit(DLcom/google/gwt/requestfactory/client/impl/json/JsonContext;)(value,
ctx);
+ } else if (type == 'string') {
+
[email protected]::visit(Ljava/lang/String;Lcom/google/gwt/requestfactory/client/impl/json/JsonContext;)(value,
ctx);
+ } else if (type == 'boolean') {
+
[email protected]::visit(ZLcom/google/gwt/requestfactory/client/impl/json/JsonContext;)(value,
ctx);
+ } else if (type == 'object') {
+ if
(@com.google.gwt.requestfactory.client.impl.json.ClientJsonUtil::isArray(Lcom/google/gwt/core/client/JavaScriptObject;)(value))
{
+ if
([email protected]::visit(Lcom/google/gwt/core/client/JsArray;Lcom/google/gwt/requestfactory/client/impl/json/JsonContext;)(value,
ctx)) {
+
[email protected]::acceptArray(Lcom/google/gwt/core/client/JavaScriptObject;)(value);
+ }
+
[email protected]::endVisit(Lcom/google/gwt/core/client/JsArray;Lcom/google/gwt/requestfactory/client/impl/json/JsonContext;)(value,
ctx)
+ } else {
+ if
([email protected]::visit(Lcom/google/gwt/requestfactory/client/impl/json/JsonMap;Lcom/google/gwt/requestfactory/client/impl/json/JsonContext;)(value,
ctx)) {
+
[email protected]::acceptMap(Lcom/google/gwt/core/client/JavaScriptObject;)(value);
+ }
+
[email protected]::endVisit(Lcom/google/gwt/requestfactory/client/impl/json/JsonMap;Lcom/google/gwt/requestfactory/client/impl/json/JsonContext;)(value,
ctx)
+ }
+ }
+
[email protected]::setFirst(Z)(false);
+ }
+
+ var value;
+ if (isArray) {
+ for (var i = 0; i < thejso.length; i++) {
+
conte...@com.google.gwt.requestfactory.client.impl.json.jsonarraycontext::setCurrentIndex(I)(i);
+ value = thejso[i];
+ if
([email protected]::visitIndex(ILcom/google/gwt/requestfactory/client/impl/json/JsonContext;)(i,
context)) {
+ dispatch(this, context, value);
+ }
+ }
+ } else {
+ for (var prop in thejso) {
+
conte...@com.google.gwt.requestfactory.client.impl.json.jsonmapcontext::setCurrentKey(Ljava/lang/String;)(prop);
+ value = thejso[prop];
+ if (thejso.hasOwnProperty(prop)
+ &&
[email protected]::visitKey(Ljava/lang/String;Lcom/google/gwt/requestfactory/client/impl/json/JsonContext;)(prop,
context)) {
+ dispatch(this, context, value);
+ }
+ }
+ }
+ }-*/;
+}
=======================================
--- /dev/null
+++
/trunk/user/test/com/google/gwt/requestfactory/client/impl/json/ClientJsonUtilTest.java
Fri Sep 24 19:41:56 2010
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2010 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may
not
+ * use this file except in compliance with the License. You may obtain a
copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
under
+ * the License.
+ */
+package com.google.gwt.requestfactory.client.impl.json;
+
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.junit.client.GWTTestCase;
+import com.google.gwt.requestfactory.client.impl.ProxyJsoImpl;
+
+/**
+ * Tests for {...@link ClientJsonUtil}
+ */
+public class ClientJsonUtilTest extends GWTTestCase {
+
+ @Override
+ public String getModuleName() {
+ return "com.google.gwt.requestfactory.RequestFactorySuite";
+ }
+
+
+ public void testEscapeControlChars() {
+ String unicodeString = "\u2060Test\ufeffis a test\u17b5";
+ assertEquals("\\u00002060Test\\u0000feffis a test\\u000017b5",
+ ClientJsonUtil.escapeControlChars(unicodeString));
+ }
+
+ public void testQuote() {
+ String badString = "\bThis\"is\ufeff\ta\\bad\nstring\u2029\u2029";
+ assertEquals("\"\\bThis\\\"is\\u0000feff\\ta\\\\bad\\nstring"
+ + "\\u00002029\\u00002029\"", ClientJsonUtil.quote(badString));
+ }
+
+ public void testLegalParse() {
+ JavaScriptObject obj = ClientJsonUtil.parse(
+ "{ \"a\":1, \"b\":\"hello\", \"c\": true,"
+ + "\"d\": null, \"e\": [1,2,3,4], \"f\": {} }");
+ assertNotNull(obj);
+ }
+
+ public void testIllegalParse() {
+ try {
+ ClientJsonUtil.parse("{ \"a\": new String() }");
+ fail("Expected JsonException to be thrown");
+ } catch (JsonException je) {
+ }
+ }
+
+ public void testStringify() {
+ String json = "{\"a\":1,\"b\":\"hello\",\"c\":true,"
+ + "\"d\":null,\"e\":[1,2,3,4],\"f\":{\"x\":1}}";
+ assertEquals(json,
ClientJsonUtil.stringify(ClientJsonUtil.parse(json)));
+ }
+
+ public void testStringifyCycle() {
+ String json = "{\"a\":1,\"b\":\"hello\",\"c\":true,"
+ + "\"d\":null,\"e\":[1,2,3,4],\"f\":{\"x\":1}}";
+ JsonMap jso = ClientJsonUtil.parse(json).cast();
+ jso.put("cycle", jso);
+ try {
+ ClientJsonUtil.stringify(jso);
+ fail("Expected JsonException for object cycle");
+ } catch (JsonException je) {
+ }
+ }
+
+ public void testStringifyIndent() {
+ // test string taken from native Chrome window.JSON.stringify
+ String json = "{\n" + " \"a\": 1,\n" + " \"b\": \"hello\",\n"
+ + " \"c\": true,\n" + " \"d\": null,\n" + " \"e\": [\n" + "
1,\n"
+ + " 2,\n" + " 3,\n" + " 4\n" + " ],\n" + " \"f\": {\n"
+ + " \"x\": 1\n" + " }\n" + "}";
+ assertEquals(json,
ClientJsonUtil.stringify(ClientJsonUtil.parse(json), 2));
+ }
+
+ public void testStringifySkipKeys() {
+ String expectedJson = "{\"a\":1,\"b\":\"hello\",\"c\":true,"
+ + "\"d\":null,\"e\":[1,2,3,4],\"f\":{\"x\":1}}";
+ String json = "{\"a\":1,\"b\":\"hello\",\"c\":true,"
+ + "\"" + ProxyJsoImpl.REQUEST_FACTORY_FIELD + "\": 1,"
+ + "\"" + ProxyJsoImpl.SCHEMA_FIELD + "\": 1,"
+ + "\"$H\": 1,"
+ + "\"__gwt_ObjectId\": 1,"
+ + "\"d\":null,\"e\":[1,2,3,4],\"f\":{\"x\":1}}";
+ assertEquals(expectedJson, ClientJsonUtil.stringify(
+ ClientJsonUtil.parse(json)));
+ }
+}
=======================================
---
/trunk/user/src/com/google/gwt/requestfactory/client/impl/JsonResults.java
Fri Sep 24 12:10:50 2010
+++
/trunk/user/src/com/google/gwt/requestfactory/client/impl/JsonResults.java
Fri Sep 24 19:41:56 2010
@@ -17,17 +17,16 @@
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.core.client.JsArray;
+import com.google.gwt.requestfactory.client.impl.json.ClientJsonUtil;
/**
* JSO to hold result and related objects.
*/
class JsonResults extends JavaScriptObject {
- static native JsonResults fromResults(String json) /*-{
- // TODO: clean this
- eval("xyz=" + json);
- return xyz;
- }-*/;
+ static JsonResults fromResults(String json) {
+ return ClientJsonUtil.parse(json).cast();
+ }
protected JsonResults() {
}
=======================================
---
/trunk/user/src/com/google/gwt/requestfactory/client/impl/ProxyJsoImpl.java
Fri Sep 24 12:10:50 2010
+++
/trunk/user/src/com/google/gwt/requestfactory/client/impl/ProxyJsoImpl.java
Fri Sep 24 19:41:56 2010
@@ -45,6 +45,10 @@
*/
public class ProxyJsoImpl extends JavaScriptObject implements EntityProxy {
+ public static final String REQUEST_FACTORY_FIELD = "__rf";
+
+ public static final String SCHEMA_FIELD = "__key";
+
public static ProxyJsoImpl create(JavaScriptObject
rawJsoWithIdAndVersion,
ProxySchema<?> schema, RequestFactoryJsonImpl requestFactory) {
@@ -268,11 +272,11 @@
}
public final native RequestFactoryJsonImpl getRequestFactory() /*-{
- return this['__rf'];
+ return
[email protected]::REQUEST_FACTORY_FIELD];
}-*/;
public final native ProxySchema<?> getSchema() /*-{
- return this['__key'];
+ return
[email protected]::SCHEMA_FIELD];
}-*/;
public final native boolean isDefined(String name)/*-{
@@ -392,27 +396,7 @@
* @return returned string.
*/
public final native String toJson() /*-{
- // Safari 4.0.5 appears not to honor the replacer argument, so we
can't do this:
-
- // var replacer = function(key, value) {
- // if (key == '__key') {
- // return;
- // }
- // return value;
- // }
- // return $wnd.JSON.stringify(this, replacer);
-
- var key = this.__key;
- delete this.__key;
- var rf = this.__rf;
- delete this.__rf;
- var gwt = this.__gwt_ObjectId;
- delete this.__gwt_ObjectId;
- // TODO verify that the stringify() from json2.js works on IE
- var rtn = $wnd.JSON.stringify(this);
- this.__key = key;
- this.__rf = rf;
- this.__gwt_ObjectId = gwt;
+ var rtn =
@com.google.gwt.requestfactory.client.impl.json.ClientJsonUtil::stringify(Lcom/google/gwt/core/client/JavaScriptObject;)(this);
return rtn;
}-*/;
@@ -492,11 +476,11 @@
}-*/;
private native void setRequestFactory(RequestFactoryJsonImpl
requestFactory) /*-{
- this['__rf'] = requestFactory;
+
[email protected]::REQUEST_FACTORY_FIELD]
= requestFactory;
}-*/;
private native void setSchema(ProxySchema<?> schema) /*-{
- this['__key'] = schema;
+
[email protected]::SCHEMA_FIELD]
= schema;
}-*/;
private native void setString(String name, String value) /*-{
=======================================
--- /trunk/user/test/com/google/gwt/requestfactory/RequestFactorySuite.java
Tue Sep 21 17:22:41 2010
+++ /trunk/user/test/com/google/gwt/requestfactory/RequestFactorySuite.java
Fri Sep 24 19:41:56 2010
@@ -24,6 +24,7 @@
import
com.google.gwt.requestfactory.client.impl.DeltaValueStoreJsonImplTest;
import com.google.gwt.requestfactory.client.impl.ProxyJsoImplTest;
import com.google.gwt.requestfactory.client.impl.ValueStoreJsonImplTest;
+import com.google.gwt.requestfactory.client.impl.json.ClientJsonUtilTest;
import junit.framework.Test;
@@ -42,6 +43,7 @@
suite.addTestSuite(RequestFactoryStringTest.class);
suite.addTestSuite(RequestFactoryExceptionHandlerTest.class);
suite.addTestSuite(FindServiceTest.class);
+ suite.addTestSuite(ClientJsonUtilTest.class);
return suite;
}
}
--
http://groups.google.com/group/Google-Web-Toolkit-Contributors