blob: e6f9f47cf6539c33932ad05e63441ce8bfa961d7 [file] [log] [blame]
/*
* 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.apache.tapestry5.json;
import java.io.ObjectStreamException;
import java.io.Serializable;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import org.apache.tapestry5.json.exceptions.JSONTypeMismatchException;
import org.apache.tapestry5.json.exceptions.JSONValueNotFoundException;
// Note: this class was written without inspecting the non-free org.json sourcecode.
/**
* 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 RuntimeException} if the requested name has no value
* or if the value cannot be coerced to the requested type.
* <li>Use <code>opt()</code> to retrieve an optional value.
* </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.
*/
public final class JSONObject extends JSONCollection implements Map<String, Object> {
private static final long serialVersionUID = 1L;
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}
* <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 Serializable() {
private static final long serialVersionUID = 1L;
@Override
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 0;
}
@Override
public String toString() {
return "null";
}
// Serialization magic: after de-serializing, it will be back to the singleton instance of NULL.
private Object readResolve() throws ObjectStreamException
{
return NULL;
}
};
private final LinkedHashMap<String, Object> nameValuePairs;
/**
* Creates a {@code JSONObject} with no name/value mappings.
*/
public JSONObject() {
nameValuePairs = new LinkedHashMap<String, Object>();
}
/**
* 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 RuntimeException if the parse fails or doesn't yield a
* {@code JSONObject}.
*/
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(JSONObject.class);
if (object instanceof JSONObject) {
this.nameValuePairs = ((JSONObject) object).nameValuePairs;
} else {
throw JSONExceptionBuilder.tokenerTypeMismatch(object, JSONType.OBJECT);
}
}
/**
* Creates a new {@code JSONObject} with name/value mappings from the JSON
* string.
*
* @param json a JSON-encoded string containing an object.
* @throws RuntimeException 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.
*
* @param copyFrom The source object.
* @param names The names of the fields to copy.
* @throws RuntimeException On internal errors. Shouldn't happen.
*/
public JSONObject(JSONObject copyFrom, String... names) {
this();
for (String name : names) {
Object value = copyFrom.opt(name);
if (value != null) {
nameValuePairs.put(name, value);
}
}
}
/**
* Returns a new JSONObject that is a shallow copy of this JSONObject.
*
* @since 5.4
*/
public JSONObject copy()
{
JSONObject dupe = new JSONObject();
dupe.nameValuePairs.putAll(nameValuePairs);
return dupe;
}
/**
* Constructs a new JSONObject using a series of String keys and object values.
* Object values should be compatible with {@link #put(String, Object)}. Keys must be strings
* (toString() will be invoked on each key).
*
* Prior to release 5.4, keysAndValues was type String...; changing it to Object... makes
* it much easier to initialize a JSONObject in a single statement, which is more readable.
*
* @since 5.2.0
*/
public JSONObject(Object... keysAndValues)
{
this();
int i = 0;
while (i < keysAndValues.length)
{
put(keysAndValues[i++].toString(), keysAndValues[i++]);
}
}
/**
* Returns the number of name/value mappings in this object.
*
* @deprecated Use {@link #size()} instead.
* @return the length of this.
*/
@Deprecated
public int length() {
return nameValuePairs.size();
}
/**
* 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 name The name of the new value.
* @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.
* @throws IllegalArgumentException if the value is an invalid double (infinite or NaN).
*/
@Override
public JSONObject put(String name, Object value) {
if (value == null) {
nameValuePairs.remove(name);
return this;
}
JSON.testValidity(value);
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;
}
/**
* 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.
*
* 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 name The name of the field to change.
* @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}.
* @return this object after mutation.
* @throws RuntimeException If the object being added is an invalid number.
*/
// TODO: Change {@code append) to {@link #append} when append is
// unhidden.
public JSONObject accumulate(String name, Object value) {
Object current = 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);
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 RuntimeException}
* will be thrown.
*
* @param name The name of the array to which the value should be appended.
* @param value The value to append.
* @return this object.
* @throws JSONTypeMismatchException if {@code name} is {@code null} or if the mapping for
* {@code name} is non-null and is not a {@link JSONArray}.
*/
public JSONObject append(String name, Object value) {
JSON.testValidity(value);
Object current = nameValuePairs.get(checkName(name));
final JSONArray array;
if (current instanceof JSONArray) {
array = (JSONArray) current;
} else if (current == null) {
JSONArray newArray = new JSONArray();
nameValuePairs.put(name, newArray);
array = newArray;
} else {
throw new JSONTypeMismatchException("JSONObject[\"" + name + "\"]", JSONType.ARRAY, current.getClass());
}
array.checkedPut(value);
return this;
}
String checkName(String name) {
if (name == null) {
throw new RuntimeException("Names must be non-null");
}
return name;
}
/**
* Returns true if this object has no mapping for {@code name} or if it has
* a mapping whose value is {@link #NULL}.
*
* @param name The name of the value to check on.
* @return true if the field doesn't exist or is null.
*/
public boolean isNull(String name) {
Object value = 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}.
*
* @deprecated use {@link #containsKey(Object)} instead
* @param name
* The name of the value to check on.
* @return true if this object has a field named {@code name}
*/
@Deprecated
public boolean has(String name) {
return containsKey(name);
}
/**
* Returns the value mapped by {@code name}, or null if no such mapping
* exists.
*
* @param name The name of the value to get.
* @return The value.
*/
public Object opt(Object name) {
return 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.
*
* @param name The name of the field we want.
* @return The selected value if it exists.
* @throws JSONValueNotFoundException if the mapping doesn't exist
* @throws JSONTypeMismatchException if the mapping cannot be coerced
* to a boolean.
*/
public boolean getBoolean(String name) {
Object object = opt(name);
if (object == null) {
throw JSONExceptionBuilder.valueNotFound(false, name, JSONType.BOOLEAN);
}
Boolean result = JSON.toBoolean(object);
if (result == null) {
throw JSONExceptionBuilder.typeMismatch(false, name, object, JSONType.BOOLEAN);
}
return result;
}
/**
* Returns the value to which the specified key is mapped and a boolean, or
* {@code defaultValue} if this JSONObject contains no mapping for the key.
*
* @param name the key whose associated value is to be returned
* @param defaultValue the default mapping of the key
* @return the value to which the specified key is mapped, or
* {@code defaultValue} if this JSONObject contains no mapping for the key
* @throws JSONTypeMismatchException if the mapping cannot be coerced
* to a boolean.
* @since 5.7
*/
public boolean getBooleanOrDefault(String name, boolean defaultValue)
{
Object object = opt(name);
if (object == null)
{
return defaultValue;
}
Boolean result = JSON.toBoolean(object);
if (result == null)
{
throw JSONExceptionBuilder.typeMismatch(false, name, object, JSONType.BOOLEAN);
}
return result;
}
/**
* Returns the value mapped by {@code name} if it exists and is a double or
* can be coerced to a double, or throws otherwise.
*
* @param name The name of the field we want.
* @return The selected value if it exists.
* @throws JSONValueNotFoundException if the mapping doesn't exist
* @throws JSONTypeMismatchException if the mapping cannot be coerced
* to a double.
*/
public double getDouble(String name) {
Object object = opt(name);
if (object == null) {
throw JSONExceptionBuilder.valueNotFound(false, name, JSONType.NUMBER);
}
Double result = JSON.toDouble(object);
if (result == null) {
throw JSONExceptionBuilder.typeMismatch(false, name, object, JSONType.NUMBER);
}
return result;
}
/**
* Returns the value mapped by {@code name} if it exists and is an int or
* can be coerced to an int, or throws otherwise.
*
* @param name The name of the field we want.
* @return The selected value if it exists.
* @throws JSONValueNotFoundException if the mapping doesn't exist
* @throws JSONTypeMismatchException if the mapping cannot be coerced
* to an int.
*/
public int getInt(String name) {
Object object = opt(name);
if (object == null) {
throw JSONExceptionBuilder.valueNotFound(false, name, JSONType.NUMBER);
}
Integer result = JSON.toInteger(object);
if (result == null) {
throw JSONExceptionBuilder.typeMismatch(false, name, object, JSONType.NUMBER);
}
return result;
}
/**
* Returns the value to which the specified key is mapped and an int, or
* {@code defaultValue} if this JSONObject contains no mapping for the key.
*
* @param name the key whose associated value is to be returned
* @param defaultValue the default mapping of the key
* @return the value to which the specified key is mapped, or
* {@code defaultValue} if this JSONObject contains no mapping for the key
* @throws JSONTypeMismatchException if the mapping cannot be coerced
* to an int.
* @since 5.7
*/
public int getIntOrDefault(String name, int defaultValue)
{
Object object = opt(name);
if (object == null)
{
return defaultValue;
}
Integer result = JSON.toInteger(object);
if (result == null)
{
throw JSONExceptionBuilder.typeMismatch(false, name, object, JSONType.NUMBER);
}
return result;
}
/**
* Returns the value mapped by {@code name} if it exists and is a long or
* can be coerced to a long, or throws otherwise.
* Note that JSON represents numbers as doubles,
*
* so this is <a href="#lossy">lossy</a>; use strings to transfer numbers
* via JSON without loss.
*
* @param name The name of the field that we want.
* @return The value of the field.
* @throws JSONValueNotFoundException if the mapping doesn't exist
* @throws JSONTypeMismatchException if the mapping cannot be coerced
* to a long.
*/
public long getLong(String name) {
Object object = opt(name);
if (object == null) {
throw JSONExceptionBuilder.valueNotFound(false, name, JSONType.NUMBER);
}
Long result = JSON.toLong(object);
if (result == null) {
throw JSONExceptionBuilder.typeMismatch(false, name, object, JSONType.NUMBER);
}
return result;
}
/**
* Returns the value to which the specified key is mapped and a long, or
* {@code defaultValue} if this JSONObject contains no mapping for the key.
*
* @param name the key whose associated value is to be returned
* @param defaultValue the default mapping of the key
* @return the value to which the specified key is mapped, or
* {@code defaultValue} if this JSONObject contains no mapping for the key
* @throws JSONTypeMismatchException if the mapping cannot be coerced
* to a long.
* @since 5.7
*/
public long getLongOrDefault(String name, int defaultValue)
{
Object object = opt(name);
if (object == null)
{
return defaultValue;
}
Long result = JSON.toLong(object);
if (result == null)
{
throw JSONExceptionBuilder.typeMismatch(false, name, object, JSONType.NUMBER);
}
return result;
}
/**
* Returns the value mapped by {@code name} if it exists, coercing it if
* necessary, or throws if no such mapping exists.
*
* @param name The name of the field we want.
* @return The value of the field.
* @throws JSONValueNotFoundException if the mapping doesn't exist
* @throws JSONTypeMismatchException if the mapping cannot be coerced
* to String
*/
public String getString(String name) {
Object object = opt(name);
if (object == null) {
throw JSONExceptionBuilder.valueNotFound(false, name, JSONType.STRING);
}
String result = JSON.toString(object);
if (result == null) {
throw JSONExceptionBuilder.typeMismatch(false, name, object, JSONType.STRING);
}
return result;
}
/**
* Returns the value to which the specified key is mapped and a string, or
* {@code defaultValue} if this JSONObject contains no mapping for the key.
*
* @param name the key whose associated value is to be returned
* @param defaultValue the default mapping of the key
* @return the value to which the specified key is mapped, or
* {@code defaultValue} if this JSONObject contains no mapping for the key
* @throws JSONTypeMismatchException if the mapping cannot be coerced
* to a string.
* @since 5.7
*/
public String getStringOrDefault(String name, String defaultValue)
{
Object object = opt(name);
if (object == null)
{
return defaultValue;
}
String result = JSON.toString(object);
if (result == null)
{
throw JSONExceptionBuilder.typeMismatch(false, name, object, JSONType.STRING);
}
return result;
}
/**
* Returns the value mapped by {@code name} if it exists and is a {@code
* JSONArray}, or throws otherwise.
*
* @param name The field we want to get.
* @return The value of the field (if it is a JSONArray.
* @throws JSONValueNotFoundException if the mapping doesn't exist
* @throws JSONTypeMismatchException if the mapping is not a {@code
* JSONArray}.
*/
public JSONArray getJSONArray(String name) {
Object object = opt(name);
if (object == null) {
throw JSONExceptionBuilder.valueNotFound(false, name, JSONType.ARRAY);
}
if (object instanceof JSONArray) {
return (JSONArray) object;
} else {
throw JSONExceptionBuilder.typeMismatch(false, name, object, JSONType.ARRAY);
}
}
/**
* Returns the value to which the specified key is mapped and a JSONArray, or
* {@code defaultValue} if this JSONObject contains no mapping for the key.
*
* @param name the key whose associated value is to be returned
* @param defaultValue the default mapping of the key
* @return the value to which the specified key is mapped, or
* {@code defaultValue} if this JSONObject contains no mapping for the key
* @throws JSONTypeMismatchException if the mapping cannot be coerced
* to a JSONArray.
* @since 5.7
*/
public JSONArray getJSONArrayOrDefault(String name, JSONArray defaultValue)
{
Object object = opt(name);
if (object == null)
{
return defaultValue;
}
if (object instanceof JSONArray) {
return (JSONArray) object;
} else {
throw JSONExceptionBuilder.typeMismatch(false, name, object, JSONType.ARRAY);
}
}
/**
* Returns the value mapped by {@code name} if it exists and is a {@code
* JSONObject}, or throws otherwise.
*
* @param name The name of the field that we want.
* @return a specified field value (if it is a JSONObject)
* @throws JSONValueNotFoundException if the mapping doesn't exist
* @throws JSONTypeMismatchException if the mapping is not a {@code
* JSONObject}.
*/
public JSONObject getJSONObject(String name) {
Object object = opt(name);
if (object == null) {
throw JSONExceptionBuilder.valueNotFound(false, name, JSONType.OBJECT);
}
if (object instanceof JSONObject) {
return (JSONObject) object;
} else {
throw JSONExceptionBuilder.typeMismatch(false, name, object, JSONType.OBJECT);
}
}
/**
* Returns the value to which the specified key is mapped and a JSONObject, or
* {@code defaultValue} if this map contains no mapping for the key.
*
* @param name the key whose associated value is to be returned
* @param defaultValue the default mapping of the key
* @return the value to which the specified key is mapped, or
* {@code defaultValue} if this map contains no mapping for the key
* @throws JSONTypeMismatchException if the mapping cannot be coerced
* to a JSONObject.
* @since 5.7
*/
public JSONObject getJSONObjectOrDefault(String name, JSONObject defaultValue)
{
Object object = opt(name);
if (object == null)
{
return defaultValue;
}
if (object instanceof JSONObject) {
return (JSONObject) object;
} else {
throw JSONExceptionBuilder.typeMismatch(false, name, object, JSONType.OBJECT);
}
}
/**
* Returns the set of {@code String} names in this object. The returned set
* is a view of the keys in this object. {@link Set#remove(Object)} will remove
* the corresponding mapping from this object and set iterator behaviour
* is undefined if this object is modified after it is returned.
*
* See {@link #keys()}.
*
* @return The names in this object.
*/
public Set<String> keys() {
return nameValuePairs.keySet();
}
/**
* Returns an array containing the string names in this object. This method
* returns null if this object contains no mappings.
*
* @return the names.
*/
public JSONArray names() {
return nameValuePairs.isEmpty()
? null
: JSONArray.from(nameValuePairs.keySet());
}
/**
* Encodes the number as a JSON string.
*
* @param number a finite value. May not be {@link Double#isNaN() NaNs} or
* {@link Double#isInfinite() infinities}.
* @return The encoded number in string form.
* @throws RuntimeException On internal errors. Shouldn't happen.
*/
public static String numberToString(Number number) {
if (number == null) {
throw new RuntimeException("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();
}
static String doubleToString(double d)
{
if (Double.isInfinite(d) || Double.isNaN(d))
{
return "null";
}
return numberToString(d);
}
/**
* 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.
* @return the quoted string.
*/
public static String quote(String data) {
if (data == null) {
return "\"\"";
}
try {
JSONStringer stringer = new JSONStringer();
stringer.open(JSONStringer.Scope.NULL, "");
stringer.string(data);
stringer.close(JSONStringer.Scope.NULL, JSONStringer.Scope.NULL, "");
return stringer.toString();
} catch (RuntimeException e) {
throw new AssertionError();
}
}
/**
* Prints the JSONObject using the session.
*
* @since 5.2.0
*/
@Override
void print(JSONPrintSession session)
{
session.printSymbol('{');
session.indent();
boolean comma = false;
for (String key : keys())
{
if (comma)
session.printSymbol(',');
session.newline();
session.printQuoted(key);
session.printSymbol(':');
printValue(session, nameValuePairs.get(key));
comma = true;
}
session.outdent();
if (comma)
session.newline();
session.printSymbol('}');
}
/**
* Prints a value (a JSONArray or JSONObject, or a value stored in an array or object) using
* the session.
*
* @since 5.2.0
*/
static void printValue(JSONPrintSession session, Object value)
{
if (value == null || value == NULL)
{
session.print("null");
return;
}
if (value instanceof JSONObject)
{
((JSONObject) value).print(session);
return;
}
if (value instanceof JSONArray)
{
((JSONArray) value).print(session);
return;
}
if (value instanceof JSONString)
{
String printValue = ((JSONString) value).toJSONString();
session.print(printValue);
return;
}
if (value instanceof Number)
{
String printValue = numberToString((Number) value);
session.print(printValue);
return;
}
if (value instanceof Boolean)
{
session.print(value.toString());
return;
}
// Otherwise it really should just be a string. Nothing else can go in.
session.printQuoted(value.toString());
}
public boolean equals(Object obj)
{
if (obj == null)
return false;
if (!(obj instanceof JSONObject))
return false;
JSONObject other = (JSONObject) obj;
return nameValuePairs.equals(other.nameValuePairs);
}
/**
* Returns a Map of the keys and values of the JSONObject. The returned map is unmodifiable.
* Note that changes to the JSONObject will be reflected in the map. In addition, null values in the JSONObject
* are represented as {@link JSONObject#NULL} in the map.
*
* @return unmodifiable map of properties and values
* @since 5.4
*/
public Map<String, Object> toMap()
{
return Collections.unmodifiableMap(nameValuePairs);
}
/**
* Navigates into a nested JSONObject, creating the JSONObject if necessary. They key must not exist,
* or must be a JSONObject.
*
* @param key
* @return the nested JSONObject
* @throws IllegalStateException
* if the current value for the key is not null and not JSONObject
*/
public JSONObject in(String key)
{
assert key != null;
Object nested = nameValuePairs.get(key);
if (nested != null && !(nested instanceof JSONObject))
{
throw new IllegalStateException(String.format("JSONObject[%s] is not a JSONObject.", quote(key)));
}
if (nested == null)
{
nested = new JSONObject();
nameValuePairs.put(key, nested);
}
return (JSONObject) nested;
}
/**
* Returns the number of key-value mappings in this JSONObject.
* If it contains more than {@code Integer.MAX_VALUE} elements, returns
* {@code Integer.MAX_VALUE}.
*
* @return the number of key-value mappings in this JSONObject
* @since 5.7
*/
@Override
public int size()
{
return nameValuePairs.size();
}
/**
* Returns {@code true} if this JSONObject contains no key-value mappings.
*
* @return {@code true} if this JSONObject contains no key-value mappings
* @since 5.7
*/
@Override
public boolean isEmpty()
{
return nameValuePairs.isEmpty();
}
/**
* Returns {@code true} if this JSONObject contains a mapping for the specified
* key.
*
* @param key
* key whose presence in this map is to be tested
* @return {@code true} if this map contains a mapping for the specified
* key
* @since 5.7
*/
@Override
public boolean containsKey(Object key)
{
return nameValuePairs.containsKey(key);
}
/**
* Returns {@code true} if this JSONObject maps one or more keys to the
* specified value.
*
* @param value value whose presence in this map is to be tested
* @return {@code true} if this JSONObject maps one or more keys to the
* specified value
* @since 5.7
*/
@Override
public boolean containsValue(Object value)
{
return nameValuePairs.containsValue(value);
}
/**
* Returns the value mapped by {@code name}, or throws if no such mapping exists.
*
* @param name The name of the value to get.
* @return The value.
* @throws JSONValueNotFoundException if no such mapping exists.
*/ @Override
public Object get(Object name)
{
Object result = nameValuePairs.get(name);
if (result == null) {
throw JSONExceptionBuilder.valueNotFound(false, name, JSONType.ANY);
}
return result;
}
/**
* Returns the value to which the specified key is mapped, or
* {@code defaultValue} if this JSONObject contains no mapping for the key.
*
* @param key the key whose associated value is to be returned
* @param defaultValue the default mapping of the key
* @return the value to which the specified key is mapped, or
* {@code defaultValue} if this JSONObject contains no mapping for the key
* @since 5.7
*/
@Override
public Object getOrDefault(Object key, Object defaultValue)
{
Object value = opt(key);
return value == null ? defaultValue : value;
}
/**
* Removes the named mapping if it exists; does nothing otherwise.
*
* @param name The name of the mapping to remove.
* @return the value previously mapped by {@code name}, or null if there was
* no such mapping.
*/
@Override
public Object remove(Object name)
{
return nameValuePairs.remove(name);
}
/**
* Invokes {@link #put(String, Object)} for each value from the map.
*
* @param newProperties
* to add to this JSONObject
* @since 5.7
*/
@Override
public void putAll(Map<? extends String, ? extends Object> newProperties)
{
assert newProperties != null;
for (Map.Entry<? extends String, ? extends Object> e : newProperties.entrySet())
{
put(e.getKey(), e.getValue());
}
}
/**
* Removes all of the mappings from this JSONObject.
*
* @since 5.7
*/
@Override
public void clear()
{
nameValuePairs.clear();
}
/**
* Returns a {@link Set} view of the keys contained in this JSONObject.
* The set is backed by the JSONObject, so changes to the map are
* reflected in the set, and vice-versa.
*
* @return a set view of the keys contained in this JSONObject
* @since 5.7
*/
@Override
public Set<String> keySet()
{
return nameValuePairs.keySet();
}
/**
* Returns a {@link Collection} view of the values contained in this JSONObject.
* The collection is backed by the JSONObject, so changes to the map are
* reflected in the collection, and vice-versa.
*
* @return a collection view of the values contained in this JSONObject
* @since 5.7
*/
@Override
public Collection<Object> values()
{
return nameValuePairs.values();
}
/**
* Returns a {@link Set} view of the mappings contained in this JSONObject.
* The set is backed by the JSONObject, so changes to the map are
* reflected in the set, and vice-versa.
*
* @return a set view of the mappings contained in this JSONObject
* @since 5.7
*/
@Override
public Set<Entry<String, Object>> entrySet()
{
return nameValuePairs.entrySet();
}
}