blob: 870d46f7ec94071350ce83cebb5e23ca48cba28b [file] [log] [blame]
/*
* Copyright (c) 2005-2010 The Android Open Source Project
* Copyright (c) 2015-2017 BSI Business Systems Integration AG
*
* 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.
*
* Contributors:
* The Android Open Source Project - initial implementation
* BSI Business Systems Integration AG - changes and improvements
*/
package org.json;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Set;
import org.json.JSONStringer.Scope;
// Note: this class was written without inspecting the non-free org.json sourcecode.
/*
* Changes to the original code:
* -----------------------------
* - Applied Scout code formatting rules
* - Changed getString()/optString() to return null for JSON values "null" instead of the string "null".
* - Replaced task markers from the original source code with 'TO.DO'.
* - Suppress sonar warnings where necessary
*
* Copyright (c) 2015 BSI Business Systems Integration AG.
*/
/**
* A modifiable set of name/value mappings. Names are unique, non-null strings. Values may be any mix of
* {@link JSONObject JSONObjects}, {@link JSONArray JSONArrays}, Strings, Booleans, Integers, Longs, Doubles or
* {@link #NULL}. Values may not be {@code null}, {@link Double#isNaN() NaNs}, {@link Double#isInfinite() infinities},
* or of any type not listed here.
* <p>
* This class can coerce values to another type when requested.
* <ul>
* <li>When the requested type is a boolean, strings will be coerced using a case-insensitive comparison to "true" and
* "false".
* <li>When the requested type is a double, other {@link Number} types will be coerced using {@link Number#doubleValue()
* doubleValue}. Strings that can be coerced using {@link Double#valueOf(String)} will be.
* <li>When the requested type is an int, other {@link Number} types will be coerced using {@link Number#intValue()
* intValue}. Strings that can be coerced using {@link Double#valueOf(String)} will be, and then cast to int.
* <li><a name="lossy">When the requested type is a long, other {@link Number} types will be coerced using
* {@link Number#longValue() longValue}. Strings that can be coerced using {@link Double#valueOf(String)} will be, and
* then cast to long. This two-step conversion is lossy for very large values. For example, the string
* "9223372036854775806" yields the long 9223372036854775807.</a>
* <li>When the requested type is a String, other non-null values will be coerced using {@link String#valueOf(Object)}.
* Although null cannot be coerced, the sentinel value {@link JSONObject#NULL} is coerced to the string "null".
* </ul>
* <p>
* This class can look up both mandatory and optional values:
* <ul>
* <li>Use <code>get<i>Type</i>()</code> to retrieve a mandatory value. This fails with a {@code JSONException} if the
* requested name has no value or if the value cannot be coerced to the requested type.
* <li>Use <code>opt<i>Type</i>()</code> to retrieve an optional value. This returns a system- or user-supplied default
* if the requested name has no value or if the value cannot be coerced to the requested type.
* </ul>
* <p>
* <strong>Warning:</strong> this class represents null in two incompatible ways: the standard Java {@code null}
* reference, and the sentinel value {@link JSONObject#NULL}. In particular, calling {@code put(name, null)} removes the
* named entry from the object but {@code put(name, JSONObject.NULL)} stores an entry whose value is
* {@code JSONObject.NULL}.
* <p>
* Instances of this class are not thread safe. Although this class is nonfinal, it was not designed for inheritance and
* should not be subclassed. In particular, self-use by overrideable methods is not specified. See <i>Effective Java</i>
* Item 17, "Design and Document or inheritance or else prohibit it" for further information.
*/
public class JSONObject {
private static final Double NEGATIVE_ZERO = -0d;
/**
* A sentinel value used to explicitly define a name with no value. Unlike {@code null}, names with this value:
* <ul>
* <li>show up in the {@link #names} array
* <li>show up in the {@link #keys} iterator
* <li>return {@code true} for {@link #has(String)}
* <li>do not throw on {@link #get(String)}
* <li>are included in the encoded JSON string.
* </ul>
* <p>
* This value violates the general contract of {@link Object#equals} by returning true when compared to {@code null}.
* Its {@link #toString} method returns "null".
*/
public static final Object NULL = new Object() {
@Override
@SuppressWarnings({"squid:S1206", "findbugs:EQ_UNUSUAL", "EqualsWhichDoesntCheckParameterClass"}) // hashCode() cannot be implemented for value <code>null</code>.
public boolean equals(Object o) {
return o == this || o == null; // API specifies this broken equals implementation
}
// at least make the broken equals(null) consistent with Objects.hashCode(null).
@Override
public int hashCode() {
return Objects.hashCode(null);
}
@Override
public String toString() {
return "null";
}
};
private final LinkedHashMap<String, Object> m_nameValuePairs;
/**
* Creates a {@code JSONObject} with no name/value mappings.
*/
public JSONObject() {
m_nameValuePairs = new LinkedHashMap<>();
}
/**
* Creates a new {@code JSONObject} by copying all name/value mappings from the given map.
*
* @param copyFrom
* a map whose keys are of type {@link String} and whose values are of supported types.
* @throws NullPointerException
* if any of the map's keys are null.
*/
@SuppressWarnings("squid:S1695")
public JSONObject(Map<?, ?> copyFrom) {
this();
for (Entry<?, ?> entry : copyFrom.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("key == null");
}
m_nameValuePairs.put(key, wrap(entry.getValue()));
}
}
/**
* Creates a new {@code JSONObject} with name/value mappings from the next object in the tokener.
*
* @param readFrom
* a tokener whose nextValue() method will yield a {@code JSONObject}.
* @throws JSONException
* if the parse fails or doesn't yield a {@code JSONObject}.
*/
public JSONObject(JSONTokener readFrom) {
/*
* Getting the parser to populate this could get tricky. Instead, just
* parse to temporary JSONObject and then steal the data from that.
*/
Object object = readFrom.nextValue();
if (object instanceof JSONObject) {
JSONObject jsonObject = (JSONObject) object;
this.m_nameValuePairs = jsonObject.m_nameValuePairs;
}
else {
throw JSON.typeMismatch(object, "JSONObject");
}
}
/**
* Creates a new {@code JSONObject} with name/value mappings from the JSON string.
*
* @param json
* a JSON-encoded string containing an object.
* @throws JSONException
* if the parse fails or doesn't yield a {@code JSONObject}.
*/
public JSONObject(String json) {
this(new JSONTokener(json));
}
/**
* Creates a new {@code JSONObject} by copying mappings for the listed names from the given object. Names that aren't
* present in {@code copyFrom} will be skipped.
*/
public JSONObject(JSONObject copyFrom, String[] names) {
this();
for (String name : names) {
Object value = copyFrom.opt(name);
if (value != null) {
m_nameValuePairs.put(name, value);
}
}
}
/**
* Returns the number of name/value mappings in this object.
*/
public int length() {
return m_nameValuePairs.size();
}
/**
* Maps {@code name} to {@code value}, clobbering any existing name/value mapping with the same name.
*
* @return this object.
*/
public JSONObject put(String name, boolean value) {
m_nameValuePairs.put(checkName(name), value);
return this;
}
/**
* Maps {@code name} to {@code value}, clobbering any existing name/value mapping with the same name.
*
* @param value
* a finite value. May not be {@link Double#isNaN() NaNs} or {@link Double#isInfinite() infinities}.
* @return this object.
*/
public JSONObject put(String name, double value) {
m_nameValuePairs.put(checkName(name), JSON.checkDouble(value));
return this;
}
/**
* Maps {@code name} to {@code value}, clobbering any existing name/value mapping with the same name.
*
* @return this object.
*/
public JSONObject put(String name, int value) {
m_nameValuePairs.put(checkName(name), value);
return this;
}
/**
* Maps {@code name} to {@code value}, clobbering any existing name/value mapping with the same name.
*
* @return this object.
*/
public JSONObject put(String name, long value) {
m_nameValuePairs.put(checkName(name), value);
return this;
}
/**
* Maps {@code name} to {@code value}, clobbering any existing name/value mapping with the same name. If the value is
* {@code null}, any existing mapping for {@code name} is removed.
*
* @param value
* a {@link JSONObject}, {@link JSONArray}, String, Boolean, Integer, Long, Double, {@link #NULL}, or
* {@code null}. May not be {@link Double#isNaN() NaNs} or {@link Double#isInfinite() infinities}.
* @return this object.
*/
public JSONObject put(String name, Object value) {
if (value == null) {
m_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());
}
m_nameValuePairs.put(checkName(name), value);
return this;
}
/**
* Equivalent to {@code put(name, value)} when both parameters are non-null; does nothing otherwise.
*/
public JSONObject putOpt(String name, Object value) {
if (name == null || value == null) {
return this;
}
return put(name, value);
}
/**
* Appends {@code value} to the array already mapped to {@code name}. If this object has no mapping for {@code name},
* this inserts a new mapping. If the mapping exists but its value is not an array, the existing and new values are
* inserted in order into a new array which is itself mapped to {@code name}. In aggregate, this allows values to be
* added to a mapping one at a time.
* <p>
* Note that {@code append(String, Object)} provides better semantics. In particular, the mapping for {@code name}
* will <b>always</b> be a {@link JSONArray}. Using {@code accumulate} will result in either a {@link JSONArray} or a
* mapping whose type is the type of {@code value} depending on the number of calls to it.
*
* @param value
* a {@link JSONObject}, {@link JSONArray}, String, Boolean, Integer, Long, Double, {@link #NULL} or null.
* May not be {@link Double#isNaN() NaNs} or {@link Double#isInfinite() infinities}.
*/
// TO.DO Change {@code append) to {@link #append} when append is
// unhidden.
public JSONObject accumulate(String name, Object value) {
Object current = m_nameValuePairs.get(checkName(name));
if (current == null) {
return put(name, value);
}
if (current instanceof JSONArray) {
JSONArray array = (JSONArray) current;
array.checkedPut(value);
}
else {
JSONArray array = new JSONArray();
array.checkedPut(current);
array.checkedPut(value);
m_nameValuePairs.put(name, array);
}
return this;
}
/**
* Appends values to the array mapped to {@code name}. A new {@link JSONArray} mapping for {@code name} will be
* inserted if no mapping exists. If the existing mapping for {@code name} is not a {@link JSONArray}, a
* {@link JSONException} will be thrown.
*
* @throws JSONException
* if {@code name} is {@code null} or if the mapping for {@code name} is non-null and is not a
* {@link JSONArray}.
*/
public JSONObject append(String name, Object value) {
Object current = m_nameValuePairs.get(checkName(name));
final JSONArray array;
if (current instanceof JSONArray) {
array = (JSONArray) current;
}
else if (current == null) {
JSONArray newArray = new JSONArray();
m_nameValuePairs.put(name, newArray);
array = newArray;
}
else {
throw new JSONException("Key " + name + " is not a JSONArray");
}
array.checkedPut(value);
return this;
}
String checkName(String name) {
if (name == null) {
throw new JSONException("Names must be non-null");
}
return name;
}
/**
* Removes the named mapping if it exists; does nothing otherwise.
*
* @return the value previously mapped by {@code name}, or null if there was no such mapping.
*/
public Object remove(String name) {
return m_nameValuePairs.remove(name);
}
/**
* Returns true if this object has no mapping for {@code name} or if it has a mapping whose value is {@link #NULL}.
*/
public boolean isNull(String name) {
Object value = m_nameValuePairs.get(name);
return value == null || value == NULL;
}
/**
* Returns true if this object has a mapping for {@code name}. The mapping may be {@link #NULL}.
*/
public boolean has(String name) {
return m_nameValuePairs.containsKey(name);
}
/**
* Returns the value mapped by {@code name}, or throws if no such mapping exists.
*
* @throws JSONException
* if no such mapping exists.
*/
public Object get(String name) {
Object result = m_nameValuePairs.get(name);
if (result == null) {
throw new JSONException("No value for " + name);
}
return result;
}
/**
* Returns the value mapped by {@code name}, or null if no such mapping exists.
*/
public Object opt(String name) {
return m_nameValuePairs.get(name);
}
/**
* Returns the value mapped by {@code name} if it exists and is a boolean or can be coerced to a boolean, or throws
* otherwise.
*
* @throws JSONException
* if the mapping doesn't exist or cannot be coerced to a boolean.
*/
public boolean getBoolean(String name) {
Object object = get(name);
Boolean result = JSON.toBoolean(object);
if (result == null) {
throw JSON.typeMismatch(name, object, "boolean");
}
return result;
}
/**
* Returns the value mapped by {@code name} if it exists and is a boolean or can be coerced to a boolean, or false
* otherwise.
*/
public boolean optBoolean(String name) {
return optBoolean(name, false);
}
/**
* Returns the value mapped by {@code name} if it exists and is a boolean or can be coerced to a boolean, or
* {@code fallback} otherwise.
*/
public boolean optBoolean(String name, boolean fallback) {
Object object = opt(name);
Boolean result = JSON.toBoolean(object);
return result != null ? result : fallback;
}
/**
* Returns the value mapped by {@code name} if it exists and is a double or can be coerced to a double, or throws
* otherwise.
*
* @throws JSONException
* if the mapping doesn't exist or cannot be coerced to a double.
*/
public double getDouble(String name) {
Object object = get(name);
Double result = JSON.toDouble(object);
if (result == null) {
throw JSON.typeMismatch(name, object, "double");
}
return result;
}
/**
* Returns the value mapped by {@code name} if it exists and is a double or can be coerced to a double, or {@code NaN}
* otherwise.
*/
public double optDouble(String name) {
return optDouble(name, Double.NaN);
}
/**
* Returns the value mapped by {@code name} if it exists and is a double or can be coerced to a double, or
* {@code fallback} otherwise.
*/
public double optDouble(String name, double fallback) {
Object object = opt(name);
Double result = JSON.toDouble(object);
return result != null ? result : fallback;
}
/**
* Returns the value mapped by {@code name} if it exists and is an int or can be coerced to an int, or throws
* otherwise.
*
* @throws JSONException
* if the mapping doesn't exist or cannot be coerced to an int.
*/
public int getInt(String name) {
Object object = get(name);
Integer result = JSON.toInteger(object);
if (result == null) {
throw JSON.typeMismatch(name, object, "int");
}
return result;
}
/**
* Returns the value mapped by {@code name} if it exists and is an int or can be coerced to an int, or 0 otherwise.
*/
public int optInt(String name) {
return optInt(name, 0);
}
/**
* Returns the value mapped by {@code name} if it exists and is an int or can be coerced to an int, or
* {@code fallback} otherwise.
*/
public int optInt(String name, int fallback) {
Object object = opt(name);
Integer result = JSON.toInteger(object);
return result != null ? result : fallback;
}
/**
* Returns the value mapped by {@code name} if it exists and is a long or can be coerced to a long, or throws
* otherwise. Note that JSON represents numbers as doubles, so this is <a href="#lossy">lossy</a>; use strings to
* transfer numbers via JSON.
*
* @throws JSONException
* if the mapping doesn't exist or cannot be coerced to a long.
*/
public long getLong(String name) {
Object object = get(name);
Long result = JSON.toLong(object);
if (result == null) {
throw JSON.typeMismatch(name, object, "long");
}
return result;
}
/**
* Returns the value mapped by {@code name} if it exists and is a long or can be coerced to a long, or 0 otherwise.
* Note that JSON represents numbers as doubles, so this is <a href="#lossy">lossy</a>; use strings to transfer
* numbers via JSON.
*/
public long optLong(String name) {
return optLong(name, 0L);
}
/**
* Returns the value mapped by {@code name} if it exists and is a long or can be coerced to a long, or
* {@code fallback} otherwise. Note that JSON represents numbers as doubles, so this is <a href="#lossy">lossy</a>;
* use strings to transfer numbers via JSON.
*/
public long optLong(String name, long fallback) {
Object object = opt(name);
Long result = JSON.toLong(object);
return result != null ? result : fallback;
}
/**
* Returns the value mapped by {@code name} if it exists, coercing it if necessary, or throws if no such mapping
* exists.
*
* @throws JSONException
* if no such mapping exists.
*/
public String getString(String name) {
Object object = get(name);
String result = (NULL.equals(object) ? null : JSON.toString(object)); // BSI
if (result == null) {
throw JSON.typeMismatch(name, object, "String");
}
return result;
}
/**
* Returns the value mapped by {@code name} if it exists, coercing it if necessary, or the empty string if no such
* mapping exists.
*/
public String optString(String name) {
return optString(name, "");
}
/**
* Returns the value mapped by {@code name} if it exists, coercing it if necessary, or {@code fallback} if no such
* mapping exists.
*/
public String optString(String name, String fallback) {
Object object = opt(name);
String result = (NULL.equals(object) ? null : JSON.toString(object)); // BSI
return result != null ? result : fallback;
}
/**
* Returns the value mapped by {@code name} if it exists and is a {@code JSONArray}, or throws otherwise.
*
* @throws JSONException
* if the mapping doesn't exist or is not a {@code JSONArray}.
*/
public JSONArray getJSONArray(String name) {
Object object = get(name);
if (object instanceof JSONArray) {
return (JSONArray) object;
}
else {
throw JSON.typeMismatch(name, object, "JSONArray");
}
}
/**
* Returns the value mapped by {@code name} if it exists and is a {@code JSONArray}, or null otherwise.
*/
public JSONArray optJSONArray(String name) {
Object object = opt(name);
return object instanceof JSONArray ? (JSONArray) object : null;
}
/**
* Returns the value mapped by {@code name} if it exists and is a {@code JSONObject}, or throws otherwise.
*
* @throws JSONException
* if the mapping doesn't exist or is not a {@code JSONObject}.
*/
public JSONObject getJSONObject(String name) {
Object object = get(name);
if (object instanceof JSONObject) {
return (JSONObject) object;
}
else {
throw JSON.typeMismatch(name, object, "JSONObject");
}
}
/**
* Returns the value mapped by {@code name} if it exists and is a {@code JSONObject}, or null otherwise.
*/
public JSONObject optJSONObject(String name) {
Object object = opt(name);
return object instanceof JSONObject ? (JSONObject) object : null;
}
/**
* Returns an array with the values corresponding to {@code names}. The array contains null for names that aren't
* mapped. This method returns null if {@code names} is either null or empty.
*/
public JSONArray toJSONArray(JSONArray names) {
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;
}
/**
* Returns an iterator of the {@code String} names in this object. The returned iterator supports
* {@link Iterator#remove() remove}, which will remove the corresponding mapping from this object. If this object is
* modified after the iterator is returned, the iterator's behavior is undefined. The order of the keys is undefined.
*/
public Iterator<String> keys() {
return m_nameValuePairs.keySet().iterator();
}
/**
* Returns the set of {@code String} names in this object. The returned set is a view of the keys in this object.
* {@link Set#remove(Object)} will remove the corresponding mapping from this object and set iterator behaviour is
* undefined if this object is modified after it is returned. See {@link #keys()}. @hide.
*/
public Set<String> keySet() {
return m_nameValuePairs.keySet();
}
/**
* Returns an array containing the string names in this object. This method returns null if this object contains no
* mappings.
*/
public JSONArray names() {
return m_nameValuePairs.isEmpty()
? null
: new JSONArray(new ArrayList<>(m_nameValuePairs.keySet()));
}
/**
* Encodes this object as a compact JSON string, such as:
*
* <pre>
* {"query":"Pizza","locations":[94043,90210]}
* </pre>
*/
@Override
@SuppressWarnings({"squid:S1166", "squid:S2225"})
public String toString() {
try {
JSONStringer stringer = new JSONStringer();
writeTo(stringer);
return stringer.toString();
}
catch (JSONException e) {
return null;
}
}
/**
* Encodes this object as a human readable JSON string for debugging, such as:
*
* <pre>
* {
* "query": "Pizza",
* "locations": [
* 94043,
* 90210
* ]
* }
* </pre>
*
* @param indentSpaces
* the number of spaces to indent for each level of nesting.
*/
public String toString(int indentSpaces) {
JSONStringer stringer = new JSONStringer(indentSpaces);
writeTo(stringer);
return stringer.toString();
}
void writeTo(JSONStringer stringer) {
stringer.object();
for (Entry<String, Object> entry : m_nameValuePairs.entrySet()) {
stringer.key(entry.getKey()).value(entry.getValue());
}
stringer.endObject();
}
/**
* Encodes the number as a JSON string.
*
* @param number
* a finite value. May not be {@link Double#isNaN() NaNs} or {@link Double#isInfinite() infinities}.
*/
@SuppressWarnings("squid:S1244")
public static String numberToString(Number number) {
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();
}
/**
* Encodes {@code data} as a JSON string. This applies quotes and any necessary character escaping.
*
* @param data
* the string to encode. Null will be interpreted as an empty string.
*/
public static String quote(String data) {
if (data == null) {
return "\"\"";
}
JSONStringer stringer = new JSONStringer();
stringer.open(Scope.NULL, "");
stringer.value(data);
stringer.close(Scope.NULL, Scope.NULL, "");
return stringer.toString();
}
/**
* Wraps the given object if necessary.
* <p>
* If the object is null, returns {@link #NULL}. If the object is a {@code JSONArray} or {@code JSONObject}, no
* wrapping is necessary. If the object is {@code NULL}, no wrapping is necessary. If the object is an array or
* {@code Collection}, returns an equivalent {@code JSONArray}. If the object is a {@code Map}, returns an equivalent
* {@code JSONObject}. If the object is a primitive wrapper type or {@code String}, returns the object. Otherwise if
* the object is from a {@code java} package, returns the result of {@code toString}. If wrapping fails, returns null.
*/
public static Object wrap(Object o) {
if (o == null) {
return NULL;
}
if (o instanceof JSONArray || o instanceof JSONObject) {
return o;
}
if (o.equals(NULL)) {
return o;
}
try {
if (o instanceof Collection) {
return new JSONArray((Collection<?>) o);
}
else if (o.getClass().isArray()) {
return new JSONArray(o);
}
if (o instanceof Map) {
return new JSONObject((Map<?, ?>) o);
}
if (o instanceof Boolean ||
o instanceof Byte ||
o instanceof Character ||
o instanceof Double ||
o instanceof Float ||
o instanceof Integer ||
o instanceof Long ||
o instanceof Short ||
o instanceof String) {
return o;
}
if (o.getClass().getPackage().getName().startsWith("java.")) {
return o.toString();
}
}
catch (Exception ignored) { // NOSONAR
}
return null;
}
}