TAP5-2575: rewrite some of tapestry-json based upon code from 
https://github.com/tdunning/open-json


Project: http://git-wip-us.apache.org/repos/asf/tapestry-5/repo
Commit: http://git-wip-us.apache.org/repos/asf/tapestry-5/commit/d0bc5715
Tree: http://git-wip-us.apache.org/repos/asf/tapestry-5/tree/d0bc5715
Diff: http://git-wip-us.apache.org/repos/asf/tapestry-5/diff/d0bc5715

Branch: refs/heads/5.4.x
Commit: d0bc571595124fa5fafdaa56333d2892049db642
Parents: 840ada0
Author: Jochen Kemnade <jkemn...@apache.org>
Authored: Wed Mar 8 14:24:46 2017 +0100
Committer: Jochen Kemnade <jochen.kemn...@eddyson.de>
Committed: Mon Mar 20 09:12:54 2017 +0100

----------------------------------------------------------------------
 tapestry-json/build.gradle                      |    2 +-
 .../java/org/apache/tapestry5/json/JSON.java    |  115 ++
 .../org/apache/tapestry5/json/JSONArray.java    |  589 ++++-----
 .../org/apache/tapestry5/json/JSONObject.java   | 1121 +++++++-----------
 .../org/apache/tapestry5/json/JSONStringer.java |  295 +++++
 .../org/apache/tapestry5/json/JSONTokener.java  |  749 +++++++-----
 .../org/apache/tapestry5/json/package-info.java |    2 +-
 7 files changed, 1550 insertions(+), 1323 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/d0bc5715/tapestry-json/build.gradle
----------------------------------------------------------------------
diff --git a/tapestry-json/build.gradle b/tapestry-json/build.gradle
index ccf936e..9b9d573 100644
--- a/tapestry-json/build.gradle
+++ b/tapestry-json/build.gradle
@@ -1,4 +1,4 @@
-description = "Repackaged, improved (and tested) version of code originally 
from json.org"
+description = "Repackaged, improved (and tested) version of code originally 
from https://github.com/tdunning/open-json";
 
 dependencies {
 

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/d0bc5715/tapestry-json/src/main/java/org/apache/tapestry5/json/JSON.java
----------------------------------------------------------------------
diff --git a/tapestry-json/src/main/java/org/apache/tapestry5/json/JSON.java 
b/tapestry-json/src/main/java/org/apache/tapestry5/json/JSON.java
new file mode 100644
index 0000000..955ee44
--- /dev/null
+++ b/tapestry-json/src/main/java/org/apache/tapestry5/json/JSON.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * 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 org.apache.tapestry5.json;
+
+class JSON {
+    /**
+     * Returns the input if it is a JSON-permissible value; throws otherwise.
+     */
+    static double checkDouble(double d) throws RuntimeException {
+        if (Double.isInfinite(d) || Double.isNaN(d)) {
+            throw new RuntimeException("JSON does not allow non-finite 
numbers.");
+        }
+        return d;
+    }
+
+    static Boolean toBoolean(Object value) {
+        if (value instanceof Boolean) {
+            return (Boolean) value;
+        } else if (value instanceof String) {
+            String stringValue = (String) value;
+            if ("true".equalsIgnoreCase(stringValue)) {
+                return true;
+            } else if ("false".equalsIgnoreCase(stringValue)) {
+                return false;
+            }
+        }
+        return null;
+    }
+
+    static Double toDouble(Object value) {
+        if (value instanceof Double) {
+            return (Double) value;
+        } else if (value instanceof Number) {
+            return ((Number) value).doubleValue();
+        } else if (value instanceof String) {
+            try {
+                return Double.valueOf((String) value);
+            } catch (NumberFormatException ignored) {
+            }
+        }
+        return null;
+    }
+
+    static Integer toInteger(Object value) {
+        if (value instanceof Integer) {
+            return (Integer) value;
+        } else if (value instanceof Number) {
+            return ((Number) value).intValue();
+        } else if (value instanceof String) {
+            try {
+                return (int) Double.parseDouble((String) value);
+            } catch (NumberFormatException ignored) {
+            }
+        }
+        return null;
+    }
+
+    static Long toLong(Object value) {
+        if (value instanceof Long) {
+            return (Long) value;
+        } else if (value instanceof Number) {
+            return ((Number) value).longValue();
+        } else if (value instanceof String) {
+            try {
+                return (long) Double.parseDouble((String) value);
+            } catch (NumberFormatException ignored) {
+            }
+        }
+        return null;
+    }
+
+    static String toString(Object value) {
+        if (value instanceof String) {
+            return (String) value;
+        } else if (value != null) {
+            return String.valueOf(value);
+        }
+        return null;
+    }
+
+    static RuntimeException typeMismatch(boolean array, Object indexOrName, 
Object actual,
+            String requiredType) throws RuntimeException {
+        String location = array ? "JSONArray[" + indexOrName + "]" : 
"JSONObject[\"" + indexOrName + "\"]";
+        if (actual == null) {
+            throw new RuntimeException(location + " is null.");
+        } else {
+            throw new RuntimeException(location + " is not a " + requiredType 
+ ".");
+        }
+    }
+
+    static RuntimeException typeMismatch(Object actual, String requiredType)
+            throws RuntimeException {
+        if (actual == null) {
+            throw new RuntimeException("Value is null.");
+        } else {
+            throw new RuntimeException("Value " + actual
+                    + " of type " + actual.getClass().getName()
+                    + " cannot be converted to " + requiredType);
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/d0bc5715/tapestry-json/src/main/java/org/apache/tapestry5/json/JSONArray.java
----------------------------------------------------------------------
diff --git 
a/tapestry-json/src/main/java/org/apache/tapestry5/json/JSONArray.java 
b/tapestry-json/src/main/java/org/apache/tapestry5/json/JSONArray.java
index 385a612..0c2e8ab 100644
--- a/tapestry-json/src/main/java/org/apache/tapestry5/json/JSONArray.java
+++ b/tapestry-json/src/main/java/org/apache/tapestry5/json/JSONArray.java
@@ -1,103 +1,101 @@
-// 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 org.apache.tapestry5.json;
-
 /*
- * Copyright (c) 2002 JSON.org
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to 
deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- * The above copyright notice and this permission notice shall be included in 
all
- * copies or substantial portions of the Software.
- * The Software shall be used for Good, not Evil.
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 
FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 
THE
- * SOFTWARE.
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * 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 org.apache.tapestry5.json;
+
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Iterator;
 import java.util.List;
 
+// Note: this class was written without inspecting the non-free org.json 
sourcecode.
+
 /**
- * A JSONArray is an ordered sequence of values. Its external text form is a 
string wrapped in square brackets with
- * commas separating the values. The internal form is an object having {@code 
get} and {@code opt} methods for
- * accessing the values by index, and {@code put} methods for adding or 
replacing values. The values can be any of
- * these types: {@code Boolean}, {@code JSONArray}, {@code JSONObject}, {@code 
Number},
- * {@code String}, or the {@code JSONObject.NULL object}.
+ * A dense indexed sequence of values. Values may be any mix of
+ * {@link JSONObject JSONObjects}, other {@link JSONArray JSONArrays}, Strings,
+ * Booleans, Integers, Longs, Doubles, {@code null} or {@link JSONObject#NULL}.
+ * Values may not be {@link Double#isNaN() NaNs}, {@link Double#isInfinite()
+ * infinities}, or of any type not listed here.
  *
- * The constructor can convert a JSON text into a Java object. The {@code 
toString} method converts to JSON text.
+ * {@code JSONArray} has the same type coercion behavior and
+ * optional/mandatory accessors as {@link JSONObject}. See that class'
+ * documentation for details.
  *
- * A {@code get} method returns a value if one can be found, and throws an 
exception if one cannot be found. An
- * {@code opt} method returns a default value instead of throwing an 
exception, and so is useful for obtaining
- * optional values.
+ * <strong>Warning:</strong> this class represents null in two incompatible
+ * ways: the standard Java {@code null} reference, and the sentinel value 
{@link
+ * JSONObject#NULL}. In particular, {@code get} fails if the requested index
+ * holds the null reference, but succeeds if it holds {@code JSONObject.NULL}.
  *
- * The generic {@code get()} and {@code opt()} methods return an object which 
you can cast or query for type.
- * There are also typed {@code get} and {@code opt} methods that do type 
checking and type coersion for you.
- *
- * The texts produced by the {@code toString} methods strictly conform to JSON 
syntax rules. The constructors are
- * more forgiving in the texts they will accept:
- * <ul>
- * <li>An extra {@code ,}&nbsp;<small>(comma)</small> may appear just before 
the closing bracket.</li>
- * <li>The {@code null} value will be inserted when there is {@code 
,}&nbsp;<small>(comma)</small> elision.</li>
- * <li>Strings may be quoted with {@code '}&nbsp;<small>(single 
quote)</small>.</li>
- * <li>Strings do not need to be quoted at all if they do not begin with a 
quote or single quote, and if they do not
- * contain leading or trailing spaces, and if they do not contain any of these 
characters:
- * {@code { } [ ] / \ : , = ; #} and if they do not look like numbers and if 
they are not the reserved words
- * {@code true}, {@code false}, or {@code null}.</li>
- * <li>Values can be separated by {@code ;} <small>(semicolon)</small> as well 
as by {@code ,}
- * <small>(comma)</small>.</li>
- * <li>Numbers may have the {@code 0-} <small>(octal)</small> or {@code 0x-} 
<small>(hex)</small> prefix.</li>
- * <li>Comments written in the slashshlash, slashstar, and hash conventions 
will be ignored.</li>
- * </ul>
- *
- * @author JSON.org
- * @version 2
+ * Instances of this class are not thread safe.
  */
-public final class JSONArray extends JSONCollection implements Iterable<Object>
-{
+public final class JSONArray extends JSONCollection implements 
Iterable<Object> {
+
+    private final List<Object> values;
 
     /**
-     * The arrayList where the JSONArray's properties are kept.
+     * Creates a {@code JSONArray} with no values.
      */
-    private final List<Object> list = new ArrayList<Object>();
+    public JSONArray() {
+        values = new ArrayList<Object>();
+    }
 
     /**
-     * Construct an empty JSONArray.
+     * Creates a new {@code JSONArray} with values from the next array in the
+     * tokener.
+     *
+     * @param readFrom a tokener whose nextValue() method will yield a
+     *                 {@code JSONArray}.
+     * @throws RuntimeExeption if the parse fails or doesn't yield a
+     *                       {@code JSONArray}.
      */
-    public JSONArray()
-    {
+    JSONArray(JSONTokener readFrom) {
+        /*
+         * Getting the parser to populate this could get tricky. Instead, just
+         * parse to temporary JSONArray and then steal the data from that.
+         */
+        Object object = readFrom.nextValue(JSONArray.class);
+        if (object instanceof JSONArray) {
+            values = ((JSONArray) object).values;
+        } else {
+            throw JSON.typeMismatch(object, "JSONArray");
+        }
     }
 
-    public JSONArray(String text)
-    {
-        JSONTokener tokener = new JSONTokener(text);
-
-        parse(tokener);
+    /**
+     * Creates a new {@code JSONArray} with values from the JSON string.
+     *
+     * @param json a JSON-encoded string containing an array.
+     * @throws RuntimeExeption if the parse fails or doesn't yield a {@code
+     *                       JSONArray}.
+     */
+    public JSONArray(String json) {
+        this(new JSONTokener(json));
     }
 
-    public JSONArray(Object... values)
-    {
-        for (Object value : values)
-            put(value);
+    /**
+     * Creates a new {@code JSONArray} with values from the given primitive 
array.
+     *
+     * @param array The values to use.
+     * @throws RuntimeExeption if any of the values are non-finite double 
values (i.e. NaN or infinite)
+     */
+    public JSONArray(Object... values) {
+        this();
+        for (int i = 0; i < values.length; ++i) {
+            put(values[i]);
+        }
     }
 
     /**
@@ -115,338 +113,256 @@ public final class JSONArray extends JSONCollection 
implements Iterable<Object>
         return new JSONArray().putAll(iterable);
     }
 
-    @Override
-    public Iterator<Object> iterator()
-    {
-        return list.iterator();
-    }
-
     /**
-     * Construct a JSONArray from a JSONTokener.
-     *
-     * @param tokenizer
-     *         A JSONTokener
-     * @throws RuntimeException
-     *         If there is a syntax error.
+     * @return Returns the number of values in this array.
      */
-    JSONArray(JSONTokener tokenizer)
-    {
-        assert tokenizer != null;
-
-        parse(tokenizer);
-    }
-
-    private void parse(JSONTokener tokenizer)
-    {
-        if (tokenizer.nextClean() != '[')
-        {
-            throw tokenizer.syntaxError("A JSONArray text must start with 
'['");
-        }
-
-        if (tokenizer.nextClean() == ']')
-        {
-            return;
-        }
-
-        tokenizer.back();
-
-        while (true)
-        {
-            if (tokenizer.nextClean() == ',')
-            {
-                tokenizer.back();
-                list.add(JSONObject.NULL);
-            } else
-            {
-                tokenizer.back();
-                list.add(tokenizer.nextValue());
-            }
-
-            switch (tokenizer.nextClean())
-            {
-                case ';':
-                case ',':
-                    if (tokenizer.nextClean() == ']')
-                    {
-                        return;
-                    }
-                    tokenizer.back();
-                    break;
-
-                case ']':
-                    return;
-
-                default:
-                    throw tokenizer.syntaxError("Expected a ',' or ']'");
-            }
-        }
+    public int length() {
+        return values.size();
     }
 
     /**
-     * Get the object value associated with an index.
+     * Appends {@code value} to the end of this array.
      *
-     * @param index
-     *         The index must be between 0 and length() - 1.
-     * @return An object value.
-     * @throws RuntimeException
-     *         If there is no value for the index.
+     * @param value a {@link JSONObject}, {@link JSONArray}, String, Boolean,
+     *              Integer, Long, Double, or {@link JSONObject#NULL}}. May
+     *              not be {@link Double#isNaN() NaNs} or {@link 
Double#isInfinite()
+     *              infinities}. Unsupported values are not permitted and will 
cause the
+     *              array to be in an inconsistent state.
+     * @return this array.
      */
-    public Object get(int index)
-    {
-        return list.get(index);
+    public JSONArray put(Object value) {
+        JSONObject.testValidity(value);
+        values.add(value);
+        return this;
     }
 
     /**
-     * Remove the object associated with the index.
+     * Same as {@link #put}, with added validity checks.
      *
-     * @param index
-     *         The index must be between 0 and length() - 1.
-     * @return An object removed.
-     * @throws RuntimeException
-     *         If there is no value for the index.
+     * @param value The value to append.
      */
-    public Object remove(int index)
-    {
-        return list.remove(index);
+    void checkedPut(Object value) {
+        JSONObject.testValidity(value);
+        if (value instanceof Number) {
+            JSON.checkDouble(((Number) value).doubleValue());
+        }
+
+        put(value);
     }
 
     /**
-     * Get the boolean value associated with an index. The string values 
"true" and "false" are converted to boolean.
+     * Sets the value at {@code index} to {@code value}, null padding this 
array
+     * to the required length if necessary. If a value already exists at {@code
+     * index}, it will be replaced.
      *
-     * @param index
-     *         The index must be between 0 and length() - 1.
-     * @return The truth.
-     * @throws RuntimeException
-     *         If there is no value for the index or if the value is not 
convertable to boolean.
+     * @param index Where to put the value.
+     * @param value a {@link JSONObject}, {@link JSONArray}, String, Boolean,
+     *              Integer, Long, Double, {@link JSONObject#NULL}, or {@code 
null}. May
+     *              not be {@link Double#isNaN() NaNs} or {@link 
Double#isInfinite()
+     *              infinities}.
+     * @return this array.
+     * @throws RuntimeExeption If the value cannot be represented as a finite 
double value.
      */
-    public boolean getBoolean(int index)
-    {
-        Object value = get(index);
-
-        if (value instanceof Boolean)
+    public JSONArray put(int index, Object value) {
+        if (index < 0)
         {
-            return (Boolean) value;
+            throw new RuntimeException("JSONArray[" + index + "] not found.");
         }
-
-        if (value instanceof String)
-        {
-            String asString = (String) value;
-
-            if (asString.equalsIgnoreCase("false"))
-                return false;
-
-            if (asString.equalsIgnoreCase("true"))
-                return true;
+        JSONObject.testValidity(value);
+        if (value instanceof Number) {
+            // deviate from the original by checking all Numbers, not just 
floats & doubles
+            JSON.checkDouble(((Number) value).doubleValue());
         }
-
-        throw new RuntimeException("JSONArray[" + index + "] is not a 
Boolean.");
+        while (values.size() <= index) {
+            values.add(null);
+        }
+        values.set(index, value);
+        return this;
     }
 
     /**
-     * Get the double value associated with an index.
+     * Returns true if this array has no value at {@code index}, or if its 
value
+     * is the {@code null} reference or {@link JSONObject#NULL}.
      *
-     * @param index
-     *         The index must be between 0 and length() - 1.
-     * @return The value.
-     * @throws IllegalArgumentException
-     *         If the key is not found or if the value cannot be converted to 
a number.
+     * @param index Which value to check.
+     * @return true if the value is null.
      */
-    public double getDouble(int index)
-    {
-        Object value = get(index);
-
-        try
-        {
-            if (value instanceof Number)
-                return ((Number) value).doubleValue();
-
-            return Double.valueOf((String) value);
-        } catch (Exception e)
-        {
-            throw new IllegalArgumentException("JSONArray[" + index + "] is 
not a number.");
-        }
+    public boolean isNull(int index) {
+        Object value = values.get(index);
+        return value == null || value == JSONObject.NULL;
     }
 
     /**
-     * Get the int value associated with an index.
+     * Returns the value at {@code index}.
      *
-     * @param index
-     *         The index must be between 0 and length() - 1.
-     * @return The value.
-     * @throws IllegalArgumentException
-     *         If the key is not found or if the value cannot be converted to 
a number. if the
-     *         value cannot be converted to a number.
+     * @param index Which value to get.
+     * @return the value at the specified location.
+     * @throws RuntimeExeption if this array has no value at {@code index}, or 
if
+     *                       that value is the {@code null} reference. This 
method returns
+     *                       normally if the value is {@code JSONObject#NULL}.
      */
-    public int getInt(int index)
-    {
-        Object o = get(index);
-        return o instanceof Number ? ((Number) o).intValue() : (int) 
getDouble(index);
+    public Object get(int index) {
+        try {
+            Object value = values.get(index);
+            if (value == null) {
+                throw new RuntimeException("Value at " + index + " is null.");
+            }
+            return value;
+        } catch (IndexOutOfBoundsException e) {
+            throw new RuntimeException("Index " + index + " out of range [0.." 
+ values.size() + ")");
+        }
     }
 
     /**
-     * Get the JSONArray associated with an index.
+     * Removes and returns the value at {@code index}, or null if the array 
has no value
+     * at {@code index}.
      *
-     * @param index
-     *         The index must be between 0 and length() - 1.
-     * @return A JSONArray value.
-     * @throws RuntimeException
-     *         If there is no value for the index. or if the value is not a 
JSONArray
+     * @param index Which value to remove.
+     * @return The value previously at the specified location.
      */
-    public JSONArray getJSONArray(int index)
-    {
-        Object o = get(index);
-        if (o instanceof JSONArray)
-        {
-            return (JSONArray) o;
+    public Object remove(int index) {
+        if (index < 0 || index >= values.size()) {
+            return null;
         }
-
-        throw new RuntimeException("JSONArray[" + index + "] is not a 
JSONArray.");
+        return values.remove(index);
     }
 
     /**
-     * Get the JSONObject associated with an index.
+     * Returns the value at {@code index} if it exists and is a boolean or can
+     * be coerced to a boolean.
      *
-     * @param index
-     *         subscript
-     * @return A JSONObject value.
-     * @throws RuntimeException
-     *         If there is no value for the index or if the value is not a 
JSONObject
+     * @param index Which value to get.
+     * @return the value at the specified location.
+     * @throws RuntimeExeption if the value at {@code index} doesn't exist or
+     *                       cannot be coerced to a boolean.
      */
-    public JSONObject getJSONObject(int index)
-    {
-        Object o = get(index);
-        if (o instanceof JSONObject)
-        {
-            return (JSONObject) o;
+    public boolean getBoolean(int index) {
+        Object object = get(index);
+        Boolean result = JSON.toBoolean(object);
+        if (result == null) {
+            throw JSON.typeMismatch(true, index, object, "Boolean");
         }
-
-        throw new RuntimeException("JSONArray[" + index + "] is not a 
JSONObject.");
+        return result;
     }
 
     /**
-     * Get the long value associated with an index.
+     * Returns the value at {@code index} if it exists and is a double or can
+     * be coerced to a double.
      *
-     * @param index
-     *         The index must be between 0 and length() - 1.
-     * @return The value.
-     * @throws IllegalArgumentException
-     *         If the key is not found or if the value cannot be converted to 
a number.
+     * @param index Which value to get.
+     * @return the value at the specified location.
+     * @throws RuntimeExeption if the value at {@code index} doesn't exist or
+     *                       cannot be coerced to a double.
      */
-    public long getLong(int index)
-    {
-        Object o = get(index);
-        return o instanceof Number ? ((Number) o).longValue() : (long) 
getDouble(index);
+    public double getDouble(int index) {
+        Object object = get(index);
+        Double result = JSON.toDouble(object);
+        if (result == null) {
+            throw JSON.typeMismatch(true, index, object, "number");
+        }
+        return result;
     }
 
     /**
-     * Get the string associated with an index.
+     * Returns the value at {@code index} if it exists and is an int or
+     * can be coerced to an int.
      *
-     * @param index
-     *         The index must be between 0 and length() - 1.
-     * @return A string value.
-     * @throws RuntimeException
-     *         If there is no value for the index.
+     * @param index Which value to get.
+     * @return the value at the specified location.
+     * @throws RuntimeExeption if the value at {@code index} doesn't exist or
+     *                       cannot be coerced to a int.
      */
-    public String getString(int index)
-    {
-        return get(index).toString();
+    public int getInt(int index) {
+        Object object = get(index);
+        Integer result = JSON.toInteger(object);
+        if (result == null) {
+            throw JSON.typeMismatch(true, index, object, "int");
+        }
+        return result;
     }
 
     /**
-     * Determine if the value is null.
+     * Returns the value at {@code index} if it exists and is a long or
+     * can be coerced to a long.
      *
-     * @param index
-     *         The index must be between 0 and length() - 1.
-     * @return true if the value at the index is null, or if there is no value.
+     * @param index Which value to get.
+     * @return the value at the specified location.
+     * @throws RuntimeExeption if the value at {@code index} doesn't exist or
+     *                       cannot be coerced to a long.
      */
-    public boolean isNull(int index)
-    {
-        return get(index) == JSONObject.NULL;
+    public long getLong(int index) {
+        Object object = get(index);
+        Long result = JSON.toLong(object);
+        if (result == null) {
+            throw JSON.typeMismatch(true, index, object, "long");
+        }
+        return result;
     }
 
     /**
-     * Get the number of elements in the JSONArray, included nulls.
+     * Returns the value at {@code index} if it exists, coercing it if
+     * necessary.
      *
-     * @return The length (or size).
+     * @param index Which value to get.
+     * @return the value at the specified location.
+     * @throws RuntimeExeption if no such value exists.
      */
-    public int length()
-    {
-        return list.size();
+    public String getString(int index) {
+        Object object = get(index);
+        String result = JSON.toString(object);
+        if (result == null) {
+            throw JSON.typeMismatch(true, index, object, "String");
+        }
+        return result;
     }
 
     /**
-     * Append an object value. This increases the array's length by one.
+     * Returns the value at {@code index} if it exists and is a {@code
+     * JSONArray}.
      *
-     * @param value
-     *         An object value. The value should be a Boolean, Double, 
Integer, JSONArray, JSONObject, JSONLiteral,
-     *         Long, or String, or the JSONObject.NULL singleton.
-     * @return this array
+     * @param index Which value to get.
+     * @return the value at the specified location.
+     * @throws RuntimeExeption if the value doesn't exist or is not a {@code
+     *                       JSONArray}.
      */
-    public JSONArray put(Object value)
-    {
-        // now testValidity checks for null values.
-        // assert value != null;
-
-        JSONObject.testValidity(value);
-
-        list.add(value);
-
-        return this;
+    public JSONArray getJSONArray(int index) {
+        Object object = get(index);
+        if (object instanceof JSONArray) {
+            return (JSONArray) object;
+        } else {
+            throw JSON.typeMismatch(true, index, object, "JSONArray");
+        }
     }
 
     /**
-     * Put or replace an object value in the JSONArray. If the index is 
greater than the length of the JSONArray, then
-     * null elements will be added as necessary to pad it out.
+     * Returns the value at {@code index} if it exists and is a {@code
+     * JSONObject}.
      *
-     * @param index
-     *         The subscript.
-     * @param value
-     *         The value to put into the array. The value should be a Boolean, 
Double, Integer, JSONArray,
-     *         JSONObject, JSONString, Long, or String, or the JSONObject.NULL 
singeton.
-     * @return this array
-     * @throws RuntimeException
-     *         If the index is negative or if the the value is an invalid 
number.
+     * @param index Which value to get.
+     * @return the value at the specified location.
+     * @throws RuntimeExeption if the value doesn't exist or is not a {@code
+     *                       JSONObject}.
      */
-    public JSONArray put(int index, Object value)
-    {
-        assert value != null;
-
-        if (index < 0)
-        {
-            throw new RuntimeException("JSONArray[" + index + "] not found.");
+    public JSONObject getJSONObject(int index) {
+        Object object = get(index);
+        if (object instanceof JSONObject) {
+            return (JSONObject) object;
+        } else {
+            throw JSON.typeMismatch(true, index, object, "JSONObject");
         }
-
-        JSONObject.testValidity(value);
-
-        if (index < length())
-        {
-            list.set(index, value);
-        } else
-        {
-            while (index != length())
-                list.add(JSONObject.NULL);
-
-            list.add(value);
-        }
-
-        return this;
     }
 
     @Override
-    public boolean equals(Object obj)
-    {
-        if (obj == null)
-            return false;
-
-        if (!(obj instanceof JSONArray))
-            return false;
-
-        JSONArray other = (JSONArray) obj;
-
-        return list.equals(other.list);
+    public boolean equals(Object o) {
+        return o instanceof JSONArray && ((JSONArray) o).values.equals(values);
     }
 
     @Override
+    public int hashCode() {
+        // diverge from the original, which doesn't implement hashCode
+        return values.hashCode();
+    }
+
     void print(JSONPrintSession session)
     {
         session.printSymbol('[');
@@ -455,7 +371,7 @@ public final class JSONArray extends JSONCollection 
implements Iterable<Object>
 
         boolean comma = false;
 
-        for (Object value : list)
+        for (Object value : values)
         {
             if (comma)
                 session.printSymbol(',');
@@ -505,6 +421,15 @@ public final class JSONArray extends JSONCollection 
implements Iterable<Object>
      */
     public List<Object> toList()
     {
-        return Collections.unmodifiableList(list);
+        return Collections.unmodifiableList(values);
     }
+
+
+    @Override
+    public Iterator<Object> iterator()
+    {
+        return values.iterator();
+    }
+
+
 }

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/d0bc5715/tapestry-json/src/main/java/org/apache/tapestry5/json/JSONObject.java
----------------------------------------------------------------------
diff --git 
a/tapestry-json/src/main/java/org/apache/tapestry5/json/JSONObject.java 
b/tapestry-json/src/main/java/org/apache/tapestry5/json/JSONObject.java
index a9555b8..0073ad9 100644
--- a/tapestry-json/src/main/java/org/apache/tapestry5/json/JSONObject.java
+++ b/tapestry-json/src/main/java/org/apache/tapestry5/json/JSONObject.java
@@ -1,132 +1,113 @@
-// 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 org.apache.tapestry5.json;
-
 /*
- * Copyright (c) 2002 JSON.org
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to 
deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- * The above copyright notice and this permission notice shall be included in 
all
- * copies or substantial portions of the Software.
- * The Software shall be used for Good, not Evil.
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 
FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 
THE
- * SOFTWARE.
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * 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 org.apache.tapestry5.json;
+
 import java.io.ObjectStreamException;
 import java.io.Serializable;
-import java.util.*;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Set;
+
+// Note: this class was written without inspecting the non-free org.json 
sourcecode.
 
 /**
- * A JSONObject is an unordered collection of name/value pairs. Its external 
form is a string wrapped in curly braces
- * with colons between the names and values, and commas between the values and 
names. The internal form is an object
- * having <code>get</code> and <code>opt</code> methods for accessing the 
values by name, and <code>put</code> methods
- * for adding or replacing values by name. The values can be any of these 
types: <code>Boolean</code>,
- * {@link org.apache.tapestry5.json.JSONArray}, {@link 
org.apache.tapestry5.json.JSONLiteral}, <code>JSONObject</code>,
- * <code>Number</code>, <code>String</code>, or the 
<code>JSONObject.NULL</code> object. A JSONObject constructor can be
- * used to convert an external form JSON text into
- * an internal form whose values can be retrieved with the <code>get</code> 
and <code>opt</code> methods, or to convert
- * values into a JSON text using the <code>put</code> and 
<code>toString</code> methods. A <code>get</code> method
- * returns a value if one can be found, and throws an exception if one cannot 
be found. An <code>opt</code> method
- * returns a default value instead of throwing an exception, and so is useful 
for obtaining optional values.
- *
- * The generic <code>get()</code> and <code>opt()</code> methods return an 
object, which you can cast or query for type.
- * There are also typed <code>get</code> and <code>opt</code> methods that do 
type checking and type coersion for you.
- *
- * The <code>put</code> methods adds values to an object. For example,
- *
- * <pre>
- * myString = new JSONObject().put(&quot;JSON&quot;, &quot;Hello, 
World!&quot;).toString();
- * </pre>
+ * A modifiable set of name/value mappings. Names are unique, non-null strings.
+ * Values may be any mix of {@link JSONObject JSONObjects}, {@link JSONArray
+ * JSONArrays}, Strings, Booleans, Integers, Longs, Doubles or {@link #NULL}.
+ * Values may not be {@code null}, {@link Double#isNaN() NaNs}, {@link
+ * Double#isInfinite() infinities}, or of any type not listed here.
  *
- * produces the string <code>{"JSON": "Hello, World"}</code>.
- *
- * The texts produced by the <code>toString</code> methods strictly conform to 
the JSON syntax rules. The constructors
- * are more forgiving in the texts they will accept:
+ * <p>This class can coerce values to another type when requested.
  * <ul>
- * <li>An extra <code>,</code>&nbsp;<small>(comma)</small> may appear just 
before the closing brace.</li>
- * <li>Strings may be quoted with <code>'</code>&nbsp;<small>(single 
quote)</small>.</li>
- * <li>Strings do not need to be quoted at all if they do not begin with a 
quote or single quote, and if they do not
- * contain leading or trailing spaces, and if they do not contain any of these 
characters: <code>{ }
- * [ ] / \ : , = ; #</code> and if they do not look like numbers and if they 
are not the reserved words
- * <code>true</code>, <code>false</code>, or <code>null</code>.</li>
- * <li>Keys can be followed by <code>=</code> or {@code =>} as well as by 
{@code :}.</li>
- * <li>Values can be followed by <code>;</code> <small>(semicolon)</small> as 
well as by <code>,</code>
- * <small>(comma)</small>.</li>
- * <li>Numbers may have the <code>0-</code> <small>(octal)</small> or 
<code>0x-</code> <small>(hex)</small> prefix.</li>
- * <li>Comments written in the slashshlash, slashstar, and hash conventions 
will be ignored.</li>
+ * <li>When the requested type is a boolean, strings will be coerced using a
+ * case-insensitive comparison to "true" and "false".
+ * <li>When the requested type is a double, other {@link Number} types will
+ * be coerced using {@link Number#doubleValue() doubleValue}. Strings
+ * that can be coerced using {@link Double#valueOf(String)} will be.
+ * <li>When the requested type is an int, other {@link Number} types will
+ * be coerced using {@link Number#intValue() intValue}. Strings
+ * that can be coerced using {@link Double#valueOf(String)} will be,
+ * and then cast to int.
+ * <li><a name="lossy">When the requested type is a long, other {@link Number} 
types will
+ * be coerced using {@link Number#longValue() longValue}. Strings
+ * that can be coerced using {@link Double#valueOf(String)} will be,
+ * and then cast to long. This two-step conversion is lossy for very
+ * large values. For example, the string "9223372036854775806" yields the
+ * long 9223372036854775807.</a>
+ * <li>When the requested type is a String, other non-null values will be
+ * coerced using {@link String#valueOf(Object)}. Although null cannot be
+ * coerced, the sentinel value {@link JSONObject#NULL} is coerced to the
+ * string "null".
  * </ul>
- * <hr>
  *
- * This class, and the other related classes, have been heavily modified from 
the original source, to fit Tapestry
- * standards and to make use of JDK 1.5 features such as generics. Further, 
since the interest of Tapestry is primarily
- * constructing JSON (and not parsing it), many of the non-essential methods 
have been removed (since the original code
- * came with no tests).
+ * <p>This class can look up both mandatory and optional values:
+ * <ul>
+ * <li>Use <code>get<i>Type</i>()</code> to retrieve a mandatory value. This
+ * fails with a {@code RuntimeException} if the requested name has no value
+ * or if the value cannot be coerced to the requested type.
+ * <li>Use <code>opt()</code> to retrieve an optional value.
+ * </ul>
  *
- * Finally, support for the {@link org.apache.tapestry5.json.JSONLiteral} type 
has been added, which allows the exact
- * output to be controlled; useful when a JSONObject is being used as a 
configuration object, and must contain values
- * that are not simple data, such as an inline function (making the result not 
JSON).
+ * <p><strong>Warning:</strong> this class represents null in two incompatible
+ * ways: the standard Java {@code null} reference, and the sentinel value 
{@link
+ * JSONObject#NULL}. In particular, calling {@code put(name, null)} removes the
+ * named entry from the object but {@code put(name, JSONObject.NULL)} stores an
+ * entry whose value is {@code JSONObject.NULL}.
  *
- * @author JSON.org
- * @version 2
+ * <p>Instances of this class are not thread safe.
  */
-@SuppressWarnings(
-        {"CloneDoesntCallSuperClone"})
-public final class JSONObject extends JSONCollection
-{
+public final class JSONObject extends JSONCollection {
+
+    private static final Double NEGATIVE_ZERO = -0d;
 
     /**
-     * JSONObject.NULL is equivalent to the value that JavaScript calls null, 
whilst Java's null is equivalent to the
-     * value that JavaScript calls undefined.
+     * A sentinel value used to explicitly define a name with no value. Unlike
+     * {@code null}, names with this value:
+     * <ul>
+     * <li>show up in the {@link #names} array
+     * <li>show up in the {@link #keys} iterator
+     * <li>return {@code true} for {@link #has(String)}
+     * <li>do not throw on {@link #get(String)}
+     * <li>are included in the encoded JSON string.
+     * </ul>
+     *
+     * <p>This value violates the general contract of {@link Object#equals} by
+     * returning true when compared to {@code null}. Its {@link #toString}
+     * method returns "null".
      */
-    private static final class Null implements JSONString, Serializable
-    {
-        /**
-         * A Null object is equal to the null value and to itself.
-         *
-         * @param object
-         *         An object to test for nullness.
-         * @return true if the object parameter is the JSONObject.NULL object 
or null.
-         */
+    public static final Object NULL = new Serializable() {
+
+      private static final long serialVersionUID = 1L;
+
+        @SuppressWarnings("EqualsWhichDoesntCheckParameterClass")
         @Override
-        public boolean equals(Object object)
-        {
-            return object == null || object == this;
+        public boolean equals(Object o) {
+            return o == this || o == null; // API specifies this broken equals 
implementation
         }
 
-        /**
-         * Get the "null" string value.
-         *
-         * @return The string "null".
-         */
+        // at least make the broken equals(null) consistent with 
Objects.hashCode(null).
         @Override
-        public String toString()
-        {
-            return "null";
+        public int hashCode() {
+            return 0;
         }
 
         @Override
-        public String toJSONString()
-        {
+        public String toString() {
             return "null";
         }
 
@@ -135,27 +116,72 @@ public final class JSONObject extends JSONCollection
         {
             return NULL;
         }
+
+    };
+
+    private final LinkedHashMap<String, Object> nameValuePairs;
+
+    /**
+     * Creates a {@code JSONObject} with no name/value mappings.
+     */
+    public JSONObject() {
+        nameValuePairs = new LinkedHashMap<String, Object>();
     }
 
     /**
-     * The map where the JSONObject's properties are kept.
+     * Creates a new {@code JSONObject} with name/value mappings from the next
+     * object in the tokener.
+     *
+     * @param readFrom a tokener whose nextValue() method will yield a
+     *                 {@code JSONObject}.
+     * @throws RuntimeException if the parse fails or doesn't yield a
+     *                       {@code JSONObject}.
      */
-    private final Map<String, Object> properties = new LinkedHashMap<String, 
Object>();
+    JSONObject(JSONTokener readFrom) {
+        /*
+         * Getting the parser to populate this could get tricky. Instead, just
+         * parse to temporary JSONObject and then steal the data from that.
+         */
+        Object object = readFrom.nextValue(JSONObject.class);
+        if (object instanceof JSONObject) {
+            this.nameValuePairs = ((JSONObject) object).nameValuePairs;
+        } else {
+            throw JSON.typeMismatch(object, "JSONObject");
+        }
+    }
 
     /**
-     * It is sometimes more convenient and less ambiguous to have a 
<code>NULL</code> object than to use Java's
-     * <code>null</code> value. <code>JSONObject.NULL.equals(null)</code> 
returns <code>true</code>.
-     * <code>JSONObject.NULL.toString()</code> returns <code>"null"</code>.
+     * Creates a new {@code JSONObject} with name/value mappings from the JSON
+     * string.
+     *
+     * @param json a JSON-encoded string containing an object.
+     * @throws RuntimeException if the parse fails or doesn't yield a {@code
+     *                       JSONObject}.
      */
-    public static final Object NULL = new Null();
+    public JSONObject(String json) {
+        this(new JSONTokener(json));
+    }
 
     /**
-     * Construct an empty JSONObject.
+     * Creates a new {@code JSONObject} by copying mappings for the listed 
names
+     * from the given object. Names that aren't present in {@code copyFrom} 
will
+     * be skipped.
+     *
+     * @param copyFrom The source object.
+     * @param names    The names of the fields to copy.
+     * @throws RuntimeException On internal errors. Shouldn't happen.
      */
-    public JSONObject()
-    {
+    public JSONObject(JSONObject copyFrom, String[] names) {
+        this();
+        for (String name : names) {
+            Object value = copyFrom.opt(name);
+            if (value != null) {
+                nameValuePairs.put(name, value);
+            }
+        }
     }
 
+
     /**
      * Returns a new JSONObject that is a shallow copy of this JSONObject.
      *
@@ -164,7 +190,7 @@ public final class JSONObject extends JSONCollection
     public JSONObject copy()
     {
         JSONObject dupe = new JSONObject();
-        dupe.properties.putAll(properties);
+        dupe.nameValuePairs.putAll(nameValuePairs);
 
         return dupe;
     }
@@ -181,6 +207,8 @@ public final class JSONObject extends JSONCollection
      */
     public JSONObject(Object... keysAndValues)
     {
+        this();
+
         int i = 0;
 
         while (i < keysAndValues.length)
@@ -190,679 +218,399 @@ public final class JSONObject extends JSONCollection
     }
 
     /**
-     * Construct a JSONObject from a subset of another JSONObject. An array of 
strings is used to identify the keys that
-     * should be copied. Missing keys are ignored.
+     * Returns the number of name/value mappings in this object.
      *
-     * @param source
-     *         A JSONObject.
-     * @param propertyNames
-     *         The strings to copy.
-     * @throws RuntimeException
-     *         If a value is a non-finite number.
+     * @return the length of this.
      */
-    public JSONObject(JSONObject source, String... propertyNames)
-    {
-        for (String name : propertyNames)
-        {
-            Object value = source.opt(name);
-
-            if (value != null)
-                put(name, value);
-        }
+    public int length() {
+        return nameValuePairs.size();
     }
 
     /**
-     * Construct a JSONObject from a JSONTokener.
+     * Maps {@code name} to {@code value}, clobbering any existing name/value
+     * mapping with the same name. If the value is {@code null}, any existing
+     * mapping for {@code name} is removed.
      *
-     * @param x
-     *         A JSONTokener object containing the source string. @ If there 
is a syntax error in the source string.
+     * @param name  The name of the new value.
+     * @param value a {@link JSONObject}, {@link JSONArray}, String, Boolean,
+     *              Integer, Long, Double, {@link #NULL}, or {@code null}. May 
not be
+     *              {@link Double#isNaN() NaNs} or {@link Double#isInfinite()
+     *              infinities}.
+     * @return this object.
+     * @throws RuntimeException if the value is an invalid double (infinite or 
NaN).
      */
-    JSONObject(JSONTokener x)
-    {
-        String key;
-
-        if (x.nextClean() != '{')
-        {
-            throw x.syntaxError("A JSONObject text must begin with '{'");
+    public JSONObject put(String name, Object value) {
+        if (value == null) {
+            nameValuePairs.remove(name);
+            return this;
         }
-
-        while (true)
-        {
-            char c = x.nextClean();
-            switch (c)
-            {
-                case 0:
-                    throw x.syntaxError("A JSONObject text must end with '}'");
-                case '}':
-                    return;
-                default:
-                    x.back();
-                    key = x.nextValue().toString();
-            }
-
-            /*
-             * The key is followed by ':'. We will also tolerate '=' or '=>'.
-             */
-
-            c = x.nextClean();
-            if (c == '=')
-            {
-                if (x.next() != '>')
-                {
-                    x.back();
-                }
-            } else if (c != ':')
-            {
-                throw x.syntaxError("Expected a ':' after a key");
-            }
-            put(key, x.nextValue());
-
-            /*
-             * Pairs are separated by ','. We will also tolerate ';'.
-             */
-
-            switch (x.nextClean())
-            {
-                case ';':
-                case ',':
-                    if (x.nextClean() == '}')
-                    {
-                        return;
-                    }
-                    x.back();
-                    break;
-                case '}':
-                    return;
-                default:
-                    throw x.syntaxError("Expected a ',' or '}'");
-            }
+        testValidity(value);
+        if (value instanceof Number) {
+            // deviate from the original by checking all Numbers, not just 
floats & doubles
+            JSON.checkDouble(((Number) value).doubleValue());
         }
+        nameValuePairs.put(checkName(name), value);
+        return this;
     }
 
     /**
-     * Construct a JSONObject from a string. This is the most commonly used 
JSONObject constructor.
-     *
-     * @param string
-     *         A string beginning with <code>{</code>&nbsp;<small>(left 
brace)</small> and ending with <code>}</code>
-     *         &nbsp;<small>(right brace)</small>.
-     * @throws RuntimeException
-     *         If there is a syntax error in the source string.
-     */
-    public JSONObject(String string)
-    {
-        this(new JSONTokener(string));
+     * Appends {@code value} to the array already mapped to {@code name}. If
+     * this object has no mapping for {@code name}, this inserts a new mapping.
+     * If the mapping exists but its value is not an array, the existing
+     * and new values are inserted in order into a new array which is itself
+     * mapped to {@code name}. In aggregate, this allows values to be added to 
a
+     * mapping one at a time.
+     *
+     * Note that {@code append(String, Object)} provides better semantics.
+     * In particular, the mapping for {@code name} will <b>always</b> be a
+     * {@link JSONArray}. Using {@code accumulate} will result in either a
+     * {@link JSONArray} or a mapping whose type is the type of {@code value}
+     * depending on the number of calls to it.
+     *
+     * @param name  The name of the field to change.
+     * @param value a {@link JSONObject}, {@link JSONArray}, String, Boolean,
+     *              Integer, Long, Double, {@link #NULL} or null. May not be 
{@link
+     *              Double#isNaN() NaNs} or {@link Double#isInfinite() 
infinities}.
+     * @return this object after mutation.
+     * @throws RuntimeException If the object being added is an invalid number.
+     */
+    // TODO: Change {@code append) to {@link #append} when append is
+    // unhidden.
+    public JSONObject accumulate(String name, Object value) {
+        Object current = nameValuePairs.get(checkName(name));
+        if (current == null) {
+            return put(name, value);
+        }
+
+        if (current instanceof JSONArray) {
+            JSONArray array = (JSONArray) current;
+            array.checkedPut(value);
+        } else {
+            JSONArray array = new JSONArray();
+            array.checkedPut(current);
+            array.checkedPut(value);
+            nameValuePairs.put(name, array);
+        }
+        return this;
     }
 
     /**
-     * Accumulate values under a key. It is similar to the put method except 
that if there is already an object stored
-     * under the key then a JSONArray is stored under the key to hold all of 
the accumulated values. If there is already
-     * a JSONArray, then the new value is appended to it. In contrast, the put 
method replaces the previous value.
+     * Appends values to the array mapped to {@code name}. A new {@link 
JSONArray}
+     * mapping for {@code name} will be inserted if no mapping exists. If the 
existing
+     * mapping for {@code name} is not a {@link JSONArray}, a {@link 
RuntimeException}
+     * will be thrown.
      *
-     * @param key
-     *         A key string.
-     * @param value
-     *         An object to be accumulated under the key.
-     * @return this.
-     * @throws RuntimeException if the value is an invalid number or if the 
key is null.
+     * @param name  The name of the array to which the value should be 
appended.
+     * @param value The value to append.
+     * @return this object.
+     * @throws RuntimeException if {@code name} is {@code null} or if the 
mapping for
+     *                       {@code name} is non-null and is not a {@link 
JSONArray}.
      */
-    public JSONObject accumulate(String key, Object value)
-    {
+    public JSONObject append(String name, Object value) {
         testValidity(value);
+        Object current = nameValuePairs.get(checkName(name));
 
-        Object existing = opt(key);
-
-        if (existing == null)
-        {
-            // Note that the original implementation of this method 
contradicted the method
-            // documentation.
-            put(key, value);
-            return this;
-        }
-
-        if (existing instanceof JSONArray)
-        {
-            ((JSONArray) existing).put(value);
-            return this;
+        final JSONArray array;
+        if (current instanceof JSONArray) {
+            array = (JSONArray) current;
+        } else if (current == null) {
+            JSONArray newArray = new JSONArray();
+            nameValuePairs.put(name, newArray);
+            array = newArray;
+        } else {
+            throw new RuntimeException("JSONObject[\"" + name + "\"] is not a 
JSONArray.");
         }
 
-        // Replace the existing value, of any type, with an array that 
includes both the
-        // existing and the new value.
-
-        put(key, new JSONArray().put(existing).put(value));
+        array.checkedPut(value);
 
         return this;
     }
 
-    /**
-     * Append values to the array under a key. If the key does not exist in 
the JSONObject, then the key is put in the
-     * JSONObject with its value being a JSONArray containing the value 
parameter. If the key was already associated
-     * with a JSONArray, then the value parameter is appended to it.
-     *
-     * @param key
-     *         A key string.
-     * @param value
-     *         An object to be accumulated under the key.
-     * @return this. @ If the key is null or if the current value associated 
with the key is not a JSONArray.
-     */
-    public JSONObject append(String key, Object value)
-    {
-        testValidity(value);
-        Object o = opt(key);
-        if (o == null)
-        {
-            put(key, new JSONArray().put(value));
-        } else if (o instanceof JSONArray)
-        {
-            put(key, ((JSONArray) o).put(value));
-        } else
-        {
-            throw new RuntimeException("JSONObject[" + quote(key) + "] is not 
a JSONArray.");
+    String checkName(String name) {
+        if (name == null) {
+            throw new RuntimeException("Names must be non-null");
         }
-
-        return this;
+        return name;
     }
 
     /**
-     * Produce a string from a double. The string "null" will be returned if 
the number is not finite.
+     * Removes the named mapping if it exists; does nothing otherwise.
      *
-     * @param d
-     *         A double.
-     * @return A String.
+     * @param name The name of the mapping to remove.
+     * @return the value previously mapped by {@code name}, or null if there 
was
+     * no such mapping.
      */
-    static String doubleToString(double d)
-    {
-        if (Double.isInfinite(d) || Double.isNaN(d))
-        {
-            return "null";
-        }
-
-        // Shave off trailing zeros and decimal point, if possible.
-
-        String s = Double.toString(d);
-        if (s.indexOf('.') > 0 && s.indexOf('e') < 0 && s.indexOf('E') < 0)
-        {
-            while (s.endsWith("0"))
-            {
-                s = s.substring(0, s.length() - 1);
-            }
-            if (s.endsWith("."))
-            {
-                s = s.substring(0, s.length() - 1);
-            }
-        }
-        return s;
+    public Object remove(String name) {
+        return nameValuePairs.remove(name);
     }
 
     /**
-     * Get the value object associated with a key.
+     * Returns true if this object has no mapping for {@code name} or if it has
+     * a mapping whose value is {@link #NULL}.
      *
-     * @param key
-     *         A key string.
-     * @return The object associated with the key.
-     * @throws RuntimeException
-     *         if the key is not found.
-     * @see #opt(String)
+     * @param name The name of the value to check on.
+     * @return true if the field doesn't exist or is null.
      */
-    public Object get(String key)
-    {
-        Object o = opt(key);
-        if (o == null)
-        {
-            throw new RuntimeException("JSONObject[" + quote(key) + "] not 
found.");
-        }
-
-        return o;
+    public boolean isNull(String name) {
+        Object value = nameValuePairs.get(name);
+        return value == null || value == NULL;
     }
 
     /**
-     * Get the boolean value associated with a key.
+     * Returns true if this object has a mapping for {@code name}. The mapping
+     * may be {@link #NULL}.
      *
-     * @param key
-     *         A key string.
-     * @return The truth.
-     * @throws RuntimeException
-     *         if the value does not exist, is not a Boolean or the String 
"true" or "false".
+     * @param name The name of the value to check on.
+     * @return true if this object has a field named {@code name}
      */
-    public boolean getBoolean(String key)
-    {
-        Object o = get(key);
-
-        if (o instanceof Boolean)
-            return o.equals(Boolean.TRUE);
-
-        if (o instanceof String)
-        {
-            String value = (String) o;
-
-            if (value.equalsIgnoreCase("true"))
-                return true;
-
-            if (value.equalsIgnoreCase("false"))
-                return false;
-        }
-
-        throw new RuntimeException("JSONObject[" + quote(key) + "] is not a 
Boolean.");
+    public boolean has(String name) {
+        return nameValuePairs.containsKey(name);
     }
 
     /**
-     * Get the double value associated with a key.
+     * Returns the value mapped by {@code name}, or throws if no such mapping 
exists.
      *
-     * @param key
-     *         A key string.
-     * @return The numeric value. @throws RuntimeException if the key is not 
found or if the value is not a Number object and cannot be
-     *         converted to a number.
+     * @param name The name of the value to get.
+     * @return The value.
+     * @throws RuntimeException if no such mapping exists.
      */
-    public double getDouble(String key)
-    {
-        Object value = get(key);
-
-        try
-        {
-            if (value instanceof Number)
-                return ((Number) value).doubleValue();
-
-            // This is a bit sloppy for the case where value is not a string.
-
-            return Double.valueOf((String) value);
-        } catch (Exception e)
-        {
-            throw new RuntimeException("JSONObject[" + quote(key) + "] is not 
a number.");
+    public Object get(String name) {
+        Object result = nameValuePairs.get(name);
+        if (result == null) {
+            throw new RuntimeException("JSONObject[\"" + name + "\"] not 
found.");
         }
+        return result;
     }
 
     /**
-     * Get the int value associated with a key. If the number value is too 
large for an int, it will be clipped.
+     * Returns the value mapped by {@code name}, or null if no such mapping
+     * exists.
      *
-     * @param key
-     *         A key string.
-     * @return The integer value.
-     * @throws RuntimeException
-     *         if the key is not found or if the value cannot be converted to 
an integer.
+     * @param name The name of the value to get.
+     * @return The value.
      */
-    public int getInt(String key)
-    {
-        Object value = get(key);
-
-        if (value instanceof Number)
-            return ((Number) value).intValue();
-
-        // Very inefficient way to do this!
-        return (int) getDouble(key);
+    public Object opt(String name) {
+        return nameValuePairs.get(name);
     }
 
     /**
-     * Get the JSONArray value associated with a key.
+     * Returns the value mapped by {@code name} if it exists and is a boolean 
or
+     * can be coerced to a boolean, or throws otherwise.
      *
-     * @param key
-     *         A key string.
-     * @return A JSONArray which is the value.
-     * @throws RuntimeException
-     *         if the key is not found or if the value is not a JSONArray.
+     * @param name The name of the field we want.
+     * @return The selected value if it exists.
+     * @throws RuntimeException if the mapping doesn't exist or cannot be 
coerced
+     *                       to a boolean.
      */
-    public JSONArray getJSONArray(String key)
-    {
-        Object o = get(key);
-        if (o instanceof JSONArray)
-        {
-            return (JSONArray) o;
+    public boolean getBoolean(String name) {
+        Object object = get(name);
+        Boolean result = JSON.toBoolean(object);
+        if (result == null) {
+            throw JSON.typeMismatch(false, name, object, "Boolean");
         }
-
-        throw new RuntimeException("JSONObject[" + quote(key) + "] is not a 
JSONArray.");
+        return result;
     }
 
     /**
-     * Get the JSONObject value associated with a key.
+     * Returns the value mapped by {@code name} if it exists and is a double or
+     * can be coerced to a double, or throws otherwise.
      *
-     * @param key
-     *         A key string.
-     * @return A JSONObject which is the value.
-     * @throws RuntimeException
-     *         if the key is not found or if the value is not a JSONObject.
+     * @param name The name of the field we want.
+     * @return The selected value if it exists.
+     * @throws RuntimeException if the mapping doesn't exist or cannot be 
coerced
+     *                       to a double.
      */
-    public JSONObject getJSONObject(String key)
-    {
-        Object o = get(key);
-        if (o instanceof JSONObject)
-        {
-            return (JSONObject) o;
+    public double getDouble(String name) {
+        Object object = get(name);
+        Double result = JSON.toDouble(object);
+        if (result == null) {
+            throw JSON.typeMismatch(false, name, object, "number");
         }
-
-        throw new RuntimeException("JSONObject[" + quote(key) + "] is not a 
JSONObject.");
+        return result;
     }
 
     /**
-     * Get the long value associated with a key. If the number value is too 
long for a long, it will be clipped.
+     * Returns the value mapped by {@code name} if it exists and is an int or
+     * can be coerced to an int, or throws otherwise.
      *
-     * @param key
-     *         A key string.
-     * @return The long value.
-     * @throws RuntimeException
-     *         if the key is not found or if the value cannot be converted to 
a long.
+     * @param name The name of the field we want.
+     * @return The selected value if it exists.
+     * @throws RuntimeException if the mapping doesn't exist or cannot be 
coerced
+     *                       to an int.
      */
-    public long getLong(String key)
-    {
-        Object o = get(key);
-        return o instanceof Number ? ((Number) o).longValue() : (long) 
getDouble(key);
+    public int getInt(String name) {
+        Object object = get(name);
+        Integer result = JSON.toInteger(object);
+        if (result == null) {
+            throw JSON.typeMismatch(false, name, object, "int");
+        }
+        return result;
     }
 
     /**
-     * Get the string associated with a key.
+     * Returns the value mapped by {@code name} if it exists and is a long or
+     * can be coerced to a long, or throws otherwise.
+     * Note that JSON represents numbers as doubles,
      *
-     * @param key
-     *         A key string.
-     * @return A string which is the value.
-     * @throws RuntimeException
-     *         if the key is not found.
+     * so this is <a href="#lossy">lossy</a>; use strings to transfer numbers
+     * via JSON without loss.
+     *
+     * @param name The name of the field that we want.
+     * @return The value of the field.
+     * @throws RuntimeException if the mapping doesn't exist or cannot be 
coerced
+     *                       to a long.
      */
-    public String getString(String key)
-    {
-        return get(key).toString();
+    public long getLong(String name) {
+        Object object = get(name);
+        Long result = JSON.toLong(object);
+        if (result == null) {
+            throw JSON.typeMismatch(false, name, object, "long");
+        }
+        return result;
     }
 
     /**
-     * Determine if the JSONObject contains a specific key.
+     * Returns the value mapped by {@code name} if it exists, coercing it if
+     * necessary, or throws if no such mapping exists.
      *
-     * @param key
-     *         A key string.
-     * @return true if the key exists in the JSONObject.
+     * @param name The name of the field we want.
+     * @return The value of the field.
+     * @throws RuntimeException if no such mapping exists.
      */
-    public boolean has(String key)
-    {
-        return properties.containsKey(key);
+    public String getString(String name) {
+        Object object = get(name);
+        String result = JSON.toString(object);
+        if (result == null) {
+            throw JSON.typeMismatch(false, name, object, "String");
+        }
+        return result;
     }
 
     /**
-     * Determine if the value associated with the key is null or if there is 
no value.
+     * Returns the value mapped by {@code name} if it exists and is a {@code
+     * JSONArray}, or throws otherwise.
      *
-     * @param key
-     *         A key string.
-     * @return true if there is no value associated with the key or if the 
value is the JSONObject.NULL object.
+     * @param name The field we want to get.
+     * @return The value of the field (if it is a JSONArray.
+     * @throws RuntimeException if the mapping doesn't exist or is not a {@code
+     *                       JSONArray}.
      */
-    public boolean isNull(String key)
-    {
-        return JSONObject.NULL.equals(opt(key));
+    public JSONArray getJSONArray(String name) {
+        Object object = get(name);
+        if (object instanceof JSONArray) {
+            return (JSONArray) object;
+        } else {
+            throw JSON.typeMismatch(false, name, object, "JSONArray");
+        }
     }
 
     /**
-     * Get an enumeration of the keys of the JSONObject. Caution: the set 
should not be modified.
+     * Returns the value mapped by {@code name} if it exists and is a {@code
+     * JSONObject}, or throws otherwise.
      *
-     * @return An iterator of the keys.
+     * @param name The name of the field that we want.
+     * @return a specified field value (if it is a JSONObject)
+     * @throws RuntimeException if the mapping doesn't exist or is not a {@code
+     *                       JSONObject}.
      */
-    public Set<String> keys()
-    {
-        return properties.keySet();
+    public JSONObject getJSONObject(String name) {
+        Object object = get(name);
+        if (object instanceof JSONObject) {
+            return (JSONObject) object;
+        } else {
+            throw JSON.typeMismatch(false, name, object, "JSONObject");
+        }
     }
 
     /**
-     * Get the number of keys stored in the JSONObject.
+     * Returns the set of {@code String} names in this object. The returned set
+     * is a view of the keys in this object. {@link Set#remove(Object)} will 
remove
+     * the corresponding mapping from this object and set iterator behaviour
+     * is undefined if this object is modified after it is returned.
      *
-     * @return The number of keys in the JSONObject.
+     * See {@link #keys()}.
+     *
+     * @return The names in this object.
      */
-    public int length()
-    {
-        return properties.size();
+    public Set<String> keys() {
+        return nameValuePairs.keySet();
     }
 
     /**
-     * Produce a JSONArray containing the names of the elements of this 
JSONObject.
+     * Returns an array containing the string names in this object. This method
+     * returns null if this object contains no mappings.
      *
-     * @return A JSONArray containing the key strings, or null if the 
JSONObject is empty.
+     * @return the names.
      */
-    public JSONArray names()
-    {
-        JSONArray ja = new JSONArray();
-
-        for (String key : keys())
-        {
-            ja.put(key);
-        }
-
-        return ja.length() == 0 ? null : ja;
+    public JSONArray names() {
+        return nameValuePairs.isEmpty()
+                ? null
+                : JSONArray.from(nameValuePairs.keySet());
     }
 
     /**
-     * Produce a string from a Number.
+     * Encodes the number as a JSON string.
      *
-     * @param n
-     *         A Number
-     * @return A String. @ If n is a non-finite number.
+     * @param number a finite value. May not be {@link Double#isNaN() NaNs} or
+     *               {@link Double#isInfinite() infinities}.
+     * @return The encoded number in string form.
+     * @throws RuntimeException On internal errors. Shouldn't happen.
      */
-    static String numberToString(Number n)
-    {
-        assert n != null;
+    public static String numberToString(Number number) {
+        if (number == null) {
+            throw new RuntimeException("Number must be non-null");
+        }
 
-        testValidity(n);
+        double doubleValue = number.doubleValue();
+        JSON.checkDouble(doubleValue);
 
-        // Shave off trailing zeros and decimal point, if possible.
+        // the original returns "-0" instead of "-0.0" for negative zero
+        if (number.equals(NEGATIVE_ZERO)) {
+            return "-0";
+        }
 
-        String s = n.toString();
-        if (s.indexOf('.') > 0 && s.indexOf('e') < 0 && s.indexOf('E') < 0)
-        {
-            while (s.endsWith("0"))
-            {
-                s = s.substring(0, s.length() - 1);
-            }
-            if (s.endsWith("."))
-            {
-                s = s.substring(0, s.length() - 1);
-            }
+        long longValue = number.longValue();
+        if (doubleValue == (double) longValue) {
+            return Long.toString(longValue);
         }
-        return s;
-    }
 
-    /**
-     * Get an optional value associated with a key.
-     *
-     * @param key
-     *         A key string.
-     * @return An object which is the value, or null if there is no value.
-     * @see #get(String)
-     */
-    public Object opt(String key)
-    {
-        return properties.get(key);
+        return number.toString();
     }
 
-    /**
-     * Put a key/value pair in the JSONObject. If the value is null, then the 
key will be removed from the JSONObject if
-     * it is present.
-     *
-     * @param key
-     *         A key string.
-     * @param value
-     *         An object which is the value. It should be of one of these 
types: Boolean, Double, Integer,
-     *         JSONArray, JSONObject, JSONLiteral, Long, String, or the 
JSONObject.NULL object.
-     * @return this.
-     * @throws RuntimeException
-     *         If the value is non-finite number or if the key is null.
-     */
-    public JSONObject put(String key, Object value)
+    static String doubleToString(double d)
     {
-        assert key != null;
-
-        if (value != null)
-        {
-            testValidity(value);
-            properties.put(key, value);
-        } else
+        if (Double.isInfinite(d) || Double.isNaN(d))
         {
-            remove(key);
+            return "null";
         }
 
-        return this;
+        return numberToString(d);
     }
 
     /**
-     * Produce a string in double quotes with backslash sequences in all the 
right places,
-     * allowing JSON text to be delivered in HTML. In JSON text, a string 
cannot contain a control character
-     * or an unescaped quote or backslash.
+     * Encodes {@code data} as a JSON string. This applies quotes and any
+     * necessary character escaping.
      *
-     * @param string
-     *         A String
-     * @return A String correctly formatted for insertion in a JSON text.
+     * @param data the string to encode. Null will be interpreted as an empty
+     *             string.
+     * @return the quoted string.
      */
-    public static String quote(String string)
-    {
-        if (string == null || string.length() == 0)
-        {
+    public static String quote(String data) {
+        if (data == null) {
             return "\"\"";
         }
-
-        char b;
-        char c = 0;
-        int i;
-        int len = string.length();
-        StringBuilder buffer = new StringBuilder(len + 4);
-        String t;
-
-        buffer.append('"');
-        for (i = 0; i < len; i += 1)
-        {
-            b = c;
-            c = string.charAt(i);
-            switch (c)
-            {
-                case '\\':
-                case '"':
-                    buffer.append('\\');
-                    buffer.append(c);
-                    break;
-                case '/':
-                    if (b == '<')
-                    {
-                        buffer.append('\\');
-                    }
-                    buffer.append(c);
-                    break;
-                case '\b':
-                    buffer.append("\\b");
-                    break;
-                case '\t':
-                    buffer.append("\\t");
-                    break;
-                case '\n':
-                    buffer.append("\\n");
-                    break;
-                case '\f':
-                    buffer.append("\\f");
-                    break;
-                case '\r':
-                    buffer.append("\\r");
-                    break;
-                default:
-                    if (c < ' ' || (c >= '\u0080' && c < '\u00a0') || (c >= 
'\u2000' && c < '\u2100'))
-                    {
-                        t = "000" + Integer.toHexString(c);
-                        buffer.append("\\u").append(t.substring(t.length() - 
4));
-                    } else
-                    {
-                        buffer.append(c);
-                    }
-            }
+        try {
+            JSONStringer stringer = new JSONStringer();
+            stringer.open(JSONStringer.Scope.NULL, "");
+            stringer.string(data);
+            stringer.close(JSONStringer.Scope.NULL, JSONStringer.Scope.NULL, 
"");
+            return stringer.toString();
+        } catch (RuntimeException e) {
+            throw new AssertionError();
         }
-        buffer.append('"');
-        return buffer.toString();
-    }
-
-    /**
-     * Remove a name and its value, if present.
-     *
-     * @param key
-     *         The name to be removed.
-     * @return The value that was associated with the name, or null if there 
was no value.
-     */
-    public Object remove(String key)
-    {
-        return properties.remove(key);
     }
 
-    private static final Class[] ALLOWED = new Class[]
-            {String.class, Boolean.class, Number.class, JSONObject.class, 
JSONArray.class, JSONString.class,
-                    JSONLiteral.class, Null.class};
-
-    /**
-     * Throw an exception if the object is an NaN or infinite number, or not a 
type which may be stored.
-     *
-     * @param value
-     *         The object to test. @ If o is a non-finite number.
-     */
-    @SuppressWarnings("unchecked")
-    static void testValidity(Object value)
-    {
-        if (value == null)
-            throw new IllegalArgumentException("null isn't valid in JSONObject 
and JSONArray. Use JSONObject.NULL instead.");
-
-        boolean found = false;
-        Class actual = value.getClass();
-
-        for (Class allowed : ALLOWED)
-        {
-            if (allowed.isAssignableFrom(actual))
-            {
-                found = true;
-                break;
-            }
-        }
-
-        if (!found)
-        {
-            List<String> typeNames = new ArrayList<String>();
-
-            for (Class c : ALLOWED)
-            {
-                String name = c.getName();
 
-                if (name.startsWith("java.lang."))
-                    name = name.substring(10);
-
-                typeNames.add(name);
-            }
-
-            Collections.sort(typeNames);
-
-            StringBuilder joined = new StringBuilder();
-            String sep = "";
-
-            for (String name : typeNames)
-            {
-                joined.append(sep);
-                joined.append(name);
-
-                sep = ", ";
-            }
-
-            String message = String.format("JSONObject properties may be one 
of %s. Type %s is not allowed.",
-                    joined.toString(), actual.getName());
-
-            throw new RuntimeException(message);
-        }
-
-        if (value instanceof Double)
-        {
-            Double asDouble = (Double) value;
-
-            if (asDouble.isInfinite() || asDouble.isNaN())
-            {
-                throw new RuntimeException(
-                        "JSON does not allow non-finite numbers.");
-            }
-
-            return;
-        }
-
-        if (value instanceof Float)
-        {
-            Float asFloat = (Float) value;
-
-            if (asFloat.isInfinite() || asFloat.isNaN())
-            {
-                throw new RuntimeException(
-                        "JSON does not allow non-finite numbers.");
-            }
-
-        }
-
-    }
 
     /**
      * Prints the JSONObject using the session.
@@ -889,7 +637,7 @@ public final class JSONObject extends JSONCollection
 
             session.printSymbol(':');
 
-            printValue(session, properties.get(key));
+            printValue(session, nameValuePairs.get(key));
 
             comma = true;
         }
@@ -902,6 +650,7 @@ public final class JSONObject extends JSONCollection
         session.printSymbol('}');
     }
 
+
     /**
      * Prints a value (a JSONArray or JSONObject, or a value stored in an 
array or object) using
      * the session.
@@ -910,7 +659,12 @@ public final class JSONObject extends JSONCollection
      */
     static void printValue(JSONPrintSession session, Object value)
     {
-        
+
+        if (value == null || value == NULL)
+        {
+            session.print("null");
+            return;
+        }
         if (value instanceof JSONObject)
         {
             ((JSONObject) value).print(session);
@@ -950,10 +704,6 @@ public final class JSONObject extends JSONCollection
         session.printQuoted(value.toString());
     }
 
-    /**
-     * Returns true if the other object is a JSONObject and its set of 
properties matches this object's properties.
-     */
-    @Override
     public boolean equals(Object obj)
     {
         if (obj == null)
@@ -964,7 +714,7 @@ public final class JSONObject extends JSONCollection
 
         JSONObject other = (JSONObject) obj;
 
-        return properties.equals(other.properties);
+        return nameValuePairs.equals(other.nameValuePairs);
     }
 
     /**
@@ -977,7 +727,7 @@ public final class JSONObject extends JSONCollection
      */
     public Map<String, Object> toMap()
     {
-        return Collections.unmodifiableMap(properties);
+        return Collections.unmodifiableMap(nameValuePairs);
     }
 
     /**
@@ -1000,6 +750,7 @@ public final class JSONObject extends JSONCollection
         return this;
     }
 
+
     /**
      * Navigates into a nested JSONObject, creating the JSONObject if 
necessary. They key must not exist,
      * or must be a JSONObject.
@@ -1013,7 +764,7 @@ public final class JSONObject extends JSONCollection
     {
         assert key != null;
 
-        Object nested = properties.get(key);
+        Object nested = nameValuePairs.get(key);
 
         if (nested != null && !(nested instanceof JSONObject))
         {
@@ -1023,9 +774,33 @@ public final class JSONObject extends JSONCollection
         if (nested == null)
         {
             nested = new JSONObject();
-            properties.put(key, nested);
+            nameValuePairs.put(key, nested);
         }
 
         return (JSONObject) nested;
     }
-}
+
+    static void testValidity(Object value)
+    {
+        if (value == null)
+          throw new IllegalArgumentException("null isn't valid in JSONObject 
and JSONArray. Use JSONObject.NULL instead.");
+        if (value == NULL)
+        {
+            return;
+        }
+        Class<? extends Object> clazz = value.getClass();
+        if (Boolean.class.isAssignableFrom(clazz)
+            || Number.class.isAssignableFrom(clazz)
+            || String.class.isAssignableFrom(clazz)
+            || JSONArray.class.isAssignableFrom(clazz)
+            || JSONLiteral.class.isAssignableFrom(clazz)
+            || JSONObject.class.isAssignableFrom(clazz)
+            || JSONString.class.isAssignableFrom(clazz))
+        {
+            return;
+        }
+
+        throw new RuntimeException("JSONObject properties may be one of 
Boolean, Number, String, org.apache.tapestry5.json.JSONArray, 
org.apache.tapestry5.json.JSONLiteral, org.apache.tapestry5.json.JSONObject, 
org.apache.tapestry5.json.JSONObject$Null, 
org.apache.tapestry5.json.JSONString. Type "+clazz.getName()+" is not 
allowed.");
+    }
+
+}
\ No newline at end of file

Reply via email to