Fixes issue #1162 and issue #1128; JSON classes now defensively ensure the JavaScript string used as a key is actually a primitive when access the backStore JSO.  This was an issue particularly on FF.  Includes tests to verify the behavior.

Patch by: bobv, scottb
Review by: knorton, scottb
Found by: colesbury, hoosiemama, joerg.baumann


git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@1250 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/user/src/com/google/gwt/json/client/JSONObject.java b/user/src/com/google/gwt/json/client/JSONObject.java
index 9874b41..beb8824 100644
--- a/user/src/com/google/gwt/json/client/JSONObject.java
+++ b/user/src/com/google/gwt/json/client/JSONObject.java
@@ -25,12 +25,49 @@
  * set of {@link com.google.gwt.json.client.JSONValue} objects.
  */
 public class JSONObject extends JSONValue {
+
+  /**
+   * A sentinel value for {@link #removeBack(JavaScriptObject, String)} to
+   * return when an entry does not exist.
+   */
+  private static JavaScriptObject UNDEFINED = JavaScriptObject.createObject();
+
+  private static native void addAllKeysFromJavascriptObject(Set s,
+      JavaScriptObject javaScriptObject) /*-{
+    for(var key in javaScriptObject) {
+      s.@java.util.Set::add(Ljava/lang/Object;)(key);
+    }
+  }-*/;
+
+  private static native JSONValue getFront(JavaScriptObject frontStore, String key) /*-{
+    key = String(key);
+    return Object.prototype.hasOwnProperty.call(frontStore, key) ? frontStore[key] : null;
+  }-*/;
+
+  private static native void putFront(JavaScriptObject frontStore, String key,
+      JSONValue jsonValue) /*-{
+    frontStore[String(key)] = jsonValue;
+  }-*/;
+
+  private static native JavaScriptObject removeBack(JavaScriptObject backStore, String key) /*-{
+    key = String(key);
+    if (Object.prototype.hasOwnProperty.call(backStore, key)) {
+      var x = backStore[key];
+      delete backStore[key];
+      if (typeof x != 'object') {
+        x = Object(x); 
+      }
+      return x;
+    }
+    return @com.google.gwt.json.client.JSONObject::UNDEFINED;
+  }-*/;
+
   private final JavaScriptObject backStore;
 
-  private final JavaScriptObject frontStore = createBlankObject();
+  private final JavaScriptObject frontStore = JavaScriptObject.createObject();
 
   public JSONObject() {
-    backStore = createBlankObject();
+    backStore = JavaScriptObject.createObject();
   }
 
   /**
@@ -43,60 +80,49 @@
   /**
    * Tests whether or not this JSONObject contains the specified key.
    * 
-   * We use Object.hasOwnProperty here to verify that a given key is
-   * specified on this object rather than a superclass (such as standard
-   * properties defined on Object).
+   * We use Object.hasOwnProperty here to verify that a given key is specified
+   * on this object rather than a superclass (such as standard properties
+   * defined on Object).
    * 
    * @param key the key to search for
    * @return <code>true</code> if the JSONObject contains the specified key
    */
-  public native boolean containsKey(String key) /*-{
-    return Object.prototype.hasOwnProperty.call(
-        this.@com.google.gwt.json.client.JSONObject::backStore, key)
-        || Object.prototype.hasOwnProperty.call(
-        this.@com.google.gwt.json.client.JSONObject::frontStore, key);
-  }-*/;
+  public boolean containsKey(String key) {
+    return get(key) != null;
+  }
 
   /**
    * Gets the JSONValue associated with the specified key.
    * 
-   * We use Object.hasOwnProperty here to verify that a given key is
-   * specified on this object rather than a superclass (such as standard
-   * properties defined on Object).
+   * We use Object.hasOwnProperty here to verify that a given key is specified
+   * on this object rather than a superclass (such as standard properties
+   * defined on Object).
    * 
    * @param key the key to search for
    * @return if found, the value associated with the specified key, or
    *         <code>null</code> otherwise
    */
-  public native JSONValue get(String key) /*-{
-    if (Object.prototype.hasOwnProperty.call(
-        this.@com.google.gwt.json.client.JSONObject::backStore, key))
-    {
-      var x = this.@com.google.gwt.json.client.JSONObject::backStore[key];
-      if (typeof x == 'number' || typeof x == 'string' || typeof x == 'array'
-          || typeof x == 'boolean')
-      {
-        x = Object(x); 
+  public JSONValue get(String key) {
+    if (key == null) {
+      return null;
+    }
+    JSONValue result = getFront(frontStore, key);
+    if (result == null) {
+      JavaScriptObject jso = removeBack(backStore, key);
+      if (jso != UNDEFINED) {
+        result = JSONParser.buildValue(jso);
+        putFront(frontStore, key, result);
       }
-      this.@com.google.gwt.json.client.JSONObject::frontStore[key]=
-          // don't linebreak the next line
-          @com.google.gwt.json.client.JSONParser::buildValue(Lcom/google/gwt/core/client/JavaScriptObject;)(x);
-      delete this.@com.google.gwt.json.client.JSONObject::backStore[key];
     }
-    if (Object.prototype.hasOwnProperty.call(
-        this.@com.google.gwt.json.client.JSONObject::frontStore, key))
-    {
-      return this.@com.google.gwt.json.client.JSONObject::frontStore[key];
-    }
-    return null;
-  }-*/;
+    return result;
+  }
 
   /**
    * Returns <code>this</code>, as this is a JSONObject.
    */
   public JSONObject isObject() {
     return this;
-  }
+  };
 
   /**
    * Returns keys for which this JSONObject has associations.
@@ -115,16 +141,19 @@
    * specified key already has an associated value, it is overwritten.
    * 
    * @param key the key to associate with the specified value
-   * @param jsonValue the value to assoociate with this key
+   * @param jsonValue the value to associate with this key
    * @return if one existed, the previous value associated with the key, or
    *         <code>null</code> otherwise
+   * @throws NullPointerException if key is <code>null</code>
    */
-  public native JSONValue put(String key, JSONValue jsonValue) /*-{
-    var out =   // can't break next line
-      this.@com.google.gwt.json.client.JSONObject::get(Ljava/lang/String;)(key);
-    this.@com.google.gwt.json.client.JSONObject::frontStore[key] = jsonValue;
-    return out;
-  }-*/;
+  public JSONValue put(String key, JSONValue jsonValue) {
+    if (key == null) {
+      throw new NullPointerException();
+    }
+    JSONValue previous = get(key);
+    putFront(frontStore, key, jsonValue);
+    return previous;
+  }
 
   /**
    * Determines the number of keys on this object.
@@ -164,15 +193,4 @@
     out.push("}")
     return out.join("");
   }-*/;
-
-  private native void addAllKeysFromJavascriptObject(Set s,
-      JavaScriptObject javaScriptObject) /*-{
-    for(var key in javaScriptObject) {
-     s.@java.util.Set::add(Ljava/lang/Object;)(key);
-    }
-  }-*/;
-
-  private native JavaScriptObject createBlankObject() /*-{
-    return {};
-  }-*/;
 }
diff --git a/user/test/com/google/gwt/json/client/JSONTest.java b/user/test/com/google/gwt/json/client/JSONTest.java
index 0da0d49..29d5532 100644
--- a/user/test/com/google/gwt/json/client/JSONTest.java
+++ b/user/test/com/google/gwt/json/client/JSONTest.java
@@ -301,16 +301,16 @@
     JSONObject j2 = new JSONObject();
     j2.put("test1", new JSONString(""));
 
-    JSONObject j2_2 = new JSONObject();
-    j2_2.put("test1_2", new JSONString(""));
-    j2.put("j2_2", j2_2);
+    JSONObject j22 = new JSONObject();
+    j22.put("test12", new JSONString(""));
+    j2.put("j22", j22);
 
     JSONObject j3 = new JSONObject();
     j3.put("j1", j1);
     j3.put("j2", j2);
 
     assertEquals(
-        "{\"j1\":{\"test1\":\"\"}, \"j2\":{\"test1\":\"\", \"j2_2\":{\"test1_2\":\"\"}}}",
+        "{\"j1\":{\"test1\":\"\"}, \"j2\":{\"test1\":\"\", \"j22\":{\"test12\":\"\"}}}",
         j3.toString());
   }
 
@@ -328,6 +328,17 @@
     checkRoundTripJsonText("\"hel\\\\\\\"lo\"", "hel\\\"lo");
   }
 
+  public void testStringTypes() {
+    JSONObject object = JSONParser.parse("{\"a\":\"b\",\"null\":\"foo\"}").isObject();
+    assertNotNull(object);
+
+    assertEquals("b", object.get(stringAsPrimitive("a")).isString().stringValue());
+    assertEquals("b", object.get(stringAsObject("a")).isString().stringValue());
+    assertEquals("foo", object.get(stringAsPrimitive("null")).isString().stringValue());
+    assertEquals("foo", object.get(stringAsObject("null")).isString().stringValue());
+    assertNull(object.get(null));
+  }
+
   public void testWidget() {
     JSONObject v = (JSONObject) JSONParser.parse(widgetTest);
     JSONObject widget = (JSONObject) v.get("widget");
@@ -388,4 +399,11 @@
     return newArray;
   }
 
+  private native String stringAsObject(String str) /*-{
+    return new String(str);
+  }-*/;
+
+  private native String stringAsPrimitive(String str) /*-{
+    return String(str);
+  }-*/;
 }