diff options
author | Jesse Wilson <jessewilson@google.com> | 2010-03-12 00:52:29 -0800 |
---|---|---|
committer | Jesse Wilson <jessewilson@google.com> | 2010-03-12 17:36:17 -0800 |
commit | 51a095f0bc7aadfcc7e6b3873b97c050c523d102 (patch) | |
tree | 577c03ef830620a596a377e2ce035d553b8ebc63 /json/src | |
parent | c6dc33d084214a07fb5599832f3bfb266d06fbf9 (diff) | |
download | libcore-51a095f0bc7aadfcc7e6b3873b97c050c523d102.zip libcore-51a095f0bc7aadfcc7e6b3873b97c050c523d102.tar.gz libcore-51a095f0bc7aadfcc7e6b3873b97c050c523d102.tar.bz2 |
A cleanroom implementation of the org.json API.
This implementation lacks documentation. I intend to write that after checking
it into the master branch. By not waiting we'll have more time to exercise the
code, if only in Google's own applications.
This passes all of my tests. I rewrote some of the tests to make Crockford's
implementation fail. The tests that fail on Crockford's implementation are:
JSONArrayTest
testEqualsAndHashCode equals() not consistent with hashCode()
testTokenerConstructorParseFail StackOverflowError
testStringConstructorParseFail StackOverflowError
JSONObjectTest
testOtherNumbers Object.put() accepted a NaN (via a custom Number class)
testMapConstructorWithBogusEntries JSONObject constructor doesn't validate its input!
JSONTokenerTest
testNextNWithAllRemaining off-by-one error?
testNext0 Returning an empty string should be valid
testNextCleanCommentsTrailingSingleSlash nextClean doesn't consume a trailing slash
assertNotClean The character line tabulation is not whitespace according to the JSON spec.
testNextToDoesntStopOnNull nextTo() shouldn't stop after \0 characters
testNextToConsumesNull nextTo shouldn't consume \0.
testSkipToStopsOnNull skipTo shouldn't stop when it sees '\0'
ParsingTest
testParsingLargeHexValues For input "0x80000000" Hex values are parsed as Strings if their signed value is greater than Integer.MAX_VALUE.
testSyntaxProblemUnterminatedArray Stack overflowed on input "["
Change-Id: I44c4a4a698a66bf043ed339d6bd804951e732cbf
Diffstat (limited to 'json/src')
-rw-r--r-- | json/src/rewrite/java/org/json/JSON.java | 112 | ||||
-rw-r--r-- | json/src/rewrite/java/org/json/JSONArray.java | 322 | ||||
-rw-r--r-- | json/src/rewrite/java/org/json/JSONException.java | 49 | ||||
-rw-r--r-- | json/src/rewrite/java/org/json/JSONObject.java | 395 | ||||
-rw-r--r-- | json/src/rewrite/java/org/json/JSONStringer.java | 340 | ||||
-rw-r--r-- | json/src/rewrite/java/org/json/JSONTokener.java | 480 | ||||
-rw-r--r-- | json/src/test/java/org/json/AllTests.java | 1 | ||||
-rw-r--r-- | json/src/test/java/org/json/JSONArrayTest.java | 120 | ||||
-rw-r--r-- | json/src/test/java/org/json/JSONObjectTest.java | 145 | ||||
-rw-r--r-- | json/src/test/java/org/json/JSONStringerTest.java | 60 | ||||
-rw-r--r-- | json/src/test/java/org/json/JSONTokenerTest.java | 204 | ||||
-rw-r--r-- | json/src/test/java/org/json/ParsingTest.java | 260 | ||||
-rw-r--r-- | json/src/test/java/org/json/SelfUseTest.java | 35 |
13 files changed, 2362 insertions, 161 deletions
diff --git a/json/src/rewrite/java/org/json/JSON.java b/json/src/rewrite/java/org/json/JSON.java new file mode 100644 index 0000000..029884b --- /dev/null +++ b/json/src/rewrite/java/org/json/JSON.java @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.json; + +class JSON { + /** + * Returns the input if it is a JSON-permissable value; throws otherwise. + */ + static double checkDouble(double d) throws JSONException { + if (Double.isInfinite(d) || Double.isNaN(d)) { + throw new JSONException("Forbidden numeric value: " + d); + } + return d; + } + + static Boolean toBoolean(Object value) { + if (value instanceof Boolean) { + return (Boolean) value; + } else if (value instanceof String) { + return Boolean.valueOf(((String) value)); + } else { + return null; + } + } + + static Double toDouble(Object value) { + if (value instanceof Double) { + return (Double) value; + } else if (value instanceof Number) { + return ((Number) value).doubleValue(); + } else if (value instanceof String) { + try { + return Double.valueOf((String) value); + } catch (NumberFormatException e) { + } + } + return null; + } + + static Integer toInteger(Object value) { + if (value instanceof Integer) { + return (Integer) value; + } else if (value instanceof Number) { + return ((Number) value).intValue(); + } else if (value instanceof String) { + try { + return Double.valueOf((String) value).intValue(); + } catch (NumberFormatException e) { + } + } + return null; + } + + static Long toLong(Object value) { + if (value instanceof Long) { + return (Long) value; + } else if (value instanceof Number) { + return ((Number) value).longValue(); + } else if (value instanceof String) { + try { + return Double.valueOf((String) value).longValue(); + } catch (NumberFormatException e) { + } + } + return null; + } + + static String toString(Object value) { + if (value instanceof String) { + return (String) value; + } else if (value != null) { + return String.valueOf(value); + } + return null; + } + + public static JSONException typeMismatch(Object indexOrName, Object actual, + String requiredType) throws JSONException { + if (actual == null) { + throw new JSONException("Value at " + indexOrName + " is null."); + } else { + throw new JSONException("Value " + actual + " at " + indexOrName + + " of type " + actual.getClass().getName() + + " cannot be converted to " + requiredType); + } + } + + public static JSONException typeMismatch(Object actual, String requiredType) + throws JSONException { + if (actual == null) { + throw new JSONException("Value is null."); + } else { + throw new JSONException("Value " + actual + + " of type " + actual.getClass().getName() + + " cannot be converted to " + requiredType); + } + } +} diff --git a/json/src/rewrite/java/org/json/JSONArray.java b/json/src/rewrite/java/org/json/JSONArray.java new file mode 100644 index 0000000..fa42054 --- /dev/null +++ b/json/src/rewrite/java/org/json/JSONArray.java @@ -0,0 +1,322 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.json; + +import java.util.List; +import java.util.ArrayList; +import java.util.Collection; + +// Note: this class was written without inspecting the non-free org.json sourcecode. + +/** + * An indexed sequence of JSON-safe values. + */ +public class JSONArray { + + private final List<Object> values; + + public JSONArray() { + values = new ArrayList<Object>(); + } + + /* Accept a raw type for API compatibility */ + public JSONArray(Collection copyFrom) { + this(); + Collection<?> copyFromTyped = (Collection<?>) copyFrom; + values.addAll(copyFromTyped); + } + + public JSONArray(JSONTokener readFrom) throws JSONException { + /* + * Getting the parser to populate this could get tricky. Instead, just + * parse to temporary JSONArray and then steal the data from that. + */ + Object object = readFrom.nextValue(); + if (object instanceof JSONArray) { + values = ((JSONArray) object).values; + } else { + throw JSON.typeMismatch(object, "JSONArray"); + } + } + + public JSONArray(String json) throws JSONException { + this(new JSONTokener(json)); + } + + public int length() { + return values.size(); + } + + public JSONArray put(boolean value) { + values.add(value); + return this; + } + + public JSONArray put(double value) throws JSONException { + values.add(JSON.checkDouble(value)); + return this; + } + + public JSONArray put(int value) { + values.add(value); + return this; + } + + public JSONArray put(long value) { + values.add(value); + return this; + } + + public JSONArray put(Object value) { + values.add(value); + return this; + } + + public JSONArray put(int index, boolean value) throws JSONException { + return put(index, (Boolean) value); + } + + public JSONArray put(int index, double value) throws JSONException { + return put(index, (Double) value); + } + + public JSONArray put(int index, int value) throws JSONException { + return put(index, (Integer) value); + } + + public JSONArray put(int index, long value) throws JSONException { + return put(index, (Long) value); + } + + public JSONArray put(int index, Object value) throws JSONException { + if (value instanceof Number) { + // deviate from the original by checking all Numbers, not just floats & doubles + JSON.checkDouble(((Number) value).doubleValue()); + } + while (values.size() <= index) { + values.add(null); + } + values.set(index, value); + return this; + } + + public boolean isNull(int index) { + Object value = opt(index); + return value == null || value == JSONObject.NULL; + } + + public Object get(int index) throws JSONException { + try { + Object value = values.get(index); + if (value == null) { + throw new JSONException("Value at " + index + " is null."); + } + return value; + } catch (IndexOutOfBoundsException e) { + throw new JSONException("Index " + index + " out of range [0.." + values.size() + ")"); + } + } + + public Object opt(int index) { + if (index < 0 || index >= values.size()) { + return null; + } + return values.get(index); + } + + public boolean getBoolean(int index) throws JSONException { + Object object = get(index); + Boolean result = JSON.toBoolean(object); + if (result == null) { + throw JSON.typeMismatch(index, object, "boolean"); + } + return result; + } + + public boolean optBoolean(int index) { + return optBoolean(index, false); + } + + public boolean optBoolean(int index, boolean fallback) { + Object object = opt(index); + Boolean result = JSON.toBoolean(object); + return result != null ? result : fallback; + } + + public double getDouble(int index) throws JSONException { + Object object = get(index); + Double result = JSON.toDouble(object); + if (result == null) { + throw JSON.typeMismatch(index, object, "double"); + } + return result; + } + + public double optDouble(int index) { + return optDouble(index, Double.NaN); + } + + public double optDouble(int index, double fallback) { + Object object = opt(index); + Double result = JSON.toDouble(object); + return result != null ? result : fallback; + } + + public int getInt(int index) throws JSONException { + Object object = get(index); + Integer result = JSON.toInteger(object); + if (result == null) { + throw JSON.typeMismatch(index, object, "int"); + } + return result; + } + + public int optInt(int index) { + return optInt(index, 0); + } + + public int optInt(int index, int fallback) { + Object object = opt(index); + Integer result = JSON.toInteger(object); + return result != null ? result : fallback; + } + + public long getLong(int index) throws JSONException { + Object object = get(index); + Long result = JSON.toLong(object); + if (result == null) { + throw JSON.typeMismatch(index, object, "long"); + } + return result; + } + + public long optLong(int index) { + return optLong(index, 0L); + } + + public long optLong(int index, long fallback) { + Object object = opt(index); + Long result = JSON.toLong(object); + return result != null ? result : fallback; + } + + public String getString(int index) throws JSONException { + Object object = get(index); + String result = JSON.toString(object); + if (result == null) { + throw JSON.typeMismatch(index, object, "String"); + } + return result; + } + + public String optString(int index) { + return optString(index, ""); + } + + public String optString(int index, String fallback) { + Object object = opt(index); + String result = JSON.toString(object); + return result != null ? result : fallback; + } + + public JSONArray getJSONArray(int index) throws JSONException { + Object object = get(index); + if (object instanceof JSONArray) { + return (JSONArray) object; + } else { + throw JSON.typeMismatch(index, object, "JSONArray"); + } + } + + public JSONArray optJSONArray(int index) { + Object object = opt(index); + return object instanceof JSONArray ? (JSONArray) object : null; + } + + public JSONObject getJSONObject(int index) throws JSONException { + Object object = get(index); + if (object instanceof JSONObject) { + return (JSONObject) object; + } else { + throw JSON.typeMismatch(index, object, "JSONObject"); + } + } + + public JSONObject optJSONObject(int index) { + Object object = opt(index); + return object instanceof JSONObject ? (JSONObject) object : null; + } + + public JSONObject toJSONObject(JSONArray names) throws JSONException { + JSONObject result = new JSONObject(); + int length = Math.min(names.length(), values.size()); + if (length == 0) { + return null; + } + for (int i = 0; i < length; i++) { + String name = JSON.toString(names.opt(i)); + result.put(name, opt(i)); + } + return result; + } + + public String join(String separator) throws JSONException { + JSONStringer stringer = new JSONStringer(); + stringer.open(JSONStringer.Scope.NULL, ""); + for (int i = 0, size = values.size(); i < size; i++) { + if (i > 0) { + stringer.out.append(separator); + } + stringer.value(values.get(i)); + } + stringer.close(JSONStringer.Scope.NULL, JSONStringer.Scope.NULL, ""); + return stringer.out.toString(); + } + + @Override public String toString() { + try { + JSONStringer stringer = new JSONStringer(); + writeTo(stringer); + return stringer.toString(); + } catch (JSONException e) { + return null; + } + } + + public String toString(int indentSpaces) throws JSONException { + JSONStringer stringer = new JSONStringer(indentSpaces); + writeTo(stringer); + return stringer.toString(); + } + + void writeTo(JSONStringer stringer) throws JSONException { + stringer.array(); + for (Object value : values) { + stringer.value(value); + } + stringer.endArray(); + } + + @Override public boolean equals(Object o) { + return o instanceof JSONArray && ((JSONArray) o).values.equals(values); + } + + @Override public int hashCode() { + // diverge from the original, which doesn't implement hashCode + return values.hashCode(); + } +} diff --git a/json/src/rewrite/java/org/json/JSONException.java b/json/src/rewrite/java/org/json/JSONException.java new file mode 100644 index 0000000..e1efd9f --- /dev/null +++ b/json/src/rewrite/java/org/json/JSONException.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.json; + +// Note: this class was written without inspecting the non-free org.json sourcecode. + +/** + * Thrown to indicate a problem with the JSON API. Such problems include: + * <ul> + * <li>Attempts to parse or construct malformed documents + * <li>Use of null as a name + * <li>Use of numeric types not available to JSON, such as {@link Double#NaN + * NaN} or {@link Double#POSITIVE_INFINITY infinity}. + * <li>Lookups using an out of range index or nonexistant name + * <li>Type mismatches on lookups + * </ul> + * + * <p>Although this is a checked exception, it is rarely recoverable. Most + * callers should simply wrap this exception in an unchecked exception and + * rethrow: + * <pre> public JSONArray toJSONObject() { + * try { + * JSONObject result = new JSONObject(); + * ... + * } catch (JSONException e) { + * throw new RuntimeException(e); + * } + * }</pre> + */ +public class JSONException extends Exception { + + public JSONException(String s) { + super(s); + } +} diff --git a/json/src/rewrite/java/org/json/JSONObject.java b/json/src/rewrite/java/org/json/JSONObject.java new file mode 100644 index 0000000..c92d549 --- /dev/null +++ b/json/src/rewrite/java/org/json/JSONObject.java @@ -0,0 +1,395 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.json; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +// Note: this class was written without inspecting the non-free org.json sourcecode. + +/** + * + * + * <p>TODO: Note about self-use + */ +public class JSONObject { + + private static final Double NEGATIVE_ZERO = -0d; + + public static final Object NULL = new Object() { + @Override public boolean equals(Object o) { + return o == this || o == null; // API specifies this broken equals implementation + } + @Override public String toString() { + return "null"; + } + }; + + private final Map<String, Object> nameValuePairs; + + public JSONObject() { + nameValuePairs = new HashMap<String, Object>(); + } + + /* Accept a raw type for API compatibility */ + public JSONObject(Map copyFrom) { + this(); + Map<?, ?> contentsTyped = (Map<?, ?>) copyFrom; + for (Map.Entry<?, ?> entry : contentsTyped.entrySet()) { + /* + * Deviate from the original by checking that keys are non-null and + * of the proper type. (We still defer validating the values). + */ + String key = (String) entry.getKey(); + if (key == null) { + throw new NullPointerException(); + } + nameValuePairs.put(key, entry.getValue()); + } + } + + public JSONObject(JSONTokener readFrom) throws JSONException { + /* + * Getting the parser to populate this could get tricky. Instead, just + * parse to temporary JSONObject and then steal the data from that. + */ + Object object = readFrom.nextValue(); + if (object instanceof JSONObject) { + this.nameValuePairs = ((JSONObject) object).nameValuePairs; + } else { + throw JSON.typeMismatch(object, "JSONObject"); + } + } + + public JSONObject(String json) throws JSONException { + this(new JSONTokener(json)); + } + + public JSONObject(JSONObject copyFrom, String[] names) throws JSONException { + this(); + for (String name : names) { + Object value = copyFrom.opt(name); + if (value != null) { + nameValuePairs.put(name, value); + } + } + } + + public int length() { + return nameValuePairs.size(); + } + + public JSONObject put(String name, boolean value) throws JSONException { + nameValuePairs.put(checkName(name), value); + return this; + } + + public JSONObject put(String name, double value) throws JSONException { + nameValuePairs.put(checkName(name), JSON.checkDouble(value)); + return this; + } + + public JSONObject put(String name, int value) throws JSONException { + nameValuePairs.put(checkName(name), value); + return this; + } + + public JSONObject put(String name, long value) throws JSONException { + nameValuePairs.put(checkName(name), value); + return this; + } + + public JSONObject put(String name, Object value) throws JSONException { + if (value == null) { + nameValuePairs.remove(name); + return this; + } + if (value instanceof Number) { + // deviate from the original by checking all Numbers, not just floats & doubles + JSON.checkDouble(((Number) value).doubleValue()); + } + nameValuePairs.put(checkName(name), value); + return this; + } + + public JSONObject putOpt(String name, Object value) throws JSONException { + if (name == null || value == null) { + return this; + } + return put(name, value); + } + + public JSONObject accumulate(String name, Object value) throws JSONException { + Object current = nameValuePairs.get(checkName(name)); + if (current == null) { + put(name, value); + } else if (current instanceof JSONArray) { + JSONArray array = (JSONArray) current; + array.put(value); + } else { + JSONArray array = new JSONArray(); + array.put(current); + array.put(value); // fails on bogus values + nameValuePairs.put(name, array); + } + return this; + } + + String checkName(String name) throws JSONException { + if (name == null) { + throw new JSONException("Names must be non-null"); + } + return name; + } + + public Object remove(String name) { + return nameValuePairs.remove(name); + } + + public boolean isNull(String name) { + Object value = nameValuePairs.get(name); + return value == null || value == NULL; + } + + public boolean has(String name) { + return nameValuePairs.containsKey(name); + } + + public Object get(String name) throws JSONException { + Object result = nameValuePairs.get(name); + if (result == null) { + throw new JSONException("No value for " + name); + } + return result; + } + + public Object opt(String name) { + return nameValuePairs.get(name); + } + + public boolean getBoolean(String name) throws JSONException { + Object object = get(name); + Boolean result = JSON.toBoolean(object); + if (result == null) { + throw JSON.typeMismatch(name, object, "boolean"); + } + return result; + } + + public boolean optBoolean(String name) { + return optBoolean(name, false); + } + + public boolean optBoolean(String name, boolean fallback) { + Object object = opt(name); + Boolean result = JSON.toBoolean(object); + return result != null ? result : fallback; + } + + public double getDouble(String name) throws JSONException { + Object object = get(name); + Double result = JSON.toDouble(object); + if (result == null) { + throw JSON.typeMismatch(name, object, "double"); + } + return result; + } + + public double optDouble(String name) { + return optDouble(name, Double.NaN); + } + + public double optDouble(String name, double fallback) { + Object object = opt(name); + Double result = JSON.toDouble(object); + return result != null ? result : fallback; + } + + public int getInt(String name) throws JSONException { + Object object = get(name); + Integer result = JSON.toInteger(object); + if (result == null) { + throw JSON.typeMismatch(name, object, "int"); + } + return result; + } + + public int optInt(String name) { + return optInt(name, 0); + } + + public int optInt(String name, int fallback) { + Object object = opt(name); + Integer result = JSON.toInteger(object); + return result != null ? result : fallback; + } + + public long getLong(String name) throws JSONException { + Object object = get(name); + Long result = JSON.toLong(object); + if (result == null) { + throw JSON.typeMismatch(name, object, "long"); + } + return result; + } + + public long optLong(String name) { + return optLong(name, 0L); + } + + public long optLong(String name, long fallback) { + Object object = opt(name); + Long result = JSON.toLong(object); + return result != null ? result : fallback; + } + + public String getString(String name) throws JSONException { + Object object = get(name); + String result = JSON.toString(object); + if (result == null) { + throw JSON.typeMismatch(name, object, "String"); + } + return result; + } + + public String optString(String name) { + return optString(name, ""); + } + + public String optString(String name, String fallback) { + Object object = opt(name); + String result = JSON.toString(object); + return result != null ? result : fallback; + } + + public JSONArray getJSONArray(String name) throws JSONException { + Object object = get(name); + if (object instanceof JSONArray) { + return (JSONArray) object; + } else { + throw JSON.typeMismatch(name, object, "JSONArray"); + } + } + + public JSONArray optJSONArray(String name) { + Object object = opt(name); + return object instanceof JSONArray ? (JSONArray) object : null; + } + + public JSONObject getJSONObject(String name) throws JSONException { + Object object = get(name); + if (object instanceof JSONObject) { + return (JSONObject) object; + } else { + throw JSON.typeMismatch(name, object, "JSONObject"); + } + } + + public JSONObject optJSONObject(String name) { + Object object = opt(name); + return object instanceof JSONObject ? (JSONObject) object : null; + } + + public JSONArray toJSONArray(JSONArray names) throws JSONException { + JSONArray result = new JSONArray(); + if (names == null) { + return null; + } + int length = names.length(); + if (length == 0) { + return null; + } + for (int i = 0; i < length; i++) { + String name = JSON.toString(names.opt(i)); + result.put(opt(name)); + } + return result; + } + + /* Return a raw type for API compatibility */ + public Iterator keys() { + return nameValuePairs.keySet().iterator(); + } + + public JSONArray names() { + return nameValuePairs.isEmpty() + ? null + : new JSONArray(new ArrayList<String>(nameValuePairs.keySet())); + } + + @Override public String toString() { + try { + JSONStringer stringer = new JSONStringer(); + writeTo(stringer); + return stringer.toString(); + } catch (JSONException e) { + return null; + } + } + + public String toString(int indentSpaces) throws JSONException { + JSONStringer stringer = new JSONStringer(indentSpaces); + writeTo(stringer); + return stringer.toString(); + } + + void writeTo(JSONStringer stringer) throws JSONException { + stringer.object(); + for (Map.Entry<String, Object> entry : nameValuePairs.entrySet()) { + stringer.key(entry.getKey()).value(entry.getValue()); + } + stringer.endObject(); + } + + public static String numberToString(Number number) throws JSONException { + if (number == null) { + throw new JSONException("Number must be non-null"); + } + + double doubleValue = number.doubleValue(); + JSON.checkDouble(doubleValue); + + // the original returns "-0" instead of "-0.0" for negative zero + if (number.equals(NEGATIVE_ZERO)) { + return "-0"; + } + + long longValue = number.longValue(); + if (doubleValue == (double) longValue) { + return Long.toString(longValue); + } + + return number.toString(); + } + + public static String quote(String data) { + if (data == null) { + return "\"\""; + } + try { + JSONStringer stringer = new JSONStringer(); + stringer.open(JSONStringer.Scope.NULL, ""); + stringer.value(data); + stringer.close(JSONStringer.Scope.NULL, JSONStringer.Scope.NULL, ""); + return stringer.toString(); + } catch (JSONException e) { + throw new AssertionError(); + } + } +} diff --git a/json/src/rewrite/java/org/json/JSONStringer.java b/json/src/rewrite/java/org/json/JSONStringer.java new file mode 100644 index 0000000..fb60bd1 --- /dev/null +++ b/json/src/rewrite/java/org/json/JSONStringer.java @@ -0,0 +1,340 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.json; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +// Note: this class was written without inspecting the non-free org.json sourcecode. + +/** + * + */ +public class JSONStringer { + + /** The output data, containing at most one top-level array or object. */ + final StringBuilder out = new StringBuilder(); + + /** + * Lexical scoping elements within this stringer, necessary to insert the + * appropriate separator characters (ie. commas and colons) and to detect + * nesting errors. + */ + enum Scope { + + /** + * An array with no elements requires no separators or newlines before + * it is closed. + */ + EMPTY_ARRAY, + + /** + * A array with at least one value requires a comma and newline before + * the next element. + */ + NONEMPTY_ARRAY, + + /** + * An object with no keys or values requires no separators or newlines + * before it is closed. + */ + EMPTY_OBJECT, + + /** + * An object whose most recent element is a key. The next element must + * be a value. + */ + DANGLING_KEY, + + /** + * An object with at least one name/value pair requires a comma and + * newline before the next element. + */ + NONEMPTY_OBJECT, + + /** + * A special bracketless array needed by JSONStringer.join() and + * JSONObject.quote() only. Not used for JSON encoding. + */ + NULL, + } + + /** + * Unlike the original implementation, this stack isn't limited to 20 + * levels of nesting. + */ + private final List<Scope> stack = new ArrayList<Scope>(); + + /** + * A string containing a full set of spaces for a single level of + * indentation, or null for no pretty printing. + */ + private final String indent; + + public JSONStringer() { + indent = null; + } + + JSONStringer(int indentSpaces) { + char[] indentChars = new char[indentSpaces]; + Arrays.fill(indentChars, ' '); + indent = new String(indentChars); + } + + public JSONStringer array() throws JSONException { + return open(Scope.EMPTY_ARRAY, "["); + } + + public JSONStringer endArray() throws JSONException { + return close(Scope.EMPTY_ARRAY, Scope.NONEMPTY_ARRAY, "]"); + } + + public JSONStringer object() throws JSONException { + return open(Scope.EMPTY_OBJECT, "{"); + } + + public JSONStringer endObject() throws JSONException { + return close(Scope.EMPTY_OBJECT, Scope.NONEMPTY_OBJECT, "}"); + } + + /** + * Enters a new scope by appending any necessary whitespace and the given + * bracket. + */ + JSONStringer open(Scope empty, String openBracket) throws JSONException { + if (stack.isEmpty() && out.length() > 0) { + throw new JSONException("Nesting problem: multiple top-level roots"); + } + beforeValue(); + stack.add(empty); + out.append(openBracket); + return this; + } + + /** + * Closes the current scope by appending any necessary whitespace and the + * given bracket. + */ + JSONStringer close(Scope empty, Scope nonempty, String closeBracket) throws JSONException { + Scope context = peek(); + if (context != nonempty && context != empty) { + throw new JSONException("Nesting problem"); + } + + stack.remove(stack.size() - 1); + if (context == nonempty) { + newline(); + } + out.append(closeBracket); + return this; + } + + /** + * Returns the value on the top of the stack. + */ + private Scope peek() throws JSONException { + if (stack.isEmpty()) { + throw new JSONException("Nesting problem"); + } + return stack.get(stack.size() - 1); + } + + /** + * Replace the value on the top of the stack with the given value. + */ + private void replaceTop(Scope topOfStack) { + stack.set(stack.size() - 1, topOfStack); + } + + public JSONStringer value(Object value) throws JSONException { + if (stack.isEmpty()) { + throw new JSONException("Nesting problem"); + } + + if (value instanceof JSONArray) { + ((JSONArray) value).writeTo(this); + return this; + + } else if (value instanceof JSONObject) { + ((JSONObject) value).writeTo(this); + return this; + } + + beforeValue(); + + if (value == null + || value instanceof Boolean + || value == JSONObject.NULL) { + out.append(value); + + } else if (value instanceof Number) { + out.append(JSONObject.numberToString((Number) value)); + + } else { + string(value.toString()); + } + + return this; + } + + public JSONStringer value(boolean value) throws JSONException { + if (stack.isEmpty()) { + throw new JSONException("Nesting problem"); + } + beforeValue(); + out.append(value); + return this; + } + + public JSONStringer value(double value) throws JSONException { + if (stack.isEmpty()) { + throw new JSONException("Nesting problem"); + } + beforeValue(); + out.append(JSONObject.numberToString(value)); + return this; + } + + public JSONStringer value(long value) throws JSONException { + if (stack.isEmpty()) { + throw new JSONException("Nesting problem"); + } + beforeValue(); + out.append(value); + return this; + } + + private void string(String value) { + out.append("\""); + for (int i = 0, length = value.length(); i < length; i++) { + char c = value.charAt(i); + + /* + * From RFC 4627, "All Unicode characters may be placed within the + * quotation marks except for the characters that must be escaped: + * quotation mark, reverse solidus, and the control characters + * (U+0000 through U+001F)." + */ + switch (c) { + case '"': + case '\\': + case '/': + out.append('\\').append(c); + break; + + case '\t': + out.append("\\t"); + break; + + case '\b': + out.append("\\b"); + break; + + case '\n': + out.append("\\n"); + break; + + case '\r': + out.append("\\r"); + break; + + case '\f': + out.append("\\f"); + break; + + default: + if (c <= 0x1F) { + out.append(String.format("\\u%04x", (int) c)); + } else { + out.append(c); + } + break; + } + + } + out.append("\""); + } + + private void newline() { + if (indent == null) { + return; + } + + out.append("\n"); + for (int i = 0; i < stack.size(); i++) { + out.append(indent); + } + } + + public JSONStringer key(String name) throws JSONException { + if (name == null) { + throw new JSONException("Names must be non-null"); + } + beforeKey(); + string(name); + return this; + } + + /** + * Inserts any necessary separators and whitespace before a name. Also + * adjusts the stack to expect the key's value. + */ + private void beforeKey() throws JSONException { + Scope context = peek(); + if (context == Scope.NONEMPTY_OBJECT) { // first in object + out.append(','); + } else if (context != Scope.EMPTY_OBJECT) { // not in an object! + throw new JSONException("Nesting problem"); + } + newline(); + replaceTop(Scope.DANGLING_KEY); + } + + /** + * Inserts any necessary separators and whitespace before a literal value, + * inline array, or inline object. Also adjusts the stack to expect either a + * closing bracket or another element. + */ + private void beforeValue() throws JSONException { + if (stack.isEmpty()) { + return; + } + + Scope context = peek(); + if (context == Scope.EMPTY_ARRAY) { // first in array + replaceTop(Scope.NONEMPTY_ARRAY); + newline(); + } else if (context == Scope.NONEMPTY_ARRAY) { // another in array + out.append(','); + newline(); + } else if (context == Scope.DANGLING_KEY) { // value for key + out.append(indent == null ? ":" : ": "); + replaceTop(Scope.NONEMPTY_OBJECT); + } else if (context != Scope.NULL) { + throw new JSONException("Nesting problem"); + } + } + + /** + * Although it contradicts the general contract of {@link Object#toString}, + * this method returns null if the stringer contains no data. + */ + @Override public String toString() { + return out.length() == 0 ? null : out.toString(); + } +} diff --git a/json/src/rewrite/java/org/json/JSONTokener.java b/json/src/rewrite/java/org/json/JSONTokener.java new file mode 100644 index 0000000..e249c74 --- /dev/null +++ b/json/src/rewrite/java/org/json/JSONTokener.java @@ -0,0 +1,480 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.json; + +// Note: this class was written without inspecting the non-free org.json sourcecode. + +/** + * + */ +public class JSONTokener { + + /** The input JSON. */ + private final String in; + + /** + * The index of the next character to be returned by {@link #next()}. When + * the input is exhausted, this equals the input's length. + */ + private int pos; + + public JSONTokener(String in) { + this.in = in; + } + + public Object nextValue() throws JSONException { + int c = nextCleanInternal(); + switch (c) { + case -1: + throw syntaxError("End of input"); + + case '{': + return readObject(); + + case '[': + return readArray(); + + case '\'': + case '"': + return nextString((char) c); + + default: + pos--; + return readLiteral(); + } + } + + private int nextCleanInternal() throws JSONException { + while (pos < in.length()) { + int c = in.charAt(pos++); + switch (c) { + case '\t': + case ' ': + case '\n': + case '\r': + continue; + + case '/': + if (pos == in.length()) { + return c; + } + + char peek = in.charAt(pos); + if (peek != '*' && peek != '/') { + return c; + } + + skipComment(); + continue; + + default: + return c; + } + } + + return -1; + } + + /** + * Advances the position until it is beyond the current comment. The opening + * slash '/' should have already been read, and character at the current + * position be an asterisk '*' for a C-style comment or a slash '/' for an + * end-of-line comment. + * + * @throws JSONException if a C-style comment was not terminated. + */ + private void skipComment() throws JSONException { + if (in.charAt(pos++) == '*') { + int commentEnd = in.indexOf("*/", pos); + if (commentEnd == -1) { + throw syntaxError("Unterminated comment"); + } + pos = commentEnd + 2; + + } else { + /* + * Skip to the next newline character. If the line is terminated by + * "\r\n", the '\n' will be consumed as whitespace by the caller. + */ + for (; pos < in.length(); pos++) { + char c = in.charAt(pos); + if (c == '\r' || c == '\n') { + pos++; + break; + } + } + } + } + + /** + * + * + * @throws NumberFormatException if any unicode escape sequences are + * malformed. + */ + public String nextString(char quote) throws JSONException { + /* + * For strings that are free of escape sequences, we can just extract + * the result as a substring of the input. But if we encounter an escape + * sequence, we need to use a StringBuilder to compose the result. + */ + StringBuilder builder = null; + + /* the index of the first character not yet appended to the builder. */ + int start = pos; + + while (pos < in.length()) { + int c = in.charAt(pos++); + if (c == quote) { + if (builder == null) { + // a new string avoids leaking memory + return new String(in.substring(start, pos - 1)); + } else { + builder.append(in, start, pos - 1); + return builder.toString(); + } + } + + if (c == '\\') { + if (pos == in.length()) { + throw syntaxError("Unterminated escape sequence"); + } + if (builder == null) { + builder = new StringBuilder(); + } + builder.append(in, start, pos - 1); + builder.append(readEscapeCharacter()); + start = pos; + } + } + + throw syntaxError("Unterminated string"); + } + + /** + * Unescapes the character identified by the character or characters that + * immediately follow a backslash. The backslash '\' should have already + * been read. This supports both unicode escapes "u000A" and two-character + * escapes "\n". + * + * @throws NumberFormatException if any unicode escape sequences are + * malformed. + */ + private char readEscapeCharacter() throws JSONException { + char escaped = in.charAt(pos++); + switch (escaped) { + case 'u': + if (pos + 4 > in.length()) { + throw syntaxError("Unterminated escape sequence"); + } + String hex = in.substring(pos, pos + 4); + pos += 4; + return (char) Integer.parseInt(hex, 16); + + case 't': + return '\t'; + + case 'b': + return '\b'; + + case 'n': + return '\n'; + + case 'r': + return '\r'; + + case 'f': + return '\f'; + + case '\'': + case '"': + case '\\': + default: + return escaped; + } + } + + /** + * Reads a null, boolean, numeric or unquoted string literal value. Numeric + * values will be returned as an Integer, Long, or Double, in that order of + * preference. + */ + private Object readLiteral() throws JSONException { + String literal = nextToInternal("{}[]/\\:,=;# \t\f"); + + if (literal.length() == 0) { + throw syntaxError("Expected literal value"); + } else if ("null".equalsIgnoreCase(literal)) { + return JSONObject.NULL; + } else if ("true".equalsIgnoreCase(literal)) { + return Boolean.TRUE; + } else if ("false".equalsIgnoreCase(literal)) { + return Boolean.FALSE; + } + + /* try to parse as an integral type... */ + if (literal.indexOf('.') == -1) { + int base = 10; + String number = literal; + if (number.startsWith("0x") || number.startsWith("0X")) { + number = number.substring(2); + base = 16; + } else if (number.startsWith("0") && number.length() > 1) { + number = number.substring(1); + base = 8; + } + try { + long longValue = Long.parseLong(number, base); + if (longValue <= Integer.MAX_VALUE && longValue >= Integer.MIN_VALUE) { + return (int) longValue; + } else { + return longValue; + } + } catch (NumberFormatException e) { + /* + * This only happens for integral numbers greater than + * Long.MAX_VALUE, numbers in exponential form (5e-10) and + * unquoted strings. Fall through to try floating point. + */ + } + } + + /* ...next try to parse as a floating point... */ + try { + return Double.valueOf(literal); + } catch (NumberFormatException e) { + } + + /* ... finally give up. We have an unquoted string */ + return new String(literal); // a new string avoids leaking memory + } + + /** + * Returns text from the current position until the first of any of the + * given characters or a newline character, excluding that character. The + * position is advanced to the excluded character. + */ + private String nextToInternal(String excluded) { + int start = pos; + for (; pos < in.length(); pos++) { + char c = in.charAt(pos); + if (c == '\r' || c == '\n' || excluded.indexOf(c) != -1) { + return in.substring(start, pos); + } + } + return in.substring(start); + } + + /** + * Reads a sequence of key/value pairs and the trailing closing brace '}' of + * an object. The opening brace '{' should have already been read. + */ + private JSONObject readObject() throws JSONException { + JSONObject result = new JSONObject(); + + /* Peek to see if this is the empty object. */ + int first = nextCleanInternal(); + if (first == '}') { + return result; + } else if (first != -1) { + pos--; + } + + while (true) { + Object name = nextValue(); + if (!(name instanceof String)) { + if (name == null) { + throw syntaxError("Names cannot be null"); + } else { + throw syntaxError("Names must be strings, but " + name + + " is of type " + name.getClass().getName()); + } + } + + /* + * Expect the name/value separator to be either a colon ':', an + * equals sign '=', or an arrow "=>". The last two are bogus but we + * include them because that's what the original implementation did. + */ + int separator = nextCleanInternal(); + if (separator != ':' && separator != '=') { + throw syntaxError("Expected ':' after " + name); + } + if (pos < in.length() && in.charAt(pos) == '>') { + pos++; + } + + result.put((String) name, nextValue()); + + switch (nextCleanInternal()) { + case '}': + return result; + case ';': + case ',': + continue; + default: + throw syntaxError("Unterminated object"); + } + } + } + + /** + * Reads a sequence of values and the trailing closing brace ']' of an + * array. The opening brace '[' should have already been read. Note that + * "[]" yields an empty array, but "[,]" returns a two-element array + * equivalent to "[null,null]". + */ + private JSONArray readArray() throws JSONException { + JSONArray result = new JSONArray(); + + /* to cover input that ends with ",]". */ + boolean hasTrailingSeparator = false; + + while (true) { + switch (nextCleanInternal()) { + case -1: + throw syntaxError("Unterminated array"); + case ']': + if (hasTrailingSeparator) { + result.put(null); + } + return result; + case ',': + case ';': + /* A separator without a value first means "null". */ + result.put(null); + hasTrailingSeparator = true; + continue; + default: + pos--; + } + + result.put(nextValue()); + + switch (nextCleanInternal()) { + case ']': + return result; + case ',': + case ';': + hasTrailingSeparator = true; + continue; + default: + throw syntaxError("Unterminated array"); + } + } + } + + public JSONException syntaxError(String text) { + return new JSONException(text + this); + } + + @Override public String toString() { + // consistent with the original implementation + return " at character " + pos + " of " + in; + } + + /* + * Legacy APIs. + * + * None of the methods below are on the critical path of parsing JSON + * documents. They exist only because they were exposed by the original + * implementation and may be used by some clients. + */ + + public boolean more() { + return pos < in.length(); + } + + public char next() { + return pos < in.length() ? in.charAt(pos++) : '\0'; + } + + public char next(char c) throws JSONException { + char result = next(); + if (result != c) { + throw syntaxError("Expected " + c + " but was " + result); + } + return result; + } + + public char nextClean() throws JSONException { + int nextCleanInt = nextCleanInternal(); + return nextCleanInt == -1 ? '\0' : (char) nextCleanInt; + } + + /** + * TODO: note about how this method returns a substring, and could cause a memory leak + */ + public String next(int length) throws JSONException { + if (pos + length > in.length()) { + throw syntaxError(length + " is out of bounds"); + } + String result = in.substring(pos, pos + length); + pos += length; + return result; + } + + /** + * TODO: note about how this method returns a substring, and could cause a memory leak + */ + public String nextTo(String excluded) { + if (excluded == null) { + throw new NullPointerException(); + } + return nextToInternal(excluded).trim(); + } + + /** + * TODO: note about how this method returns a substring, and could cause a memory leak + */ + public String nextTo(char excluded) { + return nextToInternal(String.valueOf(excluded)).trim(); + } + + public void skipPast(String thru) { + int thruStart = in.indexOf(thru, pos); + pos = thruStart == -1 ? in.length() : (thruStart + thru.length()); + } + + public char skipTo(char to) { + for (int i = pos, length = in.length(); i < length; i++) { + if (in.charAt(i) == to) { + pos = i; + return to; + } + } + return '\0'; + } + + public void back() { + if (--pos == -1) { + pos = 0; + } + } + + public static int dehexchar(char hex) { + if (hex >= '0' && hex <= '9') { + return hex - '0'; + } else if (hex >= 'A' && hex <= 'F') { + return hex - 'A' + 10; + } else if (hex >= 'a' && hex <= 'f') { + return hex - 'a' + 10; + } else { + return -1; + } + } +} diff --git a/json/src/test/java/org/json/AllTests.java b/json/src/test/java/org/json/AllTests.java index ee6c90e..8f5cc94 100644 --- a/json/src/test/java/org/json/AllTests.java +++ b/json/src/test/java/org/json/AllTests.java @@ -26,6 +26,7 @@ public class AllTests { suite.addTestSuite(JSONObjectTest.class); suite.addTestSuite(JSONStringerTest.class); suite.addTestSuite(JSONTokenerTest.class); + suite.addTestSuite(ParsingTest.class); suite.addTestSuite(SelfUseTest.class); return suite; } diff --git a/json/src/test/java/org/json/JSONArrayTest.java b/json/src/test/java/org/json/JSONArrayTest.java index d6013a8..485a729 100644 --- a/json/src/test/java/org/json/JSONArrayTest.java +++ b/json/src/test/java/org/json/JSONArrayTest.java @@ -1,11 +1,11 @@ -/** +/* * Copyright (C) 2010 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -20,6 +20,7 @@ import junit.framework.TestCase; import java.util.Arrays; import java.util.List; +import java.util.Collections; /** * This black box test was written without inspecting the non-free org.json sourcecode. @@ -50,7 +51,7 @@ public class JSONArrayTest extends TestCase { assertFalse(array.optBoolean(0)); assertTrue(array.optBoolean(0, true)); - // bogus (but documented) behaviour: returns null rather than an empty object + // bogus (but documented) behaviour: returns null rather than an empty object! assertNull(array.toJSONObject(new JSONArray())); } @@ -58,8 +59,7 @@ public class JSONArrayTest extends TestCase { JSONArray a = new JSONArray(); JSONArray b = new JSONArray(); assertTrue(a.equals(b)); - // bogus behavior: JSONArray overrides equals() but not hashCode(). - assertEquals(a.hashCode(), b.hashCode()); + assertEquals("equals() not consistent with hashCode()", a.hashCode(), b.hashCode()); a.put(true); a.put(false); @@ -129,7 +129,7 @@ public class JSONArrayTest extends TestCase { assertEquals(4, array.length()); assertEquals("[null,null,null,null]", array.toString()); - // bogus behaviour: there's 2 ways to represent null; each behaves differently! + // there's 2 ways to represent null; each behaves differently! assertEquals(JSONObject.NULL, array.get(0)); try { array.get(1); @@ -168,7 +168,7 @@ public class JSONArrayTest extends TestCase { array.put(-0d); assertEquals(4, array.length()); - // bogus behaviour: toString() and getString(int) return different values for -0d + // toString() and getString(int) return different values for -0d assertEquals("[4.9E-324,9223372036854775806,1.7976931348623157E308,-0]", array.toString()); assertEquals(Double.MIN_VALUE, array.get(0)); @@ -259,6 +259,32 @@ public class JSONArrayTest extends TestCase { assertEquals(-1.0d, array.optDouble(3, -1.0d)); } + public void testJoin() throws JSONException { + JSONArray array = new JSONArray(); + array.put(null); + assertEquals("null", array.join(" & ")); + array.put("\""); + assertEquals("null & \"\\\"\"", array.join(" & ")); + array.put(5); + assertEquals("null & \"\\\"\" & 5", array.join(" & ")); + array.put(true); + assertEquals("null & \"\\\"\" & 5 & true", array.join(" & ")); + array.put(new JSONArray(Arrays.asList(true, false))); + assertEquals("null & \"\\\"\" & 5 & true & [true,false]", array.join(" & ")); + array.put(new JSONObject(Collections.singletonMap("x", 6))); + assertEquals("null & \"\\\"\" & 5 & true & [true,false] & {\"x\":6}", array.join(" & ")); + } + + public void testJoinWithNull() throws JSONException { + JSONArray array = new JSONArray(Arrays.asList(5, 6)); + assertEquals("5null6", array.join(null)); + } + + public void testJoinWithSpecialCharacters() throws JSONException { + JSONArray array = new JSONArray(Arrays.asList(5, 6)); + assertEquals("5\"6", array.join("\"")); + } + public void testToJSONObject() throws JSONException { JSONArray keys = new JSONArray(); keys.put("a"); @@ -286,7 +312,7 @@ public class JSONArrayTest extends TestCase { values.put(5.5d); values.put(null); - // bogus behaviour: null values are stripped + // null values are stripped! JSONObject object = values.toJSONObject(keys); assertEquals(1, object.length()); assertFalse(object.has("b")); @@ -345,6 +371,14 @@ public class JSONArrayTest extends TestCase { } } + public void testPutUnsupportedNumbersAsObject() throws JSONException { + JSONArray array = new JSONArray(); + array.put(Double.valueOf(Double.NaN)); + array.put(Double.valueOf(Double.NEGATIVE_INFINITY)); + array.put(Double.valueOf(Double.POSITIVE_INFINITY)); + assertEquals(null, array.toString()); + } + /** * Although JSONArray is usually defensive about which numbers it accepts, * it doesn't check inputs in its constructor. @@ -357,7 +391,7 @@ public class JSONArrayTest extends TestCase { } public void testToStringWithUnsupportedNumbers() throws JSONException { - // bogus behaviour: when the array contains an unsupported number, toString returns null + // when the array contains an unsupported number, toString returns null! JSONArray array = new JSONArray(Arrays.asList(5.5, Double.NaN)); assertNull(array.toString()); } @@ -369,6 +403,70 @@ public class JSONArrayTest extends TestCase { assertEquals(5, array.get(0)); } + public void testTokenerConstructor() throws JSONException { + JSONArray object = new JSONArray(new JSONTokener("[false]")); + assertEquals(1, object.length()); + assertEquals(false, object.get(0)); + } + + public void testTokenerConstructorWrongType() throws JSONException { + try { + new JSONArray(new JSONTokener("{\"foo\": false}")); + fail(); + } catch (JSONException e) { + } + } + + public void testTokenerConstructorNull() throws JSONException { + try { + new JSONArray((JSONTokener) null); + fail(); + } catch (NullPointerException e) { + } + } + + public void testTokenerConstructorParseFail() { + try { + new JSONArray(new JSONTokener("[")); + fail(); + } catch (JSONException e) { + } catch (StackOverflowError e) { + fail("Stack overflowed on input: \"[\""); + } + } + + public void testStringConstructor() throws JSONException { + JSONArray object = new JSONArray("[false]"); + assertEquals(1, object.length()); + assertEquals(false, object.get(0)); + } + + public void testStringConstructorWrongType() throws JSONException { + try { + new JSONArray("{\"foo\": false}"); + fail(); + } catch (JSONException e) { + } + } + + public void testStringConstructorNull() throws JSONException { + try { + new JSONArray((String) null); + fail(); + } catch (NullPointerException e) { + } + } + + public void testStringConstructorParseFail() { + try { + new JSONArray("["); + fail(); + } catch (JSONException e) { + } catch (StackOverflowError e) { + fail("Stack overflowed on input: \"[\""); + } + } + public void testCreate() throws JSONException { JSONArray array = new JSONArray(Arrays.asList(5.5, true)); assertEquals(2, array.length()); @@ -405,8 +503,4 @@ public class JSONArrayTest extends TestCase { } catch (JSONException e) { } } - - public void testParsingConstructor() { - fail("TODO"); - } } diff --git a/json/src/test/java/org/json/JSONObjectTest.java b/json/src/test/java/org/json/JSONObjectTest.java index 83beea8..e431096 100644 --- a/json/src/test/java/org/json/JSONObjectTest.java +++ b/json/src/test/java/org/json/JSONObjectTest.java @@ -1,11 +1,11 @@ -/** - * Copyright (C) 2010 Google Inc. +/* + * Copyright (C) 2010 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -14,7 +14,6 @@ * limitations under the License. */ - package org.json; import junit.framework.TestCase; @@ -32,10 +31,10 @@ public class JSONObjectTest extends TestCase { JSONObject object = new JSONObject(); assertEquals(0, object.length()); - // bogus (but documented) behaviour: returns null rather than the empty object + // bogus (but documented) behaviour: returns null rather than the empty object! assertNull(object.names()); - // bogus behaviour: returns null rather than an empty array + // returns null rather than an empty array! assertNull(object.toJSONArray(new JSONArray())); assertEquals("{}", object.toString()); assertEquals("{}", object.toString(5)); @@ -246,13 +245,14 @@ public class JSONObjectTest extends TestCase { object.put("quux", -0d); assertEquals(4, object.length()); - assertTrue(object.toString().contains("\"foo\":4.9E-324")); - assertTrue(object.toString().contains("\"bar\":9223372036854775806")); - assertTrue(object.toString().contains("\"baz\":1.7976931348623157E308")); + String toString = object.toString(); + assertTrue(toString, toString.contains("\"foo\":4.9E-324")); + assertTrue(toString, toString.contains("\"bar\":9223372036854775806")); + assertTrue(toString, toString.contains("\"baz\":1.7976931348623157E308")); - // bogus behaviour: toString() and getString() return different values for -0d - assertTrue(object.toString().contains("\"quux\":-0}") // no trailing decimal point - || object.toString().contains("\"quux\":-0,")); + // toString() and getString() return different values for -0d! + assertTrue(toString, toString.contains("\"quux\":-0}") // no trailing decimal point + || toString.contains("\"quux\":-0,")); assertEquals(Double.MIN_VALUE, object.get("foo")); assertEquals(9223372036854775806L, object.get("bar")); @@ -310,13 +310,13 @@ public class JSONObjectTest extends TestCase { public void testOtherNumbers() throws JSONException { Number nan = new Number() { public int intValue() { - return 0; + throw new UnsupportedOperationException(); } public long longValue() { - return 1L; + throw new UnsupportedOperationException(); } public float floatValue() { - return 2; + throw new UnsupportedOperationException(); } public double doubleValue() { return Double.NaN; @@ -326,10 +326,12 @@ public class JSONObjectTest extends TestCase { } }; - // bogus behaviour: foreign object types should be rejected! JSONObject object = new JSONObject(); - object.put("foo", nan); - assertEquals("{\"foo\":x}", object.toString()); + try { + object.put("foo", nan); + fail("Object.put() accepted a NaN (via a custom Number class)"); + } catch (JSONException e) { + } } public void testForeignObjects() throws JSONException { @@ -339,7 +341,7 @@ public class JSONObjectTest extends TestCase { } }; - // bogus behaviour: foreign object types should be rejected and not treated as Strings + // foreign object types are accepted and treated as Strings! JSONObject object = new JSONObject(); object.put("foo", foreign); assertEquals("{\"foo\":\"x\"}", object.toString()); @@ -527,7 +529,7 @@ public class JSONObjectTest extends TestCase { names.put(false); names.put("foo"); - // bogus behaviour: array elements are converted to Strings + // array elements are converted to strings to do name lookups on the map! JSONArray array = object.toJSONArray(names); assertEquals(3, array.length()); assertEquals(10, array.get(0)); @@ -590,7 +592,7 @@ public class JSONObjectTest extends TestCase { } public void testToStringWithUnsupportedNumbers() { - // bogus behaviour: when the object contains an unsupported number, toString returns null + // when the object contains an unsupported number, toString returns null! JSONObject object = new JSONObject(Collections.singletonMap("foo", Double.NaN)); assertEquals(null, object.toString()); } @@ -607,8 +609,97 @@ public class JSONObjectTest extends TestCase { Map<Object, Object> contents = new HashMap<Object, Object>(); contents.put(5, 5); - // bogus behaviour: the constructor doesn't validate its input - new JSONObject(contents); + try { + new JSONObject(contents); + fail("JSONObject constructor doesn't validate its input!"); + } catch (Exception e) { + } + } + + public void testTokenerConstructor() throws JSONException { + JSONObject object = new JSONObject(new JSONTokener("{\"foo\": false}")); + assertEquals(1, object.length()); + assertEquals(false, object.get("foo")); + } + + public void testTokenerConstructorWrongType() throws JSONException { + try { + new JSONObject(new JSONTokener("[\"foo\", false]")); + fail(); + } catch (JSONException e) { + } + } + + public void testTokenerConstructorNull() throws JSONException { + try { + new JSONObject((JSONTokener) null); + fail(); + } catch (NullPointerException e) { + } + } + + public void testTokenerConstructorParseFail() { + try { + new JSONObject(new JSONTokener("{")); + fail(); + } catch (JSONException e) { + } + } + + public void testStringConstructor() throws JSONException { + JSONObject object = new JSONObject("{\"foo\": false}"); + assertEquals(1, object.length()); + assertEquals(false, object.get("foo")); + } + + public void testStringConstructorWrongType() throws JSONException { + try { + new JSONObject("[\"foo\", false]"); + fail(); + } catch (JSONException e) { + } + } + + public void testStringConstructorNull() throws JSONException { + try { + new JSONObject((String) null); + fail(); + } catch (NullPointerException e) { + } + } + + public void testStringonstructorParseFail() { + try { + new JSONObject("{"); + fail(); + } catch (JSONException e) { + } + } + + public void testCopyConstructor() throws JSONException { + JSONObject source = new JSONObject(); + source.put("a", JSONObject.NULL); + source.put("b", false); + source.put("c", 5); + + JSONObject copy = new JSONObject(source, new String[] { "a", "c" }); + assertEquals(2, copy.length()); + assertEquals(JSONObject.NULL, copy.get("a")); + assertEquals(5, copy.get("c")); + assertEquals(null, copy.opt("b")); + } + + public void testCopyConstructorMissingName() throws JSONException { + JSONObject source = new JSONObject(); + source.put("a", JSONObject.NULL); + source.put("b", false); + source.put("c", 5); + + JSONObject copy = new JSONObject(source, new String[]{ "a", "c", "d" }); + assertEquals(2, copy.length()); + assertEquals(JSONObject.NULL, copy.get("a")); + assertEquals(5, copy.get("c")); + assertEquals(0, copy.optInt("b")); } public void testAccumulateMutatesInPlace() throws JSONException { @@ -658,7 +749,7 @@ public class JSONObjectTest extends TestCase { object.put("foo", JSONObject.NULL); object.put("bar", null); - // bogus behaviour: there's 2 ways to represent null; each behaves differently! + // there are two ways to represent null; each behaves differently! assertTrue(object.has("foo")); assertFalse(object.has("bar")); assertTrue(object.isNull("foo")); @@ -767,7 +858,11 @@ public class JSONObjectTest extends TestCase { } public void testQuote() { - // covered by JSONStringerTest.testEscaping() + // covered by JSONStringerTest.testEscaping + } + + public void testQuoteNull() throws JSONException { + assertEquals("\"\"", JSONObject.quote(null)); } public void testNumberToString() throws JSONException { diff --git a/json/src/test/java/org/json/JSONStringerTest.java b/json/src/test/java/org/json/JSONStringerTest.java index 03a2903..64c3a74 100644 --- a/json/src/test/java/org/json/JSONStringerTest.java +++ b/json/src/test/java/org/json/JSONStringerTest.java @@ -1,11 +1,11 @@ -/** +/* * Copyright (C) 2010 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -24,7 +24,7 @@ import junit.framework.TestCase; public class JSONStringerTest extends TestCase { public void testEmptyStringer() { - // bogus behaviour: why isn't this the empty string? + // why isn't this the empty string? assertNull(new JSONStringer().toString()); } @@ -62,6 +62,16 @@ public class JSONStringerTest extends TestCase { assertEquals("[false,5,5,\"five\",null]", stringer.toString()); } + public void testValueObjectMethods() throws JSONException { + JSONStringer stringer = new JSONStringer(); + stringer.array(); + stringer.value(Boolean.FALSE); + stringer.value(Double.valueOf(5.0)); + stringer.value(Long.valueOf(5L)); + stringer.endArray(); + assertEquals("[false,5,5]", stringer.toString()); + } + public void testKeyValue() throws JSONException { JSONStringer stringer = new JSONStringer(); stringer.object(); @@ -187,26 +197,27 @@ public class JSONStringerTest extends TestCase { public void testEscaping() throws JSONException { assertEscapedAllWays("a", "a"); - assertEscapedAllWays("a\"", "a\\\""); - assertEscapedAllWays("\"", "\\\""); + assertEscapedAllWays("a\\\"", "a\""); + assertEscapedAllWays("\\\"", "\""); assertEscapedAllWays(":", ":"); assertEscapedAllWays(",", ","); - assertEscapedAllWays("\n", "\\n"); - assertEscapedAllWays("\t", "\\t"); + assertEscapedAllWays("\\b", "\b"); + assertEscapedAllWays("\\f", "\f"); + assertEscapedAllWays("\\n", "\n"); + assertEscapedAllWays("\\r", "\r"); + assertEscapedAllWays("\\t", "\t"); assertEscapedAllWays(" ", " "); - assertEscapedAllWays("\\", "\\\\"); + assertEscapedAllWays("\\\\", "\\"); assertEscapedAllWays("{", "{"); assertEscapedAllWays("}", "}"); assertEscapedAllWays("[", "["); assertEscapedAllWays("]", "]"); - - // how does it decide which characters to escape? - assertEscapedAllWays("\0", "\\u0000"); - assertEscapedAllWays("\u0019", "\\u0019"); - assertEscapedAllWays("\u0020", " "); + assertEscapedAllWays("\\u0000", "\0"); + assertEscapedAllWays("\\u0019", "\u0019"); + assertEscapedAllWays(" ", "\u0020"); } - private void assertEscapedAllWays(String original, String escaped) throws JSONException { + private void assertEscapedAllWays(String escaped, String original) throws JSONException { assertEquals("{\"" + escaped + "\":false}", new JSONStringer().object().key(original).value(false).endObject().toString()); assertEquals("{\"a\":\"" + escaped + "\"}", @@ -236,7 +247,7 @@ public class JSONStringerTest extends TestCase { assertEquals("{\"b\":{\"a\":false}}", stringer.toString()); } - public void testArrayNestingMaxDepthIs20() throws JSONException { + public void testArrayNestingMaxDepthSupports20() throws JSONException { JSONStringer stringer = new JSONStringer(); for (int i = 0; i < 20; i++) { stringer.array(); @@ -250,14 +261,9 @@ public class JSONStringerTest extends TestCase { for (int i = 0; i < 20; i++) { stringer.array(); } - try { - stringer.array(); - fail(); - } catch (JSONException e) { - } } - public void testObjectNestingMaxDepthIs20() throws JSONException { + public void testObjectNestingMaxDepthSupports20() throws JSONException { JSONStringer stringer = new JSONStringer(); for (int i = 0; i < 20; i++) { stringer.object(); @@ -276,14 +282,9 @@ public class JSONStringerTest extends TestCase { stringer.object(); stringer.key("a"); } - try { - stringer.object(); - fail(); - } catch (JSONException e) { - } } - public void testMixedMaxDepth() throws JSONException { + public void testMixedMaxDepthSupports20() throws JSONException { JSONStringer stringer = new JSONStringer(); for (int i = 0; i < 20; i+=2) { stringer.array(); @@ -305,11 +306,6 @@ public class JSONStringerTest extends TestCase { stringer.object(); stringer.key("a"); } - try { - stringer.array(); - fail(); - } catch (JSONException e) { - } } public void testMaxDepthWithArrayValue() throws JSONException { diff --git a/json/src/test/java/org/json/JSONTokenerTest.java b/json/src/test/java/org/json/JSONTokenerTest.java index 1409a3b..70b7384 100644 --- a/json/src/test/java/org/json/JSONTokenerTest.java +++ b/json/src/test/java/org/json/JSONTokenerTest.java @@ -1,11 +1,11 @@ -/** +/* * Copyright (C) 2010 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -17,6 +17,7 @@ package org.json; import junit.framework.TestCase; +import junit.framework.AssertionFailedError; /** * This black box test was written without inspecting the non-free org.json sourcecode. @@ -24,7 +25,7 @@ import junit.framework.TestCase; public class JSONTokenerTest extends TestCase { public void testNulls() throws JSONException { - // bogus behaviour: JSONTokener accepts null, only to fail later on almost all APIs. + // JSONTokener accepts null, only to fail later on almost all APIs! new JSONTokener(null).back(); try { @@ -147,12 +148,6 @@ public class JSONTokenerTest extends TestCase { } assertEquals('E', abcdeTokener.nextClean()); assertEquals('\0', abcdeTokener.next()); - try { - // bogus behaviour: returning an empty string should be valid - abcdeTokener.next(0); - fail(); - } catch (JSONException e) { - } assertFalse(abcdeTokener.more()); abcdeTokener.back(); assertTrue(abcdeTokener.more()); @@ -174,7 +169,7 @@ public class JSONTokenerTest extends TestCase { abcTokener.back(); abcTokener.back(); abcTokener.back(); - abcTokener.back(); // bogus behaviour: you can back up before the beginning of a String + abcTokener.back(); // you can back up before the beginning of a String! assertEquals('A', abcTokener.next()); } @@ -209,26 +204,31 @@ public class JSONTokenerTest extends TestCase { fail(); } catch (JSONException e) { } + } + + public void testNextNWithAllRemaining() throws JSONException { + JSONTokener tokener = new JSONTokener("ABCDEF"); + tokener.next(3); try { - // bogus behaviour: there should be 3 characters left, but there must be an off-by-one - // error in the implementation. - assertEquals("DEF", abcdeTokener.next(3)); - fail(); + tokener.next(3); } catch (JSONException e) { + AssertionFailedError error = new AssertionFailedError("off-by-one error?"); + error.initCause(e); + throw error; } - assertEquals("DE", abcdeTokener.next(2)); - assertEquals('F', abcdeTokener.next()); + } + + public void testNext0() throws JSONException { + JSONTokener tokener = new JSONTokener("ABCDEF"); + tokener.next(5); + tokener.next(); try { - // bogus behaviour: returning an empty string should be valid - abcdeTokener.next(0); - fail(); + tokener.next(0); } catch (JSONException e) { + Error error = new AssertionFailedError("Returning an empty string should be valid"); + error.initCause(e); + throw error; } - abcdeTokener.back(); - abcdeTokener.back(); - abcdeTokener.back(); - assertEquals("DE", abcdeTokener.next(2)); - assertEquals('F', abcdeTokener.next()); } public void testNextCleanComments() throws JSONException { @@ -241,6 +241,24 @@ public class JSONTokenerTest extends TestCase { assertEquals('\0', tokener.nextClean()); } + public void testNextCleanNestedCStyleComments() throws JSONException { + JSONTokener tokener = new JSONTokener("A /* B /* C */ D */ E"); + assertEquals('A', tokener.nextClean()); + assertEquals('D', tokener.nextClean()); + assertEquals('*', tokener.nextClean()); + assertEquals('/', tokener.nextClean()); + assertEquals('E', tokener.nextClean()); + } + + public void testNextCleanCommentsTrailingSingleSlash() throws JSONException { + JSONTokener tokener = new JSONTokener(" / S /"); + assertEquals('/', tokener.nextClean()); + assertEquals('S', tokener.nextClean()); + assertEquals('/', tokener.nextClean()); + assertEquals("nextClean doesn't consume a trailing slash", + '\0', tokener.nextClean()); + } + public void testNextCleanTrailingOpenComment() throws JSONException { try { new JSONTokener(" /* ").nextClean(); @@ -256,55 +274,54 @@ public class JSONTokenerTest extends TestCase { assertEquals('B', new JSONTokener(" // \r B ").nextClean()); } + public void testNextCleanSkippedWhitespace() throws JSONException { + assertEquals("character tabulation", 'A', new JSONTokener("\tA").nextClean()); + assertEquals("line feed", 'A', new JSONTokener("\nA").nextClean()); + assertEquals("carriage return", 'A', new JSONTokener("\rA").nextClean()); + assertEquals("space", 'A', new JSONTokener(" A").nextClean()); + } + /** * Tests which characters tokener treats as ignorable whitespace. See Kevin Bourrillion's * <a href="https://spreadsheets.google.com/pub?key=pd8dAQyHbdewRsnE5x5GzKQ">list * of whitespace characters</a>. */ - public void testNextCleanWhitespace() throws JSONException { - // This behaviour contradicts the JSON spec. It claims the only space - // characters are space, tab, newline and carriage return. But it treats - // many characters like whitespace! These are the same whitespace - // characters used by String.trim(), with the exception of '\0'. - assertEquals("character tabulation", 'A', new JSONTokener("\u0009A").nextClean()); - assertEquals("line feed", 'A', new JSONTokener("\nA").nextClean()); - assertEquals("line tabulation", 'A', new JSONTokener("\u000bA").nextClean()); - assertEquals("form feed", 'A', new JSONTokener("\u000cA").nextClean()); - assertEquals("carriage return", 'A', new JSONTokener("\rA").nextClean()); - assertEquals("information separator 4", 'A', new JSONTokener("\u001cA").nextClean()); - assertEquals("information separator 3", 'A', new JSONTokener("\u001dA").nextClean()); - assertEquals("information separator 2", 'A', new JSONTokener("\u001eA").nextClean()); - assertEquals("information separator 1", 'A', new JSONTokener("\u001fA").nextClean()); - assertEquals("space", 'A', new JSONTokener("\u0020A").nextClean()); - for (char c = '\u0002'; c < ' '; c++) { - assertEquals('A', new JSONTokener(new String(new char[] { ' ', c, 'A' })).nextClean()); - } - - // These characters are neither whitespace in the JSON spec nor the implementation - assertEquals("null", '\u0000', new JSONTokener("\u0000A").nextClean()); - assertEquals("next line", '\u0085', new JSONTokener("\u0085A").nextClean()); - assertEquals("non-breaking space", '\u00a0', new JSONTokener("\u00a0A").nextClean()); - assertEquals("ogham space mark", '\u1680', new JSONTokener("\u1680A").nextClean()); - assertEquals("mongolian vowel separator", '\u180e', new JSONTokener("\u180eA").nextClean()); - assertEquals("en quad", '\u2000', new JSONTokener("\u2000A").nextClean()); - assertEquals("em quad", '\u2001', new JSONTokener("\u2001A").nextClean()); - assertEquals("en space", '\u2002', new JSONTokener("\u2002A").nextClean()); - assertEquals("em space", '\u2003', new JSONTokener("\u2003A").nextClean()); - assertEquals("three-per-em space", '\u2004', new JSONTokener("\u2004A").nextClean()); - assertEquals("four-per-em space", '\u2005', new JSONTokener("\u2005A").nextClean()); - assertEquals("six-per-em space", '\u2006', new JSONTokener("\u2006A").nextClean()); - assertEquals("figure space", '\u2007', new JSONTokener("\u2007A").nextClean()); - assertEquals("punctuation space", '\u2008', new JSONTokener("\u2008A").nextClean()); - assertEquals("thin space", '\u2009', new JSONTokener("\u2009A").nextClean()); - assertEquals("hair space", '\u200a', new JSONTokener("\u200aA").nextClean()); - assertEquals("zero-width space", '\u200b', new JSONTokener("\u200bA").nextClean()); - assertEquals("left-to-right mark", '\u200e', new JSONTokener("\u200eA").nextClean()); - assertEquals("right-to-left mark", '\u200f', new JSONTokener("\u200fA").nextClean()); - assertEquals("line separator", '\u2028', new JSONTokener("\u2028A").nextClean()); - assertEquals("paragraph separator", '\u2029', new JSONTokener("\u2029A").nextClean()); - assertEquals("narrow non-breaking space", '\u202f', new JSONTokener("\u202fA").nextClean()); - assertEquals("medium mathematical space", '\u205f', new JSONTokener("\u205fA").nextClean()); - assertEquals("ideographic space", '\u3000', new JSONTokener("\u3000A").nextClean()); + public void testNextCleanRetainedWhitespace() throws JSONException { + assertNotClean("null", '\u0000'); + assertNotClean("next line", '\u0085'); + assertNotClean("non-breaking space", '\u00a0'); + assertNotClean("ogham space mark", '\u1680'); + assertNotClean("mongolian vowel separator", '\u180e'); + assertNotClean("en quad", '\u2000'); + assertNotClean("em quad", '\u2001'); + assertNotClean("en space", '\u2002'); + assertNotClean("em space", '\u2003'); + assertNotClean("three-per-em space", '\u2004'); + assertNotClean("four-per-em space", '\u2005'); + assertNotClean("six-per-em space", '\u2006'); + assertNotClean("figure space", '\u2007'); + assertNotClean("punctuation space", '\u2008'); + assertNotClean("thin space", '\u2009'); + assertNotClean("hair space", '\u200a'); + assertNotClean("zero-width space", '\u200b'); + assertNotClean("left-to-right mark", '\u200e'); + assertNotClean("right-to-left mark", '\u200f'); + assertNotClean("line separator", '\u2028'); + assertNotClean("paragraph separator", '\u2029'); + assertNotClean("narrow non-breaking space", '\u202f'); + assertNotClean("medium mathematical space", '\u205f'); + assertNotClean("ideographic space", '\u3000'); + assertNotClean("line tabulation", '\u000b'); + assertNotClean("form feed", '\u000c'); + assertNotClean("information separator 4", '\u001c'); + assertNotClean("information separator 3", '\u001d'); + assertNotClean("information separator 2", '\u001e'); + assertNotClean("information separator 1", '\u001f'); + } + + private void assertNotClean(String name, char c) throws JSONException { + assertEquals("The character " + name + " is not whitespace according to the JSON spec.", + c, new JSONTokener(new String(new char[] { c, 'A' })).nextClean()); } public void testNextString() throws JSONException { @@ -374,6 +391,7 @@ public class JSONTokenerTest extends TestCase { try { new JSONTokener("abc\\u002\"").nextString('"'); fail(); + } catch (NumberFormatException e) { } catch (JSONException e) { } try { @@ -433,15 +451,6 @@ public class JSONTokenerTest extends TestCase { assertEquals("ABC", tokener.nextTo("\n")); assertEquals("", tokener.nextTo("\n")); - // Bogus behaviour: the tokener stops after \0 always - tokener = new JSONTokener(" \0\t \fABC \n DEF"); - assertEquals("", tokener.nextTo("D")); - assertEquals('\t', tokener.next()); - assertEquals("ABC", tokener.nextTo("D")); - tokener = new JSONTokener("ABC\0DEF"); - assertEquals("ABC", tokener.nextTo("\0")); - assertEquals("DEF", tokener.nextTo("\0")); - tokener = new JSONTokener(""); try { tokener.nextTo(null); @@ -450,6 +459,32 @@ public class JSONTokenerTest extends TestCase { } } + public void testNextToTrimming() { + assertEquals("ABC", new JSONTokener("\t ABC \tDEF").nextTo("DE")); + assertEquals("ABC", new JSONTokener("\t ABC \tDEF").nextTo('D')); + } + + public void testNextToTrailing() { + assertEquals("ABC DEF", new JSONTokener("\t ABC DEF \t").nextTo("G")); + assertEquals("ABC DEF", new JSONTokener("\t ABC DEF \t").nextTo('G')); + } + + public void testNextToDoesntStopOnNull() { + String message = "nextTo() shouldn't stop after \\0 characters"; + JSONTokener tokener = new JSONTokener(" \0\t \fABC \n DEF"); + assertEquals(message, "ABC", tokener.nextTo("D")); + assertEquals(message, '\n', tokener.next()); + assertEquals(message, "", tokener.nextTo("D")); + } + + public void testNextToConsumesNull() { + String message = "nextTo shouldn't consume \\0."; + JSONTokener tokener = new JSONTokener("ABC\0DEF"); + assertEquals(message, "ABC", tokener.nextTo("\0")); + assertEquals(message, '\0', tokener.next()); + assertEquals(message, "DEF", tokener.nextTo("\0")); + } + public void testSkipPast() { JSONTokener tokener = new JSONTokener("ABCDEF"); tokener.skipPast("ABC"); @@ -509,11 +544,6 @@ public class JSONTokenerTest extends TestCase { tokener.skipTo('A'); assertEquals('F', tokener.next()); - tokener = new JSONTokener("ABC\0DEF"); - tokener.skipTo('F'); - // bogus behaviour: skipTo gives up when it sees '\0' - assertEquals('A', tokener.next()); - tokener = new JSONTokener("ABC\nDEF"); tokener.skipTo('F'); assertEquals('F', tokener.next()); @@ -527,6 +557,12 @@ public class JSONTokenerTest extends TestCase { assertEquals('D', tokener.next()); } + public void testSkipToStopsOnNull() { + JSONTokener tokener = new JSONTokener("ABC\0DEF"); + tokener.skipTo('F'); + assertEquals("skipTo shouldn't stop when it sees '\\0'", 'F', tokener.next()); + } + public void testDehexchar() { assertEquals( 0, JSONTokener.dehexchar('0')); assertEquals( 1, JSONTokener.dehexchar('1')); @@ -558,8 +594,4 @@ public class JSONTokenerTest extends TestCase { assertEquals("dehexchar " + c, -1, JSONTokener.dehexchar((char) c)); } } - - public void testNextValue() { - fail("TODO"); - } } diff --git a/json/src/test/java/org/json/ParsingTest.java b/json/src/test/java/org/json/ParsingTest.java new file mode 100644 index 0000000..16b9116 --- /dev/null +++ b/json/src/test/java/org/json/ParsingTest.java @@ -0,0 +1,260 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.json; + +import junit.framework.TestCase; +import junit.framework.AssertionFailedError; + +import java.util.*; + +public class ParsingTest extends TestCase { + + public void testParsingNoObjects() { + try { + new JSONTokener("").nextValue(); + fail(); + } catch (JSONException e) { + } + } + + public void testParsingLiterals() throws JSONException { + assertParsed(Boolean.TRUE, "true"); + assertParsed(Boolean.FALSE, "false"); + assertParsed(JSONObject.NULL, "null"); + assertParsed(JSONObject.NULL, "NULL"); + assertParsed(Boolean.FALSE, "False"); + assertParsed(Boolean.TRUE, "truE"); + } + + public void testParsingQuotedStrings() throws JSONException { + assertParsed("abc", "\"abc\""); + assertParsed("123", "\"123\""); + assertParsed("foo\nbar", "\"foo\\nbar\""); + assertParsed("foo bar", "\"foo\\u0020bar\""); + assertParsed("\"{}[]/\\:,=;#", "\"\\\"{}[]/\\\\:,=;#\""); + } + + public void testParsingSingleQuotedStrings() throws JSONException { + assertParsed("abc", "'abc'"); + assertParsed("123", "'123'"); + assertParsed("foo\nbar", "'foo\\nbar'"); + assertParsed("foo bar", "'foo\\u0020bar'"); + assertParsed("\"{}[]/\\:,=;#", "'\\\"{}[]/\\\\:,=;#'"); + } + + public void testParsingUnquotedStrings() throws JSONException { + assertParsed("abc", "abc"); + assertParsed("123abc", "123abc"); + assertParsed("123e0x", "123e0x"); + assertParsed("123e", "123e"); + assertParsed("123ee21", "123ee21"); + assertParsed("0xFFFFFFFFFFFFFFFFF", "0xFFFFFFFFFFFFFFFFF"); + } + + /** + * Unfortunately the original implementation attempts to figure out what + * Java number type best suits an input value. + */ + public void testParsingNumbersThatAreBestRepresentedAsLongs() throws JSONException { + assertParsed(9223372036854775807L, "9223372036854775807"); + assertParsed(9223372036854775806L, "9223372036854775806"); + assertParsed(-9223372036854775808L, "-9223372036854775808"); + assertParsed(-9223372036854775807L, "-9223372036854775807"); + } + + public void testParsingNumbersThatAreBestRepresentedAsIntegers() throws JSONException { + assertParsed(0, "0"); + assertParsed(5, "5"); + assertParsed(-2147483648, "-2147483648"); + assertParsed(2147483647, "2147483647"); + } + + public void testParsingNegativeZero() throws JSONException { + assertParsed(0, "-0"); + } + + public void testParsingIntegersWithAdditionalPrecisionYieldDoubles() throws JSONException { + assertParsed(1d, "1.00"); + assertParsed(1d, "1.0"); + assertParsed(0d, "0.0"); + assertParsed(-0d, "-0.0"); + } + + public void testParsingNumbersThatAreBestRepresentedAsDoubles() throws JSONException { + assertParsed(9.223372036854776E18, "9223372036854775808"); + assertParsed(-9.223372036854776E18, "-9223372036854775809"); + assertParsed(1.7976931348623157E308, "1.7976931348623157e308"); + assertParsed(2.2250738585072014E-308, "2.2250738585072014E-308"); + assertParsed(4.9E-324, "4.9E-324"); + assertParsed(4.9E-324, "4.9e-324"); + } + + public void testParsingOctalNumbers() throws JSONException { + assertParsed(5, "05"); + assertParsed(8, "010"); + assertParsed(1046, "02026"); + } + + public void testParsingHexNumbers() throws JSONException { + assertParsed(5, "0x5"); + assertParsed(16, "0x10"); + assertParsed(8230, "0x2026"); + assertParsed(180150010, "0xABCDEFA"); + assertParsed(2077093803, "0x7BCDEFAB"); + } + + public void testParsingLargeHexValues() throws JSONException { + assertParsed(Integer.MAX_VALUE, "0x7FFFFFFF"); + String message = "Hex values are parsed as Strings if their signed " + + "value is greater than Integer.MAX_VALUE."; + assertParsed(message, 0x80000000L, "0x80000000"); + } + + public void test64BitHexValues() throws JSONException { + assertParsed("Large hex longs shouldn't be yield ints or strings", + -1L, "0xFFFFFFFFFFFFFFFF"); + } + + public void testParsingWithCommentsAndWhitespace() throws JSONException { + assertParsed("baz", " // foo bar \n baz"); + assertParsed(5, " /* foo bar \n baz */ 5"); + assertParsed(5, " /* foo bar \n baz */ 5 // quux"); + assertParsed(5, " 5 "); + assertParsed(5, " 5 \r\n\t "); + assertParsed(5, "\r\n\t 5 "); + } + + public void testParsingArrays() throws JSONException { + assertParsed(array(), "[]"); + assertParsed(array(5, 6, true), "[5,6,true]"); + assertParsed(array(5, 6, array()), "[5,6,[]]"); + assertParsed(array(5, 6, 7), "[5;6;7]"); + assertParsed(array(5, 6, 7), "[5 , 6 \t; \r\n 7\n]"); + assertParsed(array(5, 6, 7, null), "[5,6,7,]"); + assertParsed(array(null, null), "[,]"); + assertParsed(array(5, null, null, null, 5), "[5,,,,5]"); + assertParsed(array(null, 5), "[,5]"); + assertParsed(array(null, null, null), "[,,]"); + assertParsed(array(null, null, null, 5), "[,,,5]"); + } + + public void testParsingObjects() throws JSONException { + assertParsed(object("foo", 5), "{\"foo\": 5}"); + assertParsed(object("foo", 5), "{foo: 5}"); + assertParsed(object("foo", 5, "bar", "baz"), "{\"foo\": 5, \"bar\": \"baz\"}"); + assertParsed(object("foo", 5, "bar", "baz"), "{\"foo\": 5; \"bar\": \"baz\"}"); + assertParsed(object("foo", 5, "bar", "baz"), "{\"foo\"= 5; \"bar\"= \"baz\"}"); + assertParsed(object("foo", 5, "bar", "baz"), "{\"foo\"=> 5; \"bar\"=> \"baz\"}"); + assertParsed(object("foo", object(), "bar", array()), "{\"foo\"=> {}; \"bar\"=> []}"); + assertParsed(object("foo", object("foo", array(5, 6))), "{\"foo\": {\"foo\": [5, 6]}}"); + assertParsed(object("foo", object("foo", array(5, 6))), "{\"foo\":\n\t{\t \"foo\":[5,\r6]}}"); + } + + public void testSyntaxProblemUnterminatedObject() { + assertParseFail("{"); + assertParseFail("{\"foo\""); + assertParseFail("{\"foo\":"); + assertParseFail("{\"foo\":bar"); + assertParseFail("{\"foo\":bar,"); + assertParseFail("{\"foo\":bar,\"baz\""); + assertParseFail("{\"foo\":bar,\"baz\":"); + assertParseFail("{\"foo\":bar,\"baz\":true"); + assertParseFail("{\"foo\":bar,\"baz\":true,"); + } + + public void testSyntaxProblemEmptyString() { + assertParseFail(""); + } + + public void testSyntaxProblemUnterminatedArray() { + assertParseFail("["); + assertParseFail("[,"); + assertParseFail("[,,"); + assertParseFail("[true"); + assertParseFail("[true,"); + assertParseFail("[true,,"); + } + + public void testSyntaxProblemMalformedObject() { + assertParseFail("{:}"); + assertParseFail("{\"key\":}"); + assertParseFail("{:true}"); + assertParseFail("{\"key\":true:}"); + assertParseFail("{null:true}"); + assertParseFail("{true:true}"); + assertParseFail("{0xFF:true}"); + } + + private void assertParseFail(String malformedJson) { + try { + new JSONTokener(malformedJson).nextValue(); + fail("Successfully parsed: \"" + malformedJson + "\""); + } catch (JSONException e) { + } catch (StackOverflowError e) { + fail("Stack overflowed on input: \"" + malformedJson + "\""); + } + } + + private JSONArray array(Object... elements) { + return new JSONArray(Arrays.asList(elements)); + } + + private JSONObject object(Object... keyValuePairs) throws JSONException { + JSONObject result = new JSONObject(); + for (int i = 0; i < keyValuePairs.length; i+=2) { + result.put((String) keyValuePairs[i], keyValuePairs[i+1]); + } + return result; + } + + private void assertParsed(String message, Object expected, String json) throws JSONException { + Object actual = new JSONTokener(json).nextValue(); + actual = canonicalize(actual); + expected = canonicalize(expected); + assertEquals("For input \"" + json + "\" " + message, expected, actual); + } + + private void assertParsed(Object expected, String json) throws JSONException { + assertParsed("", expected, json); + } + + /** + * Since they don't implement equals or hashCode properly, this recursively + * replaces JSONObjects with an equivalent HashMap, and JSONArrays with the + * equivalent ArrayList. + */ + private Object canonicalize(Object input) throws JSONException { + if (input instanceof JSONArray) { + JSONArray array = (JSONArray) input; + List<Object> result = new ArrayList<Object>(); + for (int i = 0; i < array.length(); i++) { + result.add(canonicalize(array.opt(i))); + } + return result; + } else if (input instanceof JSONObject) { + JSONObject object = (JSONObject) input; + Map<String, Object> result = new HashMap<String, Object>(); + for (Iterator<?> i = object.keys(); i.hasNext(); ) { + String key = (String) i.next(); + result.put(key, canonicalize(object.get(key))); + } + return result; + } else { + return input; + } + } +} diff --git a/json/src/test/java/org/json/SelfUseTest.java b/json/src/test/java/org/json/SelfUseTest.java index e3d428b..15a0824 100644 --- a/json/src/test/java/org/json/SelfUseTest.java +++ b/json/src/test/java/org/json/SelfUseTest.java @@ -1,11 +1,11 @@ -/** +/* * Copyright (C) 2010 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -19,9 +19,9 @@ package org.json; import junit.framework.TestCase; /** - * These tests checks self use; ie. when methods delegate to non-final methods. - * For the most part we doesn't attempt to cover self-use, except in those cases - * where our clean room implementation does it. + * These tests checks self use calls. For the most part we doesn't attempt to + * cover self-use, except in those cases where our clean room implementation + * does it. * * <p>This black box test was written without inspecting the non-free org.json * sourcecode. @@ -36,6 +36,8 @@ public class SelfUseTest extends TestCase { private int arrayGetCalls = 0; private int arrayOptCalls = 0; private int arrayOptTypeCalls = 0; + private int tokenerNextCalls = 0; + private int tokenerNextValueCalls = 0; private final JSONObject object = new JSONObject() { @Override public JSONObject put(String name, Object value) throws JSONException { @@ -107,6 +109,18 @@ public class SelfUseTest extends TestCase { } }; + private final JSONTokener tokener = new JSONTokener("{\"foo\": [true]}") { + @Override public char next() { + tokenerNextCalls++; + return super.next(); + } + @Override public Object nextValue() throws JSONException { + tokenerNextValueCalls++; + return super.nextValue(); + } + }; + + public void testObjectPut() throws JSONException { object.putOpt("foo", "bar"); assertEquals(1, objectPutCalls); @@ -201,4 +215,15 @@ public class SelfUseTest extends TestCase { assertEquals(0, arrayOptTypeCalls); } + public void testNextExpecting() throws JSONException { + tokener.next('{'); + assertEquals(1, tokenerNextCalls); + tokener.next('\"'); + assertEquals(2, tokenerNextCalls); + } + + public void testNextValue() throws JSONException { + tokener.nextValue(); + assertEquals(4, tokenerNextValueCalls); + } } |