diff options
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); + } } |