You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tapestry.apache.org by th...@apache.org on 2020/11/14 11:06:04 UTC

[tapestry-5] branch 5.6.x updated (010f32f -> 917e471)

This is an automated email from the ASF dual-hosted git repository.

thiagohp pushed a change to branch 5.6.x
in repository https://gitbox.apache.org/repos/asf/tapestry-5.git.


    from 010f32f  TAP5-2643: NPE in Label when corresponding form field isn't rendered
     new f68f135  TAP5-2640: tapestry-json improvements
     new 917e471  TAP5-2640: fixing JavaDoc errors.

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../main/java/org/apache/tapestry5/json/JSON.java  |  43 +-
 .../java/org/apache/tapestry5/json/JSONArray.java  | 273 +++++++++--
 .../tapestry5/json/JSONExceptionBuilder.java       |  42 ++
 .../java/org/apache/tapestry5/json/JSONObject.java | 503 +++++++++++++++++----
 .../org/apache/tapestry5/json/JSONTokener.java     |  21 +-
 .../java/org/apache/tapestry5/json/JSONType.java   |  11 +
 .../JSONArrayIndexOutOfBoundsException.java        |  21 +
 .../json/exceptions/JSONInvalidTypeException.java  |  22 +
 .../json/exceptions/JSONSyntaxException.java       |  21 +
 .../json/exceptions/JSONTypeMismatchException.java |  39 ++
 .../exceptions/JSONValueNotFoundException.java     |  30 ++
 .../test/groovy/json/specs/JSONArraySpec.groovy    | 338 ++++++++++++--
 .../test/groovy/json/specs/JSONObjectSpec.groovy   | 135 ++++--
 .../src/test/groovy/json/specs/JSONSpec.groovy     |  36 ++
 14 files changed, 1296 insertions(+), 239 deletions(-)
 create mode 100644 tapestry-json/src/main/java/org/apache/tapestry5/json/JSONExceptionBuilder.java
 create mode 100644 tapestry-json/src/main/java/org/apache/tapestry5/json/JSONType.java
 create mode 100644 tapestry-json/src/main/java/org/apache/tapestry5/json/exceptions/JSONArrayIndexOutOfBoundsException.java
 create mode 100644 tapestry-json/src/main/java/org/apache/tapestry5/json/exceptions/JSONInvalidTypeException.java
 create mode 100644 tapestry-json/src/main/java/org/apache/tapestry5/json/exceptions/JSONSyntaxException.java
 create mode 100644 tapestry-json/src/main/java/org/apache/tapestry5/json/exceptions/JSONTypeMismatchException.java
 create mode 100644 tapestry-json/src/main/java/org/apache/tapestry5/json/exceptions/JSONValueNotFoundException.java
 create mode 100644 tapestry-json/src/test/groovy/json/specs/JSONSpec.groovy


[tapestry-5] 01/02: TAP5-2640: tapestry-json improvements

Posted by th...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

thiagohp pushed a commit to branch 5.6.x
in repository https://gitbox.apache.org/repos/asf/tapestry-5.git

commit f68f13545422f2a7f41414706a148b1780ec56eb
Author: Benjamin Weidig <be...@netzgut.net>
AuthorDate: Tue Jul 28 08:03:24 2020 +0200

    TAP5-2640: tapestry-json improvements
    
    (implement Collection/Map, better exceptions)
    
    tapestry-json: added JSONTypeMismatchException
    
    Added JSONTypeMismatchException and JSONValueNotFoundException. JSONObject behaviour improved by using opt() isntead of get for mor specific types, so we can actually use the new exceptions.
    
    tapestry-json: JSONArray get() now throws IndexOutOfBoundsException
    
    There's no reason to repackage the original exception into a RuntimeException.
    
    tapestry-json: JSONTokener constructors exceptions improved
    
    tapestry-json: improve JSONSpec tests
    
    tapestry-json: better exception building
    
    tapestry-json: added JSONSyntaxException
    
    tapesty-json: javadoc updated
    
    tapestry-json: JSONArray non-finite/nan check fixed
    
    The constructor wasn't using checkedPut, even though the javadoc states that doubles are checked.
    
    tapestry-json: javadoc typo
    
    tapestry-json: added JSONArrayIndexOutOfBoundsException
    
    tapestry-json: code style
    
    tapestrsy-json: fix lossy conversion
    
    tapestry-json: source formatting
    
    tapestry-json: throw IllegalArgumentException on invalid doubles
    
    tapestry-json: JSONObject implements Map<String, Object>
    
    To provide better interoperability with Java collections the JSONObject type now implements Map<String, Object>.
    
    The needed changes are marginal:
    - putAll -> now returns void (breaking change)
    - Arguments for key are Object -> shouldn't be a problem
    
    tapestry-json: added get{type}OrDefault methods to JSONObject
    
    tapestry-json: make JSONArray a "real" Collection (wip)
    
    tapestry-json: JSONArray improve javadoc
    
    tapestry-json: JSONObject improve javadoc
---
 .../main/java/org/apache/tapestry5/json/JSON.java  |  43 +-
 .../java/org/apache/tapestry5/json/JSONArray.java  | 271 +++++++++--
 .../tapestry5/json/JSONExceptionBuilder.java       |  42 ++
 .../java/org/apache/tapestry5/json/JSONObject.java | 499 +++++++++++++++++----
 .../org/apache/tapestry5/json/JSONTokener.java     |  21 +-
 .../java/org/apache/tapestry5/json/JSONType.java   |  11 +
 .../JSONArrayIndexOutOfBoundsException.java        |  21 +
 .../json/exceptions/JSONInvalidTypeException.java  |  22 +
 .../json/exceptions/JSONSyntaxException.java       |  21 +
 .../json/exceptions/JSONTypeMismatchException.java |  39 ++
 .../exceptions/JSONValueNotFoundException.java     |  30 ++
 .../test/groovy/json/specs/JSONArraySpec.groovy    | 338 ++++++++++++--
 .../test/groovy/json/specs/JSONObjectSpec.groovy   | 135 ++++--
 .../src/test/groovy/json/specs/JSONSpec.groovy     |  36 ++
 14 files changed, 1292 insertions(+), 237 deletions(-)

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
index 955ee44..169de73 100644
--- a/tapestry-json/src/main/java/org/apache/tapestry5/json/JSON.java
+++ b/tapestry-json/src/main/java/org/apache/tapestry5/json/JSON.java
@@ -16,13 +16,15 @@
 
 package org.apache.tapestry5.json;
 
+import org.apache.tapestry5.json.exceptions.JSONInvalidTypeException;
+
 class JSON {
     /**
      * Returns the input if it is a JSON-permissible value; throws otherwise.
      */
-    static double checkDouble(double d) throws RuntimeException {
+    static double checkDouble(double d) throws IllegalArgumentException {
         if (Double.isInfinite(d) || Double.isNaN(d)) {
-            throw new RuntimeException("JSON does not allow non-finite numbers.");
+            throw new IllegalArgumentException("JSON does not allow non-finite numbers.");
         }
         return d;
     }
@@ -92,24 +94,29 @@ class JSON {
         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 void testValidity(Object value)
+    {
+        if (value == null) {
+            throw new IllegalArgumentException("null isn't valid in JSONArray. Use JSONObject.NULL instead.");
         }
-    }
 
-    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);
+        if (value == JSONObject.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 JSONInvalidTypeException(clazz);
     }
 }
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 c1fb7f4..f4fcc2f 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
@@ -17,10 +17,16 @@
 package org.apache.tapestry5.json;
 
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.Iterator;
 import java.util.List;
 
+import org.apache.tapestry5.json.exceptions.JSONArrayIndexOutOfBoundsException;
+import org.apache.tapestry5.json.exceptions.JSONSyntaxException;
+import org.apache.tapestry5.json.exceptions.JSONTypeMismatchException;
+import org.apache.tapestry5.json.exceptions.JSONValueNotFoundException;
+
 // Note: this class was written without inspecting the non-free org.json sourcecode.
 
 /**
@@ -41,7 +47,7 @@ import java.util.List;
  *
  * Instances of this class are not thread safe.
  */
-public final class JSONArray extends JSONCollection implements Iterable<Object> {
+public final class JSONArray extends JSONCollection implements Collection<Object> {
 
     private final List<Object> values;
 
@@ -58,7 +64,8 @@ public final class JSONArray extends JSONCollection implements Iterable<Object>
      *
      * @param readFrom a tokener whose nextValue() method will yield a
      *                 {@code JSONArray}.
-     * @throws RuntimeException if the parse fails or doesn't yield a
+     * @throws JSONSyntaxException if the parse fails
+     * @throws JSONTypeMismatchException if it doesn't yield a
      *                       {@code JSONArray}.
      */
     JSONArray(JSONTokener readFrom) {
@@ -70,7 +77,7 @@ public final class JSONArray extends JSONCollection implements Iterable<Object>
         if (object instanceof JSONArray) {
             values = ((JSONArray) object).values;
         } else {
-            throw JSON.typeMismatch(object, "JSONArray");
+            throw JSONExceptionBuilder.tokenerTypeMismatch(object, JSONType.ARRAY);
         }
     }
 
@@ -78,8 +85,9 @@ public final class JSONArray extends JSONCollection implements Iterable<Object>
      * Creates a new {@code JSONArray} with values from the JSON string.
      *
      * @param json a JSON-encoded string containing an array.
-     * @throws RuntimeException if the parse fails or doesn't yield a {@code
-     *                       JSONArray}.
+     * @throws JSONSyntaxException if the parse fails
+     * @throws JSONTypeMismatchException if it doesn't yield a
+     *                       {@code JSONArray}.
      */
     public JSONArray(String json) {
         this(new JSONTokener(json));
@@ -89,17 +97,17 @@ public final class JSONArray extends JSONCollection implements Iterable<Object>
      * Creates a new {@code JSONArray} with values from the given primitive array.
      *
      * @param values The values to use.
-     * @throws RuntimeException if any of the values are non-finite double values (i.e. NaN or infinite)
+     * @throws IllegalArgumentException 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]);
+            checkedPut(values[i]);
         }
     }
 
     /**
-     * Create a new array, and adds all values fro the iterable to the array (using {@link #putAll(Iterable)}.
+     * Create a new array, and adds all values from the iterable to the array (using {@link #putAll(Iterable)}.
      *
      * This is implemented as a static method so as not to break the semantics of the existing {@link #JSONArray(Object...)} constructor.
      * Adding a constructor of type Iterable would change the meaning of <code>new JSONArray(new JSONArray())</code>.
@@ -115,12 +123,39 @@ public final class JSONArray extends JSONCollection implements Iterable<Object>
 
     /**
      * @return Returns the number of values in this array.
+     * @deprecated Use {@link #size()} instead.
      */
     public int length() {
+        return size();
+    }
+
+    /**
+     * Returns the number of values in this array.
+     * If this list contains more than {@code Integer.MAX_VALUE} elements, returns
+     * {@code Integer.MAX_VALUE}.
+     *
+     * @return the number of values in this array
+     * @since 5.7
+     */
+    @Override
+    public int size()
+    {
         return values.size();
     }
 
     /**
+     * Returns {@code true} if this array contains no values.
+     *
+     * @return {@code true} if this array contains no values
+     * @since 5.7
+     */
+    @Override
+    public boolean isEmpty()
+    {
+        return values.isEmpty();
+    }
+
+    /**
      * Appends {@code value} to the end of this array.
      *
      * @param value a {@link JSONObject}, {@link JSONArray}, String, Boolean,
@@ -129,20 +164,38 @@ public final class JSONArray extends JSONCollection implements Iterable<Object>
      *              infinities}. Unsupported values are not permitted and will cause the
      *              array to be in an inconsistent state.
      * @return this array.
+     * @deprecated The use of {@link #add(Object)] is encouraged.
      */
     public JSONArray put(Object value) {
-        JSONObject.testValidity(value);
-        values.add(value);
+        add(value);
         return this;
     }
 
     /**
+     * Appends {@code value} to the end of this array.
+     *
+     * @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 {@code true} (as specified by {@link Collection#add})
+     * @since 5.7
+     */
+    @Override
+    public boolean add(Object value)
+    {
+        JSON.testValidity(value);
+        return values.add(value);
+    }
+
+    /**
      * Same as {@link #put}, with added validity checks.
      *
      * @param value The value to append.
      */
     void checkedPut(Object value) {
-        JSONObject.testValidity(value);
+        JSON.testValidity(value);
         if (value instanceof Number) {
             JSON.checkDouble(((Number) value).doubleValue());
         }
@@ -161,14 +214,15 @@ public final class JSONArray extends JSONCollection implements Iterable<Object>
      *              not be {@link Double#isNaN() NaNs} or {@link Double#isInfinite()
      *              infinities}.
      * @return this array.
-     * @throws RuntimeException If the value cannot be represented as a finite double value.
+     * @throws IllegalArgumentException If the value cannot be represented as a finite double value.
+     * @throws ArrayIndexOutOfBoundsException if the index is lower than 0
      */
     public JSONArray put(int index, Object value) {
         if (index < 0)
         {
-            throw new RuntimeException("JSONArray[" + index + "] not found.");
+            throw new JSONArrayIndexOutOfBoundsException(index);
         }
-        JSONObject.testValidity(value);
+        JSON.testValidity(value);
         if (value instanceof Number) {
             // deviate from the original by checking all Numbers, not just floats & doubles
             JSON.checkDouble(((Number) value).doubleValue());
@@ -197,7 +251,8 @@ public final class JSONArray extends JSONCollection implements Iterable<Object>
      *
      * @param index Which value to get.
      * @return the value at the specified location.
-     * @throws RuntimeException if this array has no value at {@code index}, or if
+     * @throws JSONArrayIndexOutOfBoundsException if the given index is out of bounds.
+     * @throws JSONValueNotFoundException 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}.
      */
@@ -205,11 +260,12 @@ public final class JSONArray extends JSONCollection implements Iterable<Object>
         try {
             Object value = values.get(index);
             if (value == null) {
-                throw new RuntimeException("Value at " + index + " is null.");
+                throw JSONExceptionBuilder.valueNotFound(true, index, JSONType.ANY);
             }
             return value;
-        } catch (IndexOutOfBoundsException e) {
-            throw new RuntimeException("Index " + index + " out of range [0.." + values.size() + ")");
+        }
+        catch (IndexOutOfBoundsException e) {
+            throw new JSONArrayIndexOutOfBoundsException(index);
         }
     }
 
@@ -228,19 +284,74 @@ public final class JSONArray extends JSONCollection implements Iterable<Object>
     }
 
     /**
+     * Removes the first occurrence of the specified value from this JSONArray,
+     * if it is present.
+     *
+     * @param value value to be removed from this JSONArray, if present
+     * @return {@code true} if the element was removed
+     * @since 5.7
+     */
+    @Override
+    public boolean remove(Object value)
+    {
+        return values.remove(value);
+    }
+
+    /**
+     * Removes from this JSONArray all of its values that are contained in the
+     * specified collection.
+     *
+     * @param collection collection containing value to be removed from this JSONArray
+     * @return {@code true} if this JSONArray changed as a result of the call
+     * @throws NullPointerException if the specified collection is null.
+     * @see Collection#contains(Object)
+     * @since 5.7
+     */
+    @Override
+    public boolean removeAll(Collection<?> collection)
+    {
+        return values.removeAll(collection);
+    }
+
+    /**
+     * Removes all of the values from this JSONArray.
+     * 
+     * @since 5.7
+     */
+    @Override
+    public void clear()
+    {
+        values.clear();
+    }
+
+    /**
+     * Retains only the values in this JSONArray that are contained in the
+     * specified collection.
+     *
+     * @param collection collection containing elements to be retained in this list
+     * @return {@code true} if this list changed as a result of the call
+     * @since 5.7
+     */
+    @Override
+    public boolean retainAll(Collection<?> c)
+    {
+        return values.retainAll(c);
+    }
+
+    /**
      * Returns the value at {@code index} if it exists and is a boolean or can
      * be coerced to a boolean.
      *
      * @param index Which value to get.
      * @return the value at the specified location.
-     * @throws RuntimeException if the value at {@code index} doesn't exist or
+     * @throws JSONTypeMismatchException if the value at {@code index} doesn't exist or
      *                       cannot be coerced to a boolean.
      */
     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 JSONExceptionBuilder.typeMismatch(true, index, object, JSONType.BOOLEAN);
         }
         return result;
     }
@@ -251,14 +362,14 @@ public final class JSONArray extends JSONCollection implements Iterable<Object>
      *
      * @param index Which value to get.
      * @return the value at the specified location.
-     * @throws RuntimeException if the value at {@code index} doesn't exist or
+     * @throws JSONTypeMismatchException if the value at {@code index} doesn't exist or
      *                       cannot be coerced to a double.
      */
     public double getDouble(int index) {
         Object object = get(index);
         Double result = JSON.toDouble(object);
         if (result == null) {
-            throw JSON.typeMismatch(true, index, object, "number");
+            throw JSONExceptionBuilder.typeMismatch(true, index, object, JSONType.NUMBER);
         }
         return result;
     }
@@ -269,14 +380,14 @@ public final class JSONArray extends JSONCollection implements Iterable<Object>
      *
      * @param index Which value to get.
      * @return the value at the specified location.
-     * @throws RuntimeException if the value at {@code index} doesn't exist or
+     * @throws JSONTypeMismatchException if the value at {@code index} doesn't exist or
      *                       cannot be coerced to a int.
      */
     public int getInt(int index) {
         Object object = get(index);
         Integer result = JSON.toInteger(object);
         if (result == null) {
-            throw JSON.typeMismatch(true, index, object, "int");
+            throw JSONExceptionBuilder.typeMismatch(true, index, object, JSONType.NUMBER);
         }
         return result;
     }
@@ -287,14 +398,14 @@ public final class JSONArray extends JSONCollection implements Iterable<Object>
      *
      * @param index Which value to get.
      * @return the value at the specified location.
-     * @throws RuntimeException if the value at {@code index} doesn't exist or
+     * @throws JSONTypeMismatchException if the value at {@code index} doesn't exist or
      *                       cannot be coerced to a long.
      */
     public long getLong(int index) {
         Object object = get(index);
         Long result = JSON.toLong(object);
         if (result == null) {
-            throw JSON.typeMismatch(true, index, object, "long");
+            throw JSONExceptionBuilder.typeMismatch(true, index, object, JSONType.NUMBER);
         }
         return result;
     }
@@ -305,13 +416,13 @@ public final class JSONArray extends JSONCollection implements Iterable<Object>
      *
      * @param index Which value to get.
      * @return the value at the specified location.
-     * @throws RuntimeException if no such value exists.
+     * @throws JSONTypeMismatchException if no such value exists.
      */
     public String getString(int index) {
         Object object = get(index);
         String result = JSON.toString(object);
         if (result == null) {
-            throw JSON.typeMismatch(true, index, object, "String");
+            throw JSONExceptionBuilder.typeMismatch(true, index, object, JSONType.STRING);
         }
         return result;
     }
@@ -322,7 +433,7 @@ public final class JSONArray extends JSONCollection implements Iterable<Object>
      *
      * @param index Which value to get.
      * @return the value at the specified location.
-     * @throws RuntimeException if the value doesn't exist or is not a {@code
+     * @throws JSONTypeMismatchException if the value doesn't exist or is not a {@code
      *                       JSONArray}.
      */
     public JSONArray getJSONArray(int index) {
@@ -330,7 +441,7 @@ public final class JSONArray extends JSONCollection implements Iterable<Object>
         if (object instanceof JSONArray) {
             return (JSONArray) object;
         } else {
-            throw JSON.typeMismatch(true, index, object, "JSONArray");
+            throw JSONExceptionBuilder.typeMismatch(true, index, object, JSONType.ARRAY);
         }
     }
 
@@ -340,7 +451,7 @@ public final class JSONArray extends JSONCollection implements Iterable<Object>
      *
      * @param index Which value to get.
      * @return the value at the specified location.
-     * @throws RuntimeException if the value doesn't exist or is not a {@code
+     * @throws JSONTypeMismatchException if the value doesn't exist or is not a {@code
      *                       JSONObject}.
      */
     public JSONObject getJSONObject(int index) {
@@ -348,7 +459,7 @@ public final class JSONArray extends JSONCollection implements Iterable<Object>
         if (object instanceof JSONObject) {
             return (JSONObject) object;
         } else {
-            throw JSON.typeMismatch(true, index, object, "JSONObject");
+            throw JSONExceptionBuilder.typeMismatch(true, index, object, JSONType.OBJECT);
         }
     }
 
@@ -413,6 +524,30 @@ public final class JSONArray extends JSONCollection implements Iterable<Object>
     }
 
     /**
+     * Adds all objects from the collection into this JSONArray, using {@link #add(Object)}.
+     *
+     * @param collection Any collection, or null
+     * @return boolean true, if JSONArray was changed.
+     * @since 5.7
+     */
+    @Override
+    public boolean addAll(Collection<? extends Object> collection)
+    {
+        if (collection == null)
+        { 
+            return false;
+        }
+
+        boolean changed = false;
+        for (Object value : collection)
+        {
+            changed = add(value) || changed;
+        }
+
+        return changed;
+    }
+
+    /**
      * Returns an unmodifiable list of the contents of the array. This is a wrapper around the list's internal
      * storage and is live (changes to the JSONArray affect the returned List).
      *
@@ -424,12 +559,84 @@ public final class JSONArray extends JSONCollection implements Iterable<Object>
         return Collections.unmodifiableList(values);
     }
 
+    /**
+     * Returns an array containing all of the values in this JSONArray in proper
+     * sequence.
+     *
+     * @return an array containing all of the values in this JSONArray in proper
+     *         sequence
+     * @since 5.7
+     */
+    @Override
+    public Object[] toArray()
+    {
+        return values.toArray();
+    }
+
+    /**
+     * Returns an array containing all of the values in this JSONArray in
+     * proper sequence; the runtime type of the returned array is that of
+     * the specified array.
+     *
+     * @param array
+     *            the array into which the values of this JSONArray are to
+     *            be stored, if it is big enough; otherwise, a new array of the
+     *            same runtime type is allocated for this purpose.
+     * @return an array containing the values of this JSONArray
+     * @throws ArrayStoreException
+     *             if the runtime type of the specified array
+     *             is not a supertype of the runtime type of every element in
+     *             this list
+     * @throws NullPointerException
+     *             if the specified array is null
+     * @since 5.7
+     */
+    @Override
+    public <T> T[] toArray(T[] array)
+    {
+        return values.toArray(array);
+    }
 
+    /**
+     * Returns an iterator over the values in this array in proper sequence.
+     *
+     * @return an iterator over the values in this array in proper sequence
+     */
     @Override
     public Iterator<Object> iterator()
     {
         return values.iterator();
     }
 
+    /**
+     * Returns {@code true} if this JSONArray contains the specified value.
+     *
+     * @param value value whose presence in this JSONArray is to be tested
+     * @return {@code true} if this JSONArray contains the specified
+     *         value
+     * @since 5.7
+     */
+    @Override
+    public boolean contains(Object value)
+    {
+        return values.contains(value);
+    }
 
+    /**
+     * Returns {@code true} if this JSONArray contains all of the values
+     * in the specified collection.
+     *
+     * @param c collection to be checked for containment in this collection
+     * @return {@code true} if this collection contains all of the elements
+     *         in the specified collection
+     * @throws NullPointerException
+     *             if the specified collection is null.
+     * @see #contains(Object)
+     * @since 5.7
+     */
+    @Override
+    public boolean containsAll(Collection<?> c)
+    {
+        return values.containsAll(c);
+    }
 }
diff --git a/tapestry-json/src/main/java/org/apache/tapestry5/json/JSONExceptionBuilder.java b/tapestry-json/src/main/java/org/apache/tapestry5/json/JSONExceptionBuilder.java
new file mode 100644
index 0000000..0c26a88
--- /dev/null
+++ b/tapestry-json/src/main/java/org/apache/tapestry5/json/JSONExceptionBuilder.java
@@ -0,0 +1,42 @@
+package org.apache.tapestry5.json;
+
+import org.apache.tapestry5.json.exceptions.JSONTypeMismatchException;
+import org.apache.tapestry5.json.exceptions.JSONValueNotFoundException;
+
+class JSONExceptionBuilder
+{
+
+    static RuntimeException typeMismatch(boolean array, Object indexOrName, Object actual,
+            JSONType requiredType)
+    {
+        String location = array ? "JSONArray[" + indexOrName + "]"
+                : "JSONObject[\"" + indexOrName + "\"]";
+        if (actual == null)
+        {
+            return valueNotFound(array, indexOrName, requiredType);
+        }
+        else
+        {
+            return new JSONTypeMismatchException(location, requiredType, actual.getClass());
+        }
+    }
+
+    static RuntimeException valueNotFound(boolean array, Object indexOrName, JSONType requiredType)
+    {
+        String location = array ? "JSONArray[" + indexOrName + "]"
+                : "JSONObject[\"" + indexOrName + "\"]";
+        return new JSONValueNotFoundException(location, requiredType);
+    }
+
+    static RuntimeException tokenerTypeMismatch(Object actual, JSONType requiredType)
+    {
+        if (actual == null)
+        {
+            return new JSONValueNotFoundException("Value", requiredType);
+        }
+        else
+        {
+            return new JSONTypeMismatchException("Value", requiredType, actual.getClass());
+        }
+    }
+}
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 f5727fa..caf3285 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
@@ -18,11 +18,15 @@ package org.apache.tapestry5.json;
 
 import java.io.ObjectStreamException;
 import java.io.Serializable;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.LinkedHashMap;
 import java.util.Map;
 import java.util.Set;
 
+import org.apache.tapestry5.json.exceptions.JSONTypeMismatchException;
+import org.apache.tapestry5.json.exceptions.JSONValueNotFoundException;
+
 // Note: this class was written without inspecting the non-free org.json sourcecode.
 
 /**
@@ -71,7 +75,7 @@ import java.util.Set;
  *
  * <p>Instances of this class are not thread safe.
  */
-public final class JSONObject extends JSONCollection {
+public final class JSONObject extends JSONCollection implements Map<String, Object> {
 
     private static final Double NEGATIVE_ZERO = -0d;
 
@@ -146,7 +150,7 @@ public final class JSONObject extends JSONCollection {
         if (object instanceof JSONObject) {
             this.nameValuePairs = ((JSONObject) object).nameValuePairs;
         } else {
-            throw JSON.typeMismatch(object, "JSONObject");
+            throw JSONExceptionBuilder.tokenerTypeMismatch(object, JSONType.OBJECT);
         }
     }
 
@@ -197,7 +201,7 @@ public final class JSONObject extends JSONCollection {
 
     /**
      * Constructs a new JSONObject using a series of String keys and object values.
-     * Object values sholuld be compatible with {@link #put(String, Object)}. Keys must be strings
+     * Object values should be compatible with {@link #put(String, Object)}. Keys must be strings
      * (toString() will be invoked on each key).
      *
      * Prior to release 5.4, keysAndValues was type String...; changing it to Object... makes
@@ -219,9 +223,11 @@ public final class JSONObject extends JSONCollection {
 
     /**
      * Returns the number of name/value mappings in this object.
-     *
+     * 
+     * @deprecated Use {@link #size()} instead.
      * @return the length of this.
      */
+    @Deprecated
     public int length() {
         return nameValuePairs.size();
     }
@@ -237,14 +243,15 @@ public final class JSONObject extends JSONCollection {
      *              {@link Double#isNaN() NaNs} or {@link Double#isInfinite()
      *              infinities}.
      * @return this object.
-     * @throws RuntimeException if the value is an invalid double (infinite or NaN).
+     * @throws IllegalArgumentException if the value is an invalid double (infinite or NaN).
      */
+    @Override
     public JSONObject put(String name, Object value) {
         if (value == null) {
             nameValuePairs.remove(name);
             return this;
         }
-        testValidity(value);
+        JSON.testValidity(value);
         if (value instanceof Number) {
             // deviate from the original by checking all Numbers, not just floats & doubles
             JSON.checkDouble(((Number) value).doubleValue());
@@ -303,11 +310,11 @@ public final class JSONObject extends JSONCollection {
      * @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
+     * @throws JSONTypeMismatchException if {@code name} is {@code null} or if the mapping for
      *                       {@code name} is non-null and is not a {@link JSONArray}.
      */
     public JSONObject append(String name, Object value) {
-        testValidity(value);
+        JSON.testValidity(value);
         Object current = nameValuePairs.get(checkName(name));
 
         final JSONArray array;
@@ -318,7 +325,7 @@ public final class JSONObject extends JSONCollection {
             nameValuePairs.put(name, newArray);
             array = newArray;
         } else {
-            throw new RuntimeException("JSONObject[\"" + name + "\"] is not a JSONArray.");
+            throw new JSONTypeMismatchException("JSONObject[\"" + name + "\"]", JSONType.ARRAY, current.getClass());
         }
 
         array.checkedPut(value);
@@ -334,17 +341,6 @@ public final class JSONObject extends JSONCollection {
     }
 
     /**
-     * Removes the named mapping if it exists; does nothing otherwise.
-     *
-     * @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.
-     */
-    public Object remove(String name) {
-        return nameValuePairs.remove(name);
-    }
-
-    /**
      * Returns true if this object has no mapping for {@code name} or if it has
      * a mapping whose value is {@link #NULL}.
      *
@@ -360,26 +356,14 @@ public final class JSONObject extends JSONCollection {
      * Returns true if this object has a mapping for {@code name}. The mapping
      * may be {@link #NULL}.
      *
-     * @param name The name of the value to check on.
+     * @deprecated use {@link #containsKey(Object)} instead
+     * @param name
+     *            The name of the value to check on.
      * @return true if this object has a field named {@code name}
      */
+    @Deprecated
     public boolean has(String name) {
-        return nameValuePairs.containsKey(name);
-    }
-
-    /**
-     * Returns the value mapped by {@code name}, or throws if no such mapping exists.
-     *
-     * @param name The name of the value to get.
-     * @return The value.
-     * @throws RuntimeException if no such mapping exists.
-     */
-    public Object get(String name) {
-        Object result = nameValuePairs.get(name);
-        if (result == null) {
-            throw new RuntimeException("JSONObject[\"" + name + "\"] not found.");
-        }
-        return result;
+        return containsKey(name);
     }
 
     /**
@@ -389,7 +373,7 @@ public final class JSONObject extends JSONCollection {
      * @param name The name of the value to get.
      * @return The value.
      */
-    public Object opt(String name) {
+    public Object opt(Object name) {
         return nameValuePairs.get(name);
     }
 
@@ -399,14 +383,45 @@ public final class JSONObject extends JSONCollection {
      *
      * @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
+     * @throws JSONValueNotFoundException if the mapping doesn't exist
+     * @throws JSONTypeMismatchException if the mapping cannot be coerced
      *                       to a boolean.
      */
     public boolean getBoolean(String name) {
-        Object object = get(name);
+        Object object = opt(name);
+        if (object == null) {
+            throw JSONExceptionBuilder.valueNotFound(false, name, JSONType.BOOLEAN);
+        }
         Boolean result = JSON.toBoolean(object);
         if (result == null) {
-            throw JSON.typeMismatch(false, name, object, "Boolean");
+            throw JSONExceptionBuilder.typeMismatch(false, name, object, JSONType.BOOLEAN);
+        }
+        return result;
+    }
+
+    /**
+     * Returns the value to which the specified key is mapped and a boolean, or
+     * {@code defaultValue} if this JSONObject contains no mapping for the key.
+     *
+     * @param key the key whose associated value is to be returned
+     * @param defaultValue the default mapping of the key
+     * @return the value to which the specified key is mapped, or
+     * {@code defaultValue} if this JSONObject contains no mapping for the key
+     * @throws JSONTypeMismatchException if the mapping cannot be coerced
+     *                       to a boolean.
+     * @since 5.7
+     */
+    public boolean getBooleanOrDefault(String name, boolean defaultValue)
+    {
+        Object object = opt(name);
+        if (object == null)
+        {
+            return defaultValue;
+        }
+        Boolean result = JSON.toBoolean(object);
+        if (result == null)
+        {
+            throw JSONExceptionBuilder.typeMismatch(false, name, object, JSONType.BOOLEAN);
         }
         return result;
     }
@@ -417,14 +432,18 @@ public final class JSONObject extends JSONCollection {
      *
      * @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
+     * @throws JSONValueNotFoundException if the mapping doesn't exist
+     * @throws JSONTypeMismatchException if the mapping cannot be coerced
      *                       to a double.
      */
     public double getDouble(String name) {
-        Object object = get(name);
+        Object object = opt(name);
+        if (object == null) {
+            throw JSONExceptionBuilder.valueNotFound(false, name, JSONType.NUMBER);
+        }
         Double result = JSON.toDouble(object);
         if (result == null) {
-            throw JSON.typeMismatch(false, name, object, "number");
+            throw JSONExceptionBuilder.typeMismatch(false, name, object, JSONType.NUMBER);
         }
         return result;
     }
@@ -435,19 +454,50 @@ public final class JSONObject extends JSONCollection {
      *
      * @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
+     * @throws JSONValueNotFoundException if the mapping doesn't exist
+     * @throws JSONTypeMismatchException if the mapping cannot be coerced
      *                       to an int.
      */
     public int getInt(String name) {
-        Object object = get(name);
+        Object object = opt(name);
+        if (object == null) {
+            throw JSONExceptionBuilder.valueNotFound(false, name, JSONType.NUMBER);
+        }
         Integer result = JSON.toInteger(object);
         if (result == null) {
-            throw JSON.typeMismatch(false, name, object, "int");
+            throw JSONExceptionBuilder.typeMismatch(false, name, object, JSONType.NUMBER);
         }
         return result;
     }
 
     /**
+     * Returns the value to which the specified key is mapped and an int, or
+     * {@code defaultValue} if this JSONObject contains no mapping for the key.
+     *
+     * @param key the key whose associated value is to be returned
+     * @param defaultValue the default mapping of the key
+     * @return the value to which the specified key is mapped, or
+     * {@code defaultValue} if this JSONObject contains no mapping for the key
+     * @throws JSONTypeMismatchException if the mapping cannot be coerced
+     *                       to an int.
+     * @since 5.7
+     */
+    public int getIntOrDefault(String name, int defaultValue)
+    {
+        Object object = opt(name);
+        if (object == null)
+        {
+            return defaultValue;
+        }
+        Integer result = JSON.toInteger(object);
+        if (result == null)
+        {
+            throw JSONExceptionBuilder.typeMismatch(false, name, object, JSONType.NUMBER);
+        }
+        return result;
+    }
+    
+    /**
      * 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,
@@ -457,14 +507,45 @@ public final class JSONObject extends JSONCollection {
      *
      * @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
+     * @throws JSONValueNotFoundException if the mapping doesn't exist
+     * @throws JSONTypeMismatchException if the mapping cannot be coerced
      *                       to a long.
      */
     public long getLong(String name) {
-        Object object = get(name);
+        Object object = opt(name);
+        if (object == null) {
+            throw JSONExceptionBuilder.valueNotFound(false, name, JSONType.NUMBER);
+        }
         Long result = JSON.toLong(object);
         if (result == null) {
-            throw JSON.typeMismatch(false, name, object, "long");
+            throw JSONExceptionBuilder.typeMismatch(false, name, object, JSONType.NUMBER);
+        }
+        return result;
+    }
+
+    /**
+     * Returns the value to which the specified key is mapped and a long, or
+     * {@code defaultValue} if this JSONObject contains no mapping for the key.
+     *
+     * @param key the key whose associated value is to be returned
+     * @param defaultValue the default mapping of the key
+     * @return the value to which the specified key is mapped, or
+     * {@code defaultValue} if this JSONObject contains no mapping for the key
+     * @throws JSONTypeMismatchException if the mapping cannot be coerced
+     *                       to a long.
+     * @since 5.7
+     */
+    public long getLongOrDefault(String name, int defaultValue)
+    {
+        Object object = opt(name);
+        if (object == null)
+        {
+            return defaultValue;
+        }
+        Long result = JSON.toLong(object);
+        if (result == null)
+        {
+            throw JSONExceptionBuilder.typeMismatch(false, name, object, JSONType.NUMBER);
         }
         return result;
     }
@@ -475,53 +556,145 @@ public final class JSONObject extends JSONCollection {
      *
      * @param name The name of the field we want.
      * @return The value of the field.
-     * @throws RuntimeException if no such mapping exists.
+     * @throws JSONValueNotFoundException if the mapping doesn't exist
+     * @throws JSONTypeMismatchException if the mapping cannot be coerced
+     *                               to String
      */
     public String getString(String name) {
-        Object object = get(name);
+        Object object = opt(name);
+        if (object == null) {
+            throw JSONExceptionBuilder.valueNotFound(false, name, JSONType.STRING);
+        }
         String result = JSON.toString(object);
         if (result == null) {
-            throw JSON.typeMismatch(false, name, object, "String");
+            throw JSONExceptionBuilder.typeMismatch(false, name, object, JSONType.STRING);
         }
         return result;
     }
 
     /**
+     * Returns the value to which the specified key is mapped and a string, or
+     * {@code defaultValue} if this JSONObject contains no mapping for the key.
+     *
+     * @param key the key whose associated value is to be returned
+     * @param defaultValue the default mapping of the key
+     * @return the value to which the specified key is mapped, or
+     * {@code defaultValue} if this JSONObject contains no mapping for the key
+     * @throws JSONTypeMismatchException if the mapping cannot be coerced
+     *                       to a string.
+     * @since 5.7
+     */
+    public String getStringOrDefault(String name, String defaultValue)
+    {
+        Object object = opt(name);
+        if (object == null)
+        {
+            return defaultValue;
+        }
+        String result = JSON.toString(object);
+        if (result == null)
+        {
+            throw JSONExceptionBuilder.typeMismatch(false, name, object, JSONType.STRING);
+        }
+        return result;
+    }
+    
+    /**
      * Returns the value mapped by {@code name} if it exists and is a {@code
      * JSONArray}, or throws otherwise.
      *
      * @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
+     * @throws JSONValueNotFoundException if the mapping doesn't exist
+     * @throws JSONTypeMismatchException if the mapping is not a {@code
      *                       JSONArray}.
      */
     public JSONArray getJSONArray(String name) {
-        Object object = get(name);
+        Object object = opt(name);
+        if (object == null) {
+            throw JSONExceptionBuilder.valueNotFound(false, name, JSONType.ARRAY);
+        }
         if (object instanceof JSONArray) {
             return (JSONArray) object;
         } else {
-            throw JSON.typeMismatch(false, name, object, "JSONArray");
+            throw JSONExceptionBuilder.typeMismatch(false, name, object, JSONType.ARRAY);
         }
     }
 
     /**
+     * Returns the value to which the specified key is mapped and a JSONArray, or
+     * {@code defaultValue} if this JSONObject contains no mapping for the key.
+     *
+     * @param key the key whose associated value is to be returned
+     * @param defaultValue the default mapping of the key
+     * @return the value to which the specified key is mapped, or
+     * {@code defaultValue} if this JSONObject contains no mapping for the key
+     * @throws JSONTypeMismatchException if the mapping cannot be coerced
+     *                       to a JSONArray.
+     * @since 5.7
+     */
+    public JSONArray getJSONArrayOrDefault(String name, JSONArray defaultValue)
+    {
+        Object object = opt(name);
+        if (object == null)
+        {
+            return defaultValue;
+        }
+        if (object instanceof JSONArray) {
+            return (JSONArray) object;
+        } else {
+            throw JSONExceptionBuilder.typeMismatch(false, name, object, JSONType.ARRAY);
+        }
+    }
+    
+    /**
      * Returns the value mapped by {@code name} if it exists and is a {@code
      * JSONObject}, or throws otherwise.
      *
      * @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
+     * @throws JSONValueNotFoundException if the mapping doesn't exist
+     * @throws JSONTypeMismatchException if the mapping is not a {@code
      *                       JSONObject}.
      */
     public JSONObject getJSONObject(String name) {
-        Object object = get(name);
+        Object object = opt(name);
+        if (object == null) {
+            throw JSONExceptionBuilder.valueNotFound(false, name, JSONType.OBJECT);
+        }
         if (object instanceof JSONObject) {
             return (JSONObject) object;
         } else {
-            throw JSON.typeMismatch(false, name, object, "JSONObject");
+            throw JSONExceptionBuilder.typeMismatch(false, name, object, JSONType.OBJECT);
         }
     }
 
+    
+    /**
+     * Returns the value to which the specified key is mapped and a JSONObject, or
+     * {@code defaultValue} if this map contains no mapping for the key.
+     *
+     * @param key the key whose associated value is to be returned
+     * @param defaultValue the default mapping of the key
+     * @return the value to which the specified key is mapped, or
+     * {@code defaultValue} if this map contains no mapping for the key
+     * @throws JSONTypeMismatchException if the mapping cannot be coerced
+     *                       to a JSONObject.
+     * @since 5.7
+     */
+    public JSONObject getJSONObjectOrDefault(String name, JSONObject defaultValue)
+    {
+        Object object = opt(name);
+        if (object == null)
+        {
+            return defaultValue;
+        }
+        if (object instanceof JSONObject) {
+            return (JSONObject) object;
+        } else {
+            throw JSONExceptionBuilder.typeMismatch(false, name, object, JSONType.OBJECT);
+        }
+    }
     /**
      * 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
@@ -731,27 +904,6 @@ public final class JSONObject extends JSONCollection {
     }
 
     /**
-     * Invokes {@link #put(String, Object)} for each value from the map.
-     *
-     * @param newProperties
-     *         to add to this JSONObject
-     * @return this JSONObject
-     * @since 5.4
-     */
-    public JSONObject putAll(Map<String, ?> newProperties)
-    {
-        assert newProperties != null;
-
-        for (Map.Entry<String, ?> e : newProperties.entrySet())
-        {
-            put(e.getKey(), e.getValue());
-        }
-
-        return this;
-    }
-
-
-    /**
      * Navigates into a nested JSONObject, creating the JSONObject if necessary. They key must not exist,
      * or must be a JSONObject.
      *
@@ -780,27 +932,180 @@ public final class JSONObject extends JSONCollection {
         return (JSONObject) nested;
     }
 
-    static void testValidity(Object value)
+    /**
+     * Returns the number of key-value mappings in this JSONObject.
+     * If it contains more than {@code Integer.MAX_VALUE} elements, returns
+     * {@code Integer.MAX_VALUE}.
+     *
+     * @return the number of key-value mappings in this JSONObject
+     * @since 5.7
+     */
+    @Override
+    public int size()
     {
-        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 nameValuePairs.size();
+    }
+
+    /**
+     * Returns {@code true} if this JSONObject contains no key-value mappings.
+     *
+     * @return {@code true} if this JSONObject contains no key-value mappings
+     * @since 5.7
+     */
+    @Override
+    public boolean isEmpty()
+    {
+        return nameValuePairs.isEmpty();
+    }
+
+    /**
+     * Returns {@code true} if this JSONObject contains a mapping for the specified
+     * key.
+     *
+     * @param key
+     *            key whose presence in this map is to be tested
+     * @return {@code true} if this map contains a mapping for the specified
+     *         key
+     * @since 5.7
+     */
+    @Override
+    public boolean containsKey(Object key)
+    {
+        return nameValuePairs.containsKey(key);
+    }
+
+    /**
+     * Returns {@code true} if this JSONObject maps one or more keys to the
+     * specified value.
+     *
+     * @param value value whose presence in this map is to be tested
+     * @return {@code true} if this JSONObject maps one or more keys to the
+     *         specified value
+     * @since 5.7
+     */
+    @Override
+    public boolean containsValue(Object value)
+    {
+        return nameValuePairs.containsValue(value);
+    }
+
+    /**
+     * Returns the value mapped by {@code name}, or throws if no such mapping exists.
+     *
+     * @param name The name of the value to get.
+     * @return The value.
+     * @throws JSONValueNotFoundException if no such mapping exists.
+     */ @Override
+    public Object get(Object key)
+    {
+        Object result = nameValuePairs.get(key);
+         if (result == null) {
+            throw JSONExceptionBuilder.valueNotFound(false, key, JSONType.ANY);
+         }
+         return result;
+    }
+
+    /**
+      * Returns the value to which the specified key is mapped, or
+      * {@code defaultValue} if this JSONObject contains no mapping for the key.
+      *
+      * @param key the key whose associated value is to be returned
+      * @param defaultValue the default mapping of the key
+      * @return the value to which the specified key is mapped, or
+      *         {@code defaultValue} if this JSONObject contains no mapping for the key
+      * @since 5.7
+      */
+    @Override
+    public Object getOrDefault(Object key, Object defaultValue)
+    {
+        Object value = opt(key);
+        return value == null ? defaultValue : value;
+    }
+
+    /**
+     * Removes the named mapping if it exists; does nothing otherwise.
+     *
+     * @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.
+     */
+    @Override
+    public Object remove(Object key)
+    {
+        return nameValuePairs.remove(key);
+    }
+
+    /**
+     * Invokes {@link #put(String, Object)} for each value from the map.
+     *
+     * @param newProperties
+     *            to add to this JSONObject
+     * @return this JSONObject
+     * @since 5.7
+     */
+    @Override
+    public void putAll(Map<? extends String, ? extends Object> m)
+    {
+        assert m != null;
+
+        for (Map.Entry<? extends String, ? extends Object> e : m.entrySet())
         {
-            return;
+            put(e.getKey(), e.getValue());
         }
+    }
 
-        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.");
+    /**
+     * Removes all of the mappings from this JSONObject.
+     * 
+     * @since 5.7
+     */
+    @Override
+    public void clear()
+    {
+        nameValuePairs.clear();
+    }
+
+    /**
+     * Returns a {@link Set} view of the keys contained in this JSONObject.
+     * The set is backed by the JSONObject, so changes to the map are
+     * reflected in the set, and vice-versa.
+     *
+     * @return a set view of the keys contained in this JSONObject
+     * @since 5.7
+     */
+    @Override
+    public Set<String> keySet()
+    {
+        return nameValuePairs.keySet();
+    }
+
+    /**
+     * Returns a {@link Collection} view of the values contained in this JSONObject.
+     * The collection is backed by the JSONObject, so changes to the map are
+     * reflected in the collection, and vice-versa.
+     *
+     * @return a collection view of the values contained in this JSONObject
+     * @since 5.7
+     */
+    @Override
+    public Collection<Object> values()
+    {
+        return nameValuePairs.values();
+    }
+
+    /**
+     * Returns a {@link Set} view of the mappings contained in this JSONObject.
+     * The set is backed by the JSONObject, so changes to the map are
+     * reflected in the set, and vice-versa.
+     *
+     * @return a set view of the mappings contained in this JSONObject
+     * @since 5.7
+     */
+    @Override
+    public Set<Entry<String, Object>> entrySet()
+    {
+        return nameValuePairs.entrySet();
     }
+  
 
 }
\ No newline at end of file
diff --git a/tapestry-json/src/main/java/org/apache/tapestry5/json/JSONTokener.java b/tapestry-json/src/main/java/org/apache/tapestry5/json/JSONTokener.java
index 38fb443..e0c4dac 100644
--- a/tapestry-json/src/main/java/org/apache/tapestry5/json/JSONTokener.java
+++ b/tapestry-json/src/main/java/org/apache/tapestry5/json/JSONTokener.java
@@ -20,6 +20,9 @@ package org.apache.tapestry5.json;
 
 import java.io.IOException;
 import java.io.Reader;
+import java.text.MessageFormat;
+
+import org.apache.tapestry5.json.exceptions.JSONSyntaxException;
 
 /**
  * Parses a JSON (<a href="http://www.ietf.org/rfc/rfc4627.txt">RFC 4627</a>)
@@ -105,15 +108,15 @@ class JSONTokener {
      *
      * @return a {@link JSONObject}, {@link JSONArray}, String, Boolean,
      * Integer, Long, Double or {@link JSONObject#NULL}.
-     * @throws RuntimeException if the input is malformed.
+     * @throws JSONSyntaxException if the input is malformed.
      */
      Object nextValue(Class<?> desiredType) {
         int c = nextCleanInternal();
         if (JSONObject.class.equals(desiredType) && c != '{'){
-            throw syntaxError("A JSONObject text must begin with '{'");
+            throw syntaxError(MessageFormat.format("A JSONObject text must start with '''{''' (actual: ''{0}'')", Character.toString((char)c)));
         }
         if (JSONArray.class.equals(desiredType) && c != '['){
-          throw syntaxError("A JSONArray text must start with '['");
+          throw syntaxError(MessageFormat.format("A JSONArray text must start with ''['' (actual: ''{0}'')", Character.toString((char)c)));
         }
         switch (c) {
             case -1:
@@ -269,24 +272,28 @@ class JSONTokener {
                     throw syntaxError("Unterminated escape sequence");
                 }
                 String hex = in.substring(pos, pos + 4);
-                pos += 4;
                 try {
                     return (char) Integer.parseInt(hex, 16);
                 } catch (NumberFormatException nfe) {
                     throw syntaxError("Invalid escape sequence: " + hex);
                 }
+                finally {
+                    pos += 4;
+                }
             }
             case 'x': {
               if (pos + 2 > in.length()) {
                   throw syntaxError("Unterminated escape sequence");
               }
               String hex = in.substring(pos, pos + 2);
-              pos += 2;
               try {
                   return (char) Integer.parseInt(hex, 16);
               } catch (NumberFormatException nfe) {
                   throw syntaxError("Invalid escape sequence: " + hex);
               }
+              finally {
+                  pos += 2;
+              }
 
             }
             case 't':
@@ -498,8 +505,8 @@ class JSONTokener {
      * @param message The message we want to include.
      * @return An exception that we can throw.
      */
-    private RuntimeException syntaxError(String message) {
-        return new RuntimeException(message + this);
+    private JSONSyntaxException syntaxError(String message) {
+        return new JSONSyntaxException(this.pos, message + this);
     }
 
     /**
diff --git a/tapestry-json/src/main/java/org/apache/tapestry5/json/JSONType.java b/tapestry-json/src/main/java/org/apache/tapestry5/json/JSONType.java
new file mode 100644
index 0000000..7c97f3c
--- /dev/null
+++ b/tapestry-json/src/main/java/org/apache/tapestry5/json/JSONType.java
@@ -0,0 +1,11 @@
+package org.apache.tapestry5.json;
+
+/**
+ * Represents the different data types supported by JSON.
+ * Mostly used for descriptive reasons in exceptions.
+ */
+public enum JSONType
+{
+
+    BOOLEAN, STRING, NUMBER, OBJECT, ARRAY, ANY;
+}
diff --git a/tapestry-json/src/main/java/org/apache/tapestry5/json/exceptions/JSONArrayIndexOutOfBoundsException.java b/tapestry-json/src/main/java/org/apache/tapestry5/json/exceptions/JSONArrayIndexOutOfBoundsException.java
new file mode 100644
index 0000000..923e510
--- /dev/null
+++ b/tapestry-json/src/main/java/org/apache/tapestry5/json/exceptions/JSONArrayIndexOutOfBoundsException.java
@@ -0,0 +1,21 @@
+package org.apache.tapestry5.json.exceptions;
+
+public class JSONArrayIndexOutOfBoundsException extends ArrayIndexOutOfBoundsException
+{
+
+    private static final long serialVersionUID = -53336156278974940L;
+
+    private final int index;
+
+    public JSONArrayIndexOutOfBoundsException(int index)
+    {
+        super(index);
+        this.index = index;
+    }
+
+    public int getIndex()
+    {
+        return this.index;
+    }
+
+}
diff --git a/tapestry-json/src/main/java/org/apache/tapestry5/json/exceptions/JSONInvalidTypeException.java b/tapestry-json/src/main/java/org/apache/tapestry5/json/exceptions/JSONInvalidTypeException.java
new file mode 100644
index 0000000..69873ca
--- /dev/null
+++ b/tapestry-json/src/main/java/org/apache/tapestry5/json/exceptions/JSONInvalidTypeException.java
@@ -0,0 +1,22 @@
+package org.apache.tapestry5.json.exceptions;
+
+public class JSONInvalidTypeException extends RuntimeException
+{
+
+    private static final long serialVersionUID = 934805933638996600L;
+
+    private Class<? extends Object> invalidClass;
+
+    public JSONInvalidTypeException(Class<? extends Object> invalidClass)
+    {
+        super("JSONArray values / 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 "
+                + invalidClass.getName() + " is not allowed.");
+
+        this.invalidClass = invalidClass;
+    }
+
+    public Class<? extends Object> getInvalidClass()
+    {
+        return this.invalidClass;
+    }
+}
diff --git a/tapestry-json/src/main/java/org/apache/tapestry5/json/exceptions/JSONSyntaxException.java b/tapestry-json/src/main/java/org/apache/tapestry5/json/exceptions/JSONSyntaxException.java
new file mode 100644
index 0000000..7323bc3
--- /dev/null
+++ b/tapestry-json/src/main/java/org/apache/tapestry5/json/exceptions/JSONSyntaxException.java
@@ -0,0 +1,21 @@
+package org.apache.tapestry5.json.exceptions;
+
+public class JSONSyntaxException extends RuntimeException
+{
+
+    private static final long serialVersionUID = 5647885303727734937L;
+
+    private final int position;
+
+    public JSONSyntaxException(int position, String message)
+    {
+        super(message);
+        this.position = position;
+    }
+
+    public int getPosition()
+    {
+        return this.position;
+    }
+
+}
diff --git a/tapestry-json/src/main/java/org/apache/tapestry5/json/exceptions/JSONTypeMismatchException.java b/tapestry-json/src/main/java/org/apache/tapestry5/json/exceptions/JSONTypeMismatchException.java
new file mode 100644
index 0000000..ac16203
--- /dev/null
+++ b/tapestry-json/src/main/java/org/apache/tapestry5/json/exceptions/JSONTypeMismatchException.java
@@ -0,0 +1,39 @@
+package org.apache.tapestry5.json.exceptions;
+
+import org.apache.tapestry5.json.JSONType;
+
+public class JSONTypeMismatchException extends RuntimeException
+{
+
+    private static final long serialVersionUID = 268314880458464132L;
+
+    private final String location;
+    private final JSONType requiredType;
+    private final Class<? extends Object> invalidClass;
+
+    public JSONTypeMismatchException(String location, JSONType requiredType,
+            Class<? extends Object> invalidClass)
+    {
+        super(location + (invalidClass == null ? " is null."
+                : " is not a " + requiredType.name() + ". Actual: " + invalidClass.getName()));
+        this.location = location;
+        this.requiredType = requiredType;
+        this.invalidClass = invalidClass;
+    }
+
+    public String getLocation()
+    {
+        return this.location;
+    }
+
+    public JSONType getRequiredType()
+    {
+        return this.requiredType;
+    }
+
+    public Class<? extends Object> getInvalidClass()
+    {
+        return this.invalidClass;
+    }
+
+}
diff --git a/tapestry-json/src/main/java/org/apache/tapestry5/json/exceptions/JSONValueNotFoundException.java b/tapestry-json/src/main/java/org/apache/tapestry5/json/exceptions/JSONValueNotFoundException.java
new file mode 100644
index 0000000..46193dc
--- /dev/null
+++ b/tapestry-json/src/main/java/org/apache/tapestry5/json/exceptions/JSONValueNotFoundException.java
@@ -0,0 +1,30 @@
+package org.apache.tapestry5.json.exceptions;
+
+import org.apache.tapestry5.json.JSONType;
+
+public class JSONValueNotFoundException extends RuntimeException
+{
+
+    private static final long serialVersionUID = -8709125433506778675L;
+
+    private final String location;
+    private final JSONType requiredType;
+
+    public JSONValueNotFoundException(String location, JSONType requiredType)
+    {
+        super(location + " is not found. Required: " + requiredType.name());
+        this.location = location;
+        this.requiredType = requiredType;
+    }
+
+    public String getLocation()
+    {
+        return this.location;
+    }
+
+    public JSONType getRequiredType()
+    {
+        return this.requiredType;
+    }
+
+}
diff --git a/tapestry-json/src/test/groovy/json/specs/JSONArraySpec.groovy b/tapestry-json/src/test/groovy/json/specs/JSONArraySpec.groovy
index 7cbab1b..35b7658 100644
--- a/tapestry-json/src/test/groovy/json/specs/JSONArraySpec.groovy
+++ b/tapestry-json/src/test/groovy/json/specs/JSONArraySpec.groovy
@@ -1,6 +1,13 @@
 package json.specs
 
 import org.apache.tapestry5.json.JSONArray
+import org.apache.tapestry5.json.JSONLiteral
+import org.apache.tapestry5.json.JSONObject
+import org.apache.tapestry5.json.exceptions.JSONArrayIndexOutOfBoundsException
+import org.apache.tapestry5.json.exceptions.JSONInvalidTypeException
+import org.apache.tapestry5.json.exceptions.JSONSyntaxException
+import org.apache.tapestry5.json.exceptions.JSONTypeMismatchException
+
 import spock.lang.Specification
 
 class JSONArraySpec extends Specification {
@@ -12,7 +19,7 @@ class JSONArraySpec extends Specification {
 
         then:
 
-        array.length() == 3
+        array.size() == 3
         array.get(0) == "foo"
         array.get(1) == "bar"
         array.get(2) == "baz"
@@ -25,7 +32,7 @@ class JSONArraySpec extends Specification {
 
         then:
 
-        array.length() == 3
+        array.size() == 3
 
         array.toCompactString() == /["fred","barney","wilma"]/
     }
@@ -37,9 +44,9 @@ class JSONArraySpec extends Specification {
 
         then:
 
-        RuntimeException e = thrown()
+        JSONSyntaxException e = thrown()
 
-        e.message == "A JSONArray text must start with '[' at character 1 of 1, 2, 3]"
+        e.message == "A JSONArray text must start with '[' (actual: '1') at character 1 of 1, 2, 3]"
     }
 
     def "parse an empty array"() {
@@ -49,7 +56,29 @@ class JSONArraySpec extends Specification {
 
         then:
 
+        array.isEmpty()
+    }
+
+    def "isEmpty() is false if array is non-empty"() {
+        when:
+
+        def array = new JSONArray("[1]")
+
+        then:
+
+        array.isEmpty() == false
+    }
+
+    def "isEmpty() == zero length == zero size"() {
+        when:
+
+        def array = new JSONArray("[]")
+
+        then:
+
+        array.isEmpty()
         array.length() == 0
+        array.size() == 0
     }
 
     def "an empty element in the parse is a null"() {
@@ -59,7 +88,7 @@ class JSONArraySpec extends Specification {
 
         then:
 
-        array.length() == 3
+        array.size() == 3
         array.getInt(0) == 1
         array.isNull(1)
         array.getInt(2) == 3
@@ -72,7 +101,7 @@ class JSONArraySpec extends Specification {
 
         then:
 
-        array.length() == 2
+        array.size() == 2
         array.get(0) == 1
         array.get(1) == 2
     }
@@ -86,9 +115,9 @@ class JSONArraySpec extends Specification {
 
         then:
 
-        RuntimeException e = thrown()
+        JSONTypeMismatchException e = thrown()
 
-        e.message == "JSONArray[0] is not a Boolean."
+        e.message == "JSONArray[0] is not a BOOLEAN. Actual: java.lang.Integer"
     }
 
     def "handling of boolean values passed into constructor"() {
@@ -114,9 +143,9 @@ class JSONArraySpec extends Specification {
 
         then:
 
-        RuntimeException e = thrown()
+        JSONTypeMismatchException e = thrown()
 
-        e.message == "JSONArray[0] is not a number."
+        e.message == "JSONArray[0] is not a NUMBER. Actual: java.lang.Boolean"
     }
 
     def "getDouble() works with numbers and parseable strings"() {
@@ -146,9 +175,9 @@ class JSONArraySpec extends Specification {
 
         then:
 
-        RuntimeException e = thrown()
+        JSONTypeMismatchException e = thrown()
 
-        e.message == "JSONArray[1] is not a JSONArray."
+        e.message == "JSONArray[1] is not a ARRAY. Actual: java.lang.String"
     }
 
     def "get a nested array"() {
@@ -169,9 +198,9 @@ class JSONArraySpec extends Specification {
 
         then:
 
-        RuntimeException e = thrown()
+        JSONTypeMismatchException e = thrown()
 
-        e.message == "JSONArray[1] is not a JSONObject."
+        e.message == "JSONArray[1] is not a OBJECT. Actual: java.lang.String"
     }
 
     def "may not put at a negative index"() {
@@ -179,13 +208,13 @@ class JSONArraySpec extends Specification {
 
         when:
 
-        array.put(-1, "fred")
+        array.put(-2, "fred")
 
         then:
 
-        RuntimeException e = thrown()
+        JSONArrayIndexOutOfBoundsException e = thrown()
 
-        e.message == "JSONArray[-1] not found."
+        e.index == -2
     }
 
     def "put() overrides existing value in array"() {
@@ -263,7 +292,7 @@ class JSONArraySpec extends Specification {
         !i.hasNext()
     }
 
-    def "remove an element"() {
+    def "remove an element by index"() {
         def array = new JSONArray("one", "two", "three")
 
         when:
@@ -272,10 +301,122 @@ class JSONArraySpec extends Specification {
 
         then:
 
-        array.length() == 2
+        array.size() == 2
         array.toCompactString() == /["one","three"]/
     }
 
+    def "remove an element by value"() {
+        def array = new JSONArray("one", "two", "three")
+
+        when:
+
+        def result = array.remove("one")
+
+        then:
+
+        result == true
+        array.size() == 2
+        array.toCompactString() == /["two","three"]/
+    }
+
+    def "remove an element by value - not found"() {
+        def array = new JSONArray("one", "two", "three")
+
+        when:
+
+        def result = array.remove("four")
+
+        then:
+
+        result == false
+        array.size() == 3
+        array.toCompactString() == /["one","two","three"]/
+    }
+
+    def "remove all elements by collection"() {
+        def array = new JSONArray("one", "two", "three")
+
+        when:
+
+        def result = array.removeAll(["one", "three"])
+
+        then:
+
+        result == true
+        array.size() == 1
+        array.toCompactString() == /["two"]/
+    }
+
+    def "remove all elements by collection - partial"() {
+        def array = new JSONArray("one", "two", "three")
+
+        when:
+
+        def result = array.removeAll(["one", "four"])
+
+        then:
+
+        result == true
+        array.size() == 2
+        array.toCompactString() == /["two","three"]/
+    }
+
+    def "remove all elements by collection - none found"() {
+        def array = new JSONArray("one", "two", "three")
+
+        when:
+
+        def result = array.removeAll(["four", "five"])
+
+        then:
+
+        result == false
+        array.size() == 3
+        array.toCompactString() == /["one","two","three"]/
+    }
+
+    def "retain all elements by collection - all"() {
+        def array = new JSONArray("one", "two", "three")
+
+        when:
+
+        def result = array.retainAll(["one", "two", "three"])
+
+        then:
+
+        result == false
+        array.size() == 3
+        array.toCompactString() == /["one","two","three"]/
+    }
+
+    def "retain all elements by collection - partial"() {
+        def array = new JSONArray("one", "two", "three")
+
+        when:
+
+        def result = array.retainAll(["one", "three"])
+
+        then:
+
+        result == true
+        array.size() == 2
+        array.toCompactString() == /["one","three"]/
+    }
+
+    def "retain all elements by collection - none"() {
+        def array = new JSONArray("one", "two", "three")
+
+        when:
+
+        def result = array.retainAll(["four", "five"])
+
+        then:
+
+        result == true
+        array.isEmpty()
+        array.toCompactString() == /[]/
+    }
+
     def "putAll() adds new objects to existing array"() {
         def array = new JSONArray(100, 200)
 
@@ -300,6 +441,44 @@ class JSONArraySpec extends Specification {
         array.toCompactString() == /[100,200]/
     }
 
+
+    def "addAll() adds new objects to existing array"() {
+        def array = new JSONArray(100, 200)
+
+        when:
+
+        array.addAll([300, 400, 500])
+
+        then:
+
+        array.toCompactString() == /[100,200,300,400,500]/
+    }
+
+
+    def "addAll() returns true if changed"() {
+        def array = new JSONArray(100, 200)
+
+        when:
+
+        def result = array.addAll([300, 400, 500])
+
+        then:
+
+        result == true
+    }
+
+    def "addAll() returns false if not changed"() {
+        def array = new JSONArray(100, 200)
+
+        when:
+
+        def result = array.addAll((Collection)null)
+
+        then:
+
+        result == false
+    }
+
     def "list returned by toList() is unmodifiable"() {
         def array = new JSONArray(100, 200)
         def list = array.toList()
@@ -341,32 +520,101 @@ class JSONArraySpec extends Specification {
 
         list.toString(true) == "[1,2,3]"
     }
-	
-	def "put() should throw an IllegalArgumentException when receiving null"() {
-		
-		def array = new JSONArray()
-		
-		when:
-		
-		array.put(null)
-		
-		then:
-		
-		thrown IllegalArgumentException
-		
-	}
-	
-	def "new JSONArray() should throw an IllegalArgumentException when receiving null"() {
-		
-		when:
-		
-		new JSONArray(1, null, 3)
-		
-		then:
-		
-		thrown IllegalArgumentException
-		
-	}
 
+    def "put() should throw an IllegalArgumentException when receiving null"() {
+
+        def array = new JSONArray()
+
+        when:
+
+        array.put(null)
+
+        then:
+
+        thrown IllegalArgumentException
+    }
 
+    def "new JSONArray() should throw an IllegalArgumentException when receiving null"() {
+
+        when:
+
+        new JSONArray(1, null, 3)
+
+        then:
+
+        thrown IllegalArgumentException
+    }
+
+    def "only specific object types may be added - no exception"() {
+        def array = new JSONArray()
+
+        when:
+
+        array.put(value)
+
+        then:
+
+        noExceptionThrown()
+
+        where:
+        value << [
+            true,
+            3,
+            3.5,
+            "*VALUE*",
+            new JSONLiteral("*LITERAL*"),
+            new JSONObject(),
+            new JSONArray()
+        ]
+    }
+
+    def "only specific object types may be added - exception"() {
+        def array = new JSONArray()
+
+        when:
+
+        array.put(value)
+
+        then:
+
+        JSONInvalidTypeException e = thrown()
+
+        where:
+        value << [
+            new java.util.Date(),
+            [],
+            [:]
+        ]
+    }
+
+    def "array index out of bounds must throw informative exception"() {
+        def array = new JSONArray(1, 2, 3)
+
+        when:
+
+        array.get(array.size())
+
+        then:
+
+        JSONArrayIndexOutOfBoundsException e = thrown()
+        e.index == 3
+    }
+
+    def "non-finite / NaN Double not allowed in constructor"() {
+
+        when:
+
+        new JSONArray(value)
+
+        then:
+
+        RuntimeException e = thrown()
+
+        where:
+        value << [
+            Double.POSITIVE_INFINITY,
+            Double.NEGATIVE_INFINITY,
+            Double.NaN
+        ]
+    }
 }
diff --git a/tapestry-json/src/test/groovy/json/specs/JSONObjectSpec.groovy b/tapestry-json/src/test/groovy/json/specs/JSONObjectSpec.groovy
index c2ffba4..e602327 100644
--- a/tapestry-json/src/test/groovy/json/specs/JSONObjectSpec.groovy
+++ b/tapestry-json/src/test/groovy/json/specs/JSONObjectSpec.groovy
@@ -4,6 +4,11 @@ import org.apache.tapestry5.json.JSONArray
 import org.apache.tapestry5.json.JSONLiteral
 import org.apache.tapestry5.json.JSONObject
 import org.apache.tapestry5.json.JSONString
+import org.apache.tapestry5.json.exceptions.JSONInvalidTypeException
+import org.apache.tapestry5.json.exceptions.JSONSyntaxException
+import org.apache.tapestry5.json.exceptions.JSONTypeMismatchException
+import org.apache.tapestry5.json.exceptions.JSONValueNotFoundException
+
 import spock.lang.Specification
 import spock.lang.Unroll
 
@@ -82,9 +87,33 @@ class JSONObjectSpec extends Specification {
 
         then:
 
-        RuntimeException e = thrown()
+        JSONValueNotFoundException e = thrown()
+
+        e.message == /JSONObject["barney"] is not found. Required: ANY/
+    }
+
+    def "getOrDefault returns defaultValue if not found"() {
+        def master = new JSONObject("fred", "flintstone")
+
+        when:
+
+        def result = master.getOrDefault "barney", "gumble"
+
+        then:
 
-        e.message == /JSONObject["barney"] not found./
+        result == "gumble"
+    }
+
+    def "getOrDefault returns value if found"() {
+        def master = new JSONObject("fred", "flintstone")
+
+        when:
+
+        def result = master.getOrDefault "fred", "other"
+
+        then:
+
+        result == "flintstone"
     }
 
     @Unroll
@@ -148,9 +177,9 @@ class JSONObjectSpec extends Specification {
 
         then:
 
-        RuntimeException e = thrown()
+        JSONTypeMismatchException e = thrown()
 
-        e.message == /JSONObject["akey"] is not a Boolean./
+        e.message == /JSONObject["akey"] is not a BOOLEAN. Actual: java.lang.Integer/
     }
 
     def "accumulate simple values"() {
@@ -238,7 +267,7 @@ class JSONObjectSpec extends Specification {
         object.toCompactString() == /{"friends":["barney","zaphod"]}/
     }
 
-    def "appending to a key whose value is not a JSONArray is an exception"() {
+    def "appending to a key whose value is not aArray is an exception"() {
         def object = new JSONObject(/{friends: 0 }/)
 
         when:
@@ -247,9 +276,9 @@ class JSONObjectSpec extends Specification {
 
         then:
 
-        RuntimeException e = thrown()
+        JSONTypeMismatchException e = thrown()
 
-        e.message == /JSONObject["friends"] is not a JSONArray./
+        e.message == /JSONObject["friends"] is not a ARRAY. Actual: java.lang.Integer/
     }
 
     def "getDouble() with a non-numeric value is an exception"() {
@@ -261,9 +290,9 @@ class JSONObjectSpec extends Specification {
 
         then:
 
-        RuntimeException e = thrown()
+        JSONTypeMismatchException e = thrown()
 
-        e.message == /JSONObject["notdouble"] is not a number./
+        e.message == /JSONObject["notdouble"] is not a NUMBER. Actual: java.lang.Boolean/
     }
 
     def "getDouble() with a string that can not be parsed as a number is an exception"() {
@@ -275,10 +304,9 @@ class JSONObjectSpec extends Specification {
 
         then:
 
-        RuntimeException e = thrown()
-
-        e.message == /JSONObject["notdouble"] is not a number./
+        JSONTypeMismatchException e = thrown()
 
+        e.message == /JSONObject["notdouble"] is not a NUMBER. Actual: java.lang.String/
     }
 
     def "has() will identify which keys have values and which do not"() {
@@ -290,7 +318,7 @@ class JSONObjectSpec extends Specification {
         !object.has("barney")
     }
 
-    def "getJSONArray() for a value that is not a JSONArray is an exception"() {
+    def "getJSONArray() for a value that is not aArray is an exception"() {
         def object = new JSONObject(/{notarray: 22.7}/)
 
         when:
@@ -301,7 +329,7 @@ class JSONObjectSpec extends Specification {
 
         RuntimeException e = thrown()
 
-        e.message == /JSONObject["notarray"] is not a JSONArray./
+        e.message == /JSONObject["notarray"] is not a ARRAY. Actual: java.lang.Double/
     }
 
     def "length() of a JSONObject is the number of keys"() {
@@ -400,21 +428,21 @@ class JSONObjectSpec extends Specification {
 
         then:
 
-        RuntimeException e = thrown()
+        JSONSyntaxException e = thrown()
 
         e.message.trim().startsWith expected
 
         where:
 
-        input                     | expected                                                   | desc
-        "{  "                     | "A JSONObject text must end with '}' at character 3"       | "unmatched open brace"
-        "fred"                    | "A JSONObject text must begin with '{' at character 1 of " | "missing open brace"
-        /{ "akey" }/              | /Expected a ':' after a key at character 10 of/            | "missing value after key"
-        /{ "fred" : 1 "barney" }/ | /Expected a ',' or '}' at character 14 of/                 | "missing property separator"
-        /{ "list" : [1, 2/        | /Expected a ',' or ']' at character 16 of/                 | "missing seperator or closing bracket"
-        '''/* unclosed'''         | /Unclosed comment at character 11 of/                      | "unclosed C-style comment"
-        /{ "fred \n}/             | /Unterminated string at character 11 of/                   | "unterminated string at line end"
-        /{ fred: ,}/              | /Missing value at character 8 of /                         | "missing value after key"
+        input                     | expected                                                                 | desc
+        "{  "                     | "A JSONObject text must end with '}' at character 3"                     | "unmatched open brace"
+        "fred"                    | /A JSONObject text must start with '{' (actual: 'f') at character 1 of/  | "missing open brace"
+        /{ "akey" }/              | /Expected a ':' after a key at character 10 of/                          | "missing value after key"
+        /{ "fred" : 1 "barney" }/ | /Expected a ',' or '}' at character 14 of/                               | "missing property separator"
+        /{ "list" : [1, 2/        | /Expected a ',' or ']' at character 16 of/                               | "missing seperator or closing bracket"
+        '''/* unclosed'''         | /Unclosed comment at character 11 of/                                    | "unclosed C-style comment"
+        /{ "fred \n}/             | /Unterminated string at character 11 of/                                 | "unterminated string at line end"
+        /{ fred: ,}/              | /Missing value at character 8 of /                                       | "missing value after key"
     }
 
     def "can use ':' or '=>' as key seperator, and ';' as property separator"() {
@@ -469,9 +497,14 @@ class JSONObjectSpec extends Specification {
 
         where:
 
-        value << [Double.NaN, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, Float.NaN,
-            Float.NEGATIVE_INFINITY, Float.POSITIVE_INFINITY]
-
+        value << [
+            Double.NaN,
+            Double.NEGATIVE_INFINITY,
+            Double.POSITIVE_INFINITY,
+            Float.NaN,
+            Float.NEGATIVE_INFINITY,
+            Float.POSITIVE_INFINITY
+        ]
     }
 
     def "parse an empty object is empty"() {
@@ -496,16 +529,47 @@ class JSONObjectSpec extends Specification {
         object.toCompactString() == /{"barney":"rubble"}/
     }
 
-    def "only specific object types may be added"() {
+    def "only specific object types may be added - no exception"() {
+        def object = new JSONObject(/{}/)
+
         when:
 
-        JSONObject.testValidity([:])
+        object.put("key", value)
 
         then:
 
-        RuntimeException e = thrown()
+        noExceptionThrown()
+
+        where:
+        value << [
+            null,
+            true,
+            3,
+            3.5,
+            "*VALUE*",
+            new JSONLiteral("*LITERAL*"),
+            new JSONObject(),
+            new JSONArray()
+        ]
+    }
+
+    def "only specific object types may be added - exception"() {
+        def object = new JSONObject(/{}/)
 
-        e.message == '''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 java.util.LinkedHashMap is not allowed.'''
+        when:
+
+        object.put("key", value)
+
+        then:
+
+        JSONInvalidTypeException e = thrown()
+
+        where:
+        value << [
+            new java.util.Date(),
+            [],
+            [:]
+        ]
     }
 
     def "JSONString can output anything it wants"() {
@@ -651,7 +715,6 @@ class JSONObjectSpec extends Specification {
         then:
 
         object.get("foo") == "bar"
-
     }
 
     def "pretty-print an empty JSONObject"() {
@@ -741,7 +804,6 @@ class JSONObjectSpec extends Specification {
         json == /{
   "kermit" : "frog"
 }/
-
     }
 
     def "getString() at index"() {
@@ -770,8 +832,8 @@ class JSONObjectSpec extends Specification {
 
     def "can access contents of object as a map"() {
         def object = new JSONObject("foo", "bar")
-            .put("null", JSONObject.NULL)
-            .put("number", 6)
+                .put("null", JSONObject.NULL)
+                .put("number", 6)
 
         when:
 
@@ -808,7 +870,6 @@ class JSONObjectSpec extends Specification {
 
         then:
 
-        result.is object
         object.toCompactString() == /{"fred":"flintstone","barney":"rubble","wilma":"flintstone"}/
     }
 
@@ -884,7 +945,5 @@ class JSONObjectSpec extends Specification {
         then:
 
         source == copy
-
     }
-
 }
diff --git a/tapestry-json/src/test/groovy/json/specs/JSONSpec.groovy b/tapestry-json/src/test/groovy/json/specs/JSONSpec.groovy
new file mode 100644
index 0000000..5b8ae3f
--- /dev/null
+++ b/tapestry-json/src/test/groovy/json/specs/JSONSpec.groovy
@@ -0,0 +1,36 @@
+package json.specs
+
+import org.apache.tapestry5.json.JSONArray
+import org.apache.tapestry5.json.JSONLiteral
+import org.apache.tapestry5.json.JSONObject
+import org.apache.tapestry5.json.JSONString
+import org.apache.tapestry5.json.exceptions.JSONInvalidTypeException
+import org.apache.tapestry5.json.JSON
+import spock.lang.Specification
+import spock.lang.Unroll
+
+class JSONSpec extends Specification {
+
+    def "invalid types throw JSONInvalidTypeException"() {
+        when:
+
+        JSON.testValidity([:])
+
+        then:
+
+        JSONInvalidTypeException e = thrown()
+
+        e.message == '''JSONArray values / 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 java.util.LinkedHashMap is not allowed.'''
+    }
+
+   def "null is invalid"() {
+        when:
+
+        JSON.testValidity(null)
+
+        then:
+
+        IllegalArgumentException e = thrown()
+    }
+
+}


[tapestry-5] 02/02: TAP5-2640: fixing JavaDoc errors.

Posted by th...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

thiagohp pushed a commit to branch 5.6.x
in repository https://gitbox.apache.org/repos/asf/tapestry-5.git

commit 917e4719aed2c2f853fefc68207aa7249d87365f
Author: Thiago H. de Paula Figueiredo <th...@arsmachina.com.br>
AuthorDate: Sat Oct 3 15:05:21 2020 -0300

    TAP5-2640: fixing JavaDoc errors.
---
 .../java/org/apache/tapestry5/json/JSONArray.java  |  8 +++--
 .../java/org/apache/tapestry5/json/JSONObject.java | 34 +++++++++++-----------
 2 files changed, 22 insertions(+), 20 deletions(-)

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 f4fcc2f..ee9e755 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
@@ -48,6 +48,8 @@ import org.apache.tapestry5.json.exceptions.JSONValueNotFoundException;
  * Instances of this class are not thread safe.
  */
 public final class JSONArray extends JSONCollection implements Collection<Object> {
+    
+    private static final long serialVersionUID = 1L;
 
     private final List<Object> values;
 
@@ -164,7 +166,7 @@ public final class JSONArray extends JSONCollection implements Collection<Object
      *              infinities}. Unsupported values are not permitted and will cause the
      *              array to be in an inconsistent state.
      * @return this array.
-     * @deprecated The use of {@link #add(Object)] is encouraged.
+     * @deprecated The use of {@link #add(Object)} is encouraged.
      */
     public JSONArray put(Object value) {
         add(value);
@@ -333,9 +335,9 @@ public final class JSONArray extends JSONCollection implements Collection<Object
      * @since 5.7
      */
     @Override
-    public boolean retainAll(Collection<?> c)
+    public boolean retainAll(Collection<?> collection)
     {
-        return values.retainAll(c);
+        return values.retainAll(collection);
     }
 
     /**
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 caf3285..e6f9f47 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
@@ -77,6 +77,8 @@ import org.apache.tapestry5.json.exceptions.JSONValueNotFoundException;
  */
 public final class JSONObject extends JSONCollection implements Map<String, Object> {
 
+    private static final long serialVersionUID = 1L;
+
     private static final Double NEGATIVE_ZERO = -0d;
 
     /**
@@ -86,7 +88,7 @@ public final class JSONObject extends JSONCollection implements Map<String, Obje
      * <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>do not throw on {@link #get}
      * <li>are included in the encoded JSON string.
      * </ul>
      *
@@ -98,7 +100,6 @@ public final class JSONObject extends JSONCollection implements Map<String, Obje
 
       private static final long serialVersionUID = 1L;
 
-        @SuppressWarnings("EqualsWhichDoesntCheckParameterClass")
         @Override
         public boolean equals(Object o) {
             return o == this || o == null; // API specifies this broken equals implementation
@@ -403,7 +404,7 @@ public final class JSONObject extends JSONCollection implements Map<String, Obje
      * Returns the value to which the specified key is mapped and a boolean, or
      * {@code defaultValue} if this JSONObject contains no mapping for the key.
      *
-     * @param key the key whose associated value is to be returned
+     * @param name the key whose associated value is to be returned
      * @param defaultValue the default mapping of the key
      * @return the value to which the specified key is mapped, or
      * {@code defaultValue} if this JSONObject contains no mapping for the key
@@ -474,7 +475,7 @@ public final class JSONObject extends JSONCollection implements Map<String, Obje
      * Returns the value to which the specified key is mapped and an int, or
      * {@code defaultValue} if this JSONObject contains no mapping for the key.
      *
-     * @param key the key whose associated value is to be returned
+     * @param name the key whose associated value is to be returned
      * @param defaultValue the default mapping of the key
      * @return the value to which the specified key is mapped, or
      * {@code defaultValue} if this JSONObject contains no mapping for the key
@@ -527,7 +528,7 @@ public final class JSONObject extends JSONCollection implements Map<String, Obje
      * Returns the value to which the specified key is mapped and a long, or
      * {@code defaultValue} if this JSONObject contains no mapping for the key.
      *
-     * @param key the key whose associated value is to be returned
+     * @param name the key whose associated value is to be returned
      * @param defaultValue the default mapping of the key
      * @return the value to which the specified key is mapped, or
      * {@code defaultValue} if this JSONObject contains no mapping for the key
@@ -576,7 +577,7 @@ public final class JSONObject extends JSONCollection implements Map<String, Obje
      * Returns the value to which the specified key is mapped and a string, or
      * {@code defaultValue} if this JSONObject contains no mapping for the key.
      *
-     * @param key the key whose associated value is to be returned
+     * @param name the key whose associated value is to be returned
      * @param defaultValue the default mapping of the key
      * @return the value to which the specified key is mapped, or
      * {@code defaultValue} if this JSONObject contains no mapping for the key
@@ -625,7 +626,7 @@ public final class JSONObject extends JSONCollection implements Map<String, Obje
      * Returns the value to which the specified key is mapped and a JSONArray, or
      * {@code defaultValue} if this JSONObject contains no mapping for the key.
      *
-     * @param key the key whose associated value is to be returned
+     * @param name the key whose associated value is to be returned
      * @param defaultValue the default mapping of the key
      * @return the value to which the specified key is mapped, or
      * {@code defaultValue} if this JSONObject contains no mapping for the key
@@ -674,7 +675,7 @@ public final class JSONObject extends JSONCollection implements Map<String, Obje
      * Returns the value to which the specified key is mapped and a JSONObject, or
      * {@code defaultValue} if this map contains no mapping for the key.
      *
-     * @param key the key whose associated value is to be returned
+     * @param name the key whose associated value is to be returned
      * @param defaultValue the default mapping of the key
      * @return the value to which the specified key is mapped, or
      * {@code defaultValue} if this map contains no mapping for the key
@@ -996,11 +997,11 @@ public final class JSONObject extends JSONCollection implements Map<String, Obje
      * @return The value.
      * @throws JSONValueNotFoundException if no such mapping exists.
      */ @Override
-    public Object get(Object key)
+    public Object get(Object name)
     {
-        Object result = nameValuePairs.get(key);
+        Object result = nameValuePairs.get(name);
          if (result == null) {
-            throw JSONExceptionBuilder.valueNotFound(false, key, JSONType.ANY);
+            throw JSONExceptionBuilder.valueNotFound(false, name, JSONType.ANY);
          }
          return result;
     }
@@ -1030,9 +1031,9 @@ public final class JSONObject extends JSONCollection implements Map<String, Obje
      *         no such mapping.
      */
     @Override
-    public Object remove(Object key)
+    public Object remove(Object name)
     {
-        return nameValuePairs.remove(key);
+        return nameValuePairs.remove(name);
     }
 
     /**
@@ -1040,15 +1041,14 @@ public final class JSONObject extends JSONCollection implements Map<String, Obje
      *
      * @param newProperties
      *            to add to this JSONObject
-     * @return this JSONObject
      * @since 5.7
      */
     @Override
-    public void putAll(Map<? extends String, ? extends Object> m)
+    public void putAll(Map<? extends String, ? extends Object> newProperties)
     {
-        assert m != null;
+        assert newProperties != null;
 
-        for (Map.Entry<? extends String, ? extends Object> e : m.entrySet())
+        for (Map.Entry<? extends String, ? extends Object> e : newProperties.entrySet())
         {
             put(e.getKey(), e.getValue());
         }