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
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 @@
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 @@
*
* 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 @@
*
* @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 @@
if (object instanceof JSONArray) {
values = ((JSONArray) object).values;
} else {
- throw JSON.typeMismatch(object, "JSONArray");
+ throw JSONExceptionBuilder.tokenerTypeMismatch(object, JSONType.ARRAY);
}
}
@@ -78,8 +85,9 @@
* 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 @@
* 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 @@
/**
* @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 @@
* 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 @@
* 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 @@
*
* @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 @@
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 @@
}
/**
+ * 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 @@
*
* @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 @@
*
* @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 @@
*
* @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 @@
*
* @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 @@
*
* @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 @@
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 @@
*
* @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 @@
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 @@
}
/**
+ * 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 @@
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 @@
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 @@
*
* <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 @@
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 @@
/**
* 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 @@
/**
* 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 @@
* {@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 @@
* @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 @@
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 @@
}
/**
- * 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 @@
* 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 @@
* @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 @@
*
* @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 @@
*
* @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 @@
*
* @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 @@
*
* @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 @@
*
* @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 @@
}
/**
- * 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 @@
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;
- }
-
- 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.");
+ 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())
+ {
+ put(e.getKey(), e.getValue());
+ }
+ }
+
+ /**
+ * 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 @@
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 @@
*
* @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 @@
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 @@
* @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 @@
then:
- array.length() == 3
+ array.size() == 3
array.get(0) == "foo"
array.get(1) == "bar"
array.get(2) == "baz"
@@ -25,7 +32,7 @@
then:
- array.length() == 3
+ array.size() == 3
array.toCompactString() == /["fred","barney","wilma"]/
}
@@ -37,9 +44,9 @@
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 @@
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 @@
then:
- array.length() == 3
+ array.size() == 3
array.getInt(0) == 1
array.isNull(1)
array.getInt(2) == 3
@@ -72,7 +101,7 @@
then:
- array.length() == 2
+ array.size() == 2
array.get(0) == 1
array.get(1) == 2
}
@@ -86,9 +115,9 @@
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 @@
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 @@
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 @@
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 @@
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 @@
!i.hasNext()
}
- def "remove an element"() {
+ def "remove an element by index"() {
def array = new JSONArray("one", "two", "three")
when:
@@ -272,10 +301,122 @@
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 @@
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 @@
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.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 @@
then:
- RuntimeException e = thrown()
+ JSONValueNotFoundException e = thrown()
- e.message == /JSONObject["barney"] not found./
+ 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:
+
+ 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 @@
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 @@
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 @@
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 @@
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 @@
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.String/
}
def "has() will identify which keys have values and which do not"() {
@@ -290,7 +318,7 @@
!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 @@
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 @@
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 @@
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 @@
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()
- 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.'''
+ 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(/{}/)
+
+ 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 @@
then:
object.get("foo") == "bar"
-
}
def "pretty-print an empty JSONObject"() {
@@ -741,7 +804,6 @@
json == /{
"kermit" : "frog"
}/
-
}
def "getString() at index"() {
@@ -770,8 +832,8 @@
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 @@
then:
- result.is object
object.toCompactString() == /{"fred":"flintstone","barney":"rubble","wilma":"flintstone"}/
}
@@ -884,7 +945,5 @@
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()
+ }
+
+}