    001/*
    002 * Copyright (C) 2010 The Android Open Source Project
    003 *
    004 * Licensed under the Apache License, Version 2.0 (the "License");
    005 * you may not use this file except in compliance with the License.
    006 * You may obtain a copy of the License at
    007 *
    008 *      http://www.apache.org/licenses/LICENSE-2.0
    009 *
    010 * Unless required by applicable law or agreed to in writing, software
    011 * distributed under the License is distributed on an "AS IS" BASIS,
    012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    013 * See the License for the specific language governing permissions and
    014 * limitations under the License.
    015 */
    016
    017package org.apache.tapestry5.json;
    018
    019import java.io.ObjectStreamException;
    020import java.io.Serializable;
    021import java.util.Collections;
    022import java.util.LinkedHashMap;
    023import java.util.Map;
    024import java.util.Set;
    025
    026// Note: this class was written without inspecting the non-free org.json sourcecode.
    027
    028/**
    029 * A modifiable set of name/value mappings. Names are unique, non-null strings.
    030 * Values may be any mix of {@link JSONObject JSONObjects}, {@link JSONArray
    031 * JSONArrays}, Strings, Booleans, Integers, Longs, Doubles or {@link #NULL}.
    032 * Values may not be {@code null}, {@link Double#isNaN() NaNs}, {@link
    033 * Double#isInfinite() infinities}, or of any type not listed here.
    034 *
    035 * <p>This class can coerce values to another type when requested.
    036 * <ul>
    037 * <li>When the requested type is a boolean, strings will be coerced using a
    038 * case-insensitive comparison to "true" and "false".
    039 * <li>When the requested type is a double, other {@link Number} types will
    040 * be coerced using {@link Number#doubleValue() doubleValue}. Strings
    041 * that can be coerced using {@link Double#valueOf(String)} will be.
    042 * <li>When the requested type is an int, other {@link Number} types will
    043 * be coerced using {@link Number#intValue() intValue}. Strings
    044 * that can be coerced using {@link Double#valueOf(String)} will be,
    045 * and then cast to int.
    046 * <li><a name="lossy">When the requested type is a long, other {@link Number} types will
    047 * be coerced using {@link Number#longValue() longValue}. Strings
    048 * that can be coerced using {@link Double#valueOf(String)} will be,
    049 * and then cast to long. This two-step conversion is lossy for very
    050 * large values. For example, the string "9223372036854775806" yields the
    051 * long 9223372036854775807.</a>
    052 * <li>When the requested type is a String, other non-null values will be
    053 * coerced using {@link String#valueOf(Object)}. Although null cannot be
    054 * coerced, the sentinel value {@link JSONObject#NULL} is coerced to the
    055 * string "null".
    056 * </ul>
    057 *
    058 * <p>This class can look up both mandatory and optional values:
    059 * <ul>
    060 * <li>Use <code>get<i>Type</i>()</code> to retrieve a mandatory value. This
    061 * fails with a {@code RuntimeException} if the requested name has no value
    062 * or if the value cannot be coerced to the requested type.
    063 * <li>Use <code>opt()</code> to retrieve an optional value.
    064 * </ul>
    065 *
    066 * <p><strong>Warning:</strong> this class represents null in two incompatible
    067 * ways: the standard Java {@code null} reference, and the sentinel value {@link
    068 * JSONObject#NULL}. In particular, calling {@code put(name, null)} removes the
    069 * named entry from the object but {@code put(name, JSONObject.NULL)} stores an
    070 * entry whose value is {@code JSONObject.NULL}.
    071 *
    072 * <p>Instances of this class are not thread safe.
    073 */
    074public final class JSONObject extends JSONCollection {
    075
    076    private static final Double NEGATIVE_ZERO = -0d;
    077
    078    /**
    079     * A sentinel value used to explicitly define a name with no value. Unlike
    080     * {@code null}, names with this value:
    081     * <ul>
    082     * <li>show up in the {@link #names} array
    083     * <li>show up in the {@link #keys} iterator
    084     * <li>return {@code true} for {@link #has(String)}
    085     * <li>do not throw on {@link #get(String)}
    086     * <li>are included in the encoded JSON string.
    087     * </ul>
    088     *
    089     * <p>This value violates the general contract of {@link Object#equals} by
    090     * returning true when compared to {@code null}. Its {@link #toString}
    091     * method returns "null".
    092     */
    093    public static final Object NULL = new Serializable() {
    094
    095      private static final long serialVersionUID = 1L;
    096
    097        @SuppressWarnings("EqualsWhichDoesntCheckParameterClass")
    098        @Override
    099        public boolean equals(Object o) {
    100            return o == this || o == null; // API specifies this broken equals implementation
    101        }
    102
    103        // at least make the broken equals(null) consistent with Objects.hashCode(null).
    104        @Override
    105        public int hashCode() {
    106            return 0;
    107        }
    108
    109        @Override
    110        public String toString() {
    111            return "null";
    112        }
    113
    114        // Serialization magic: after de-serializing, it will be back to the singleton instance of NULL.
    115        private Object readResolve() throws ObjectStreamException
    116        {
    117            return NULL;
    118        }
    119
    120    };
    121
    122    private final LinkedHashMap<String, Object> nameValuePairs;
    123
    124    /**
    125     * Creates a {@code JSONObject} with no name/value mappings.
    126     */
    127    public JSONObject() {
    128        nameValuePairs = new LinkedHashMap<String, Object>();
    129    }
    130
    131    /**
    132     * Creates a new {@code JSONObject} with name/value mappings from the next
    133     * object in the tokener.
    134     *
    135     * @param readFrom a tokener whose nextValue() method will yield a
    136     *                 {@code JSONObject}.
    137     * @throws RuntimeException if the parse fails or doesn't yield a
    138     *                       {@code JSONObject}.
    139     */
    140    JSONObject(JSONTokener readFrom) {
    141        /*
    142         * Getting the parser to populate this could get tricky. Instead, just
    143         * parse to temporary JSONObject and then steal the data from that.
    144         */
    145        Object object = readFrom.nextValue(JSONObject.class);
    146        if (object instanceof JSONObject) {
    147            this.nameValuePairs = ((JSONObject) object).nameValuePairs;
    148        } else {
    149            throw JSON.typeMismatch(object, "JSONObject");
    150        }
    151    }
    152
    153    /**
    154     * Creates a new {@code JSONObject} with name/value mappings from the JSON
    155     * string.
    156     *
    157     * @param json a JSON-encoded string containing an object.
    158     * @throws RuntimeException if the parse fails or doesn't yield a {@code
    159     *                       JSONObject}.
    160     */
    161    public JSONObject(String json) {
    162        this(new JSONTokener(json));
    163    }
    164
    165    /**
    166     * Creates a new {@code JSONObject} by copying mappings for the listed names
    167     * from the given object. Names that aren't present in {@code copyFrom} will
    168     * be skipped.
    169     *
    170     * @param copyFrom The source object.
    171     * @param names    The names of the fields to copy.
    172     * @throws RuntimeException On internal errors. Shouldn't happen.
    173     */
    174    public JSONObject(JSONObject copyFrom, String... names) {
    175        this();
    176        for (String name : names) {
    177            Object value = copyFrom.opt(name);
    178            if (value != null) {
    179                nameValuePairs.put(name, value);
    180            }
    181        }
    182    }
    183
    184
    185    /**
    186     * Returns a new JSONObject that is a shallow copy of this JSONObject.
    187     *
    188     * @since 5.4
    189     */
    190    public JSONObject copy()
    191    {
    192        JSONObject dupe = new JSONObject();
    193        dupe.nameValuePairs.putAll(nameValuePairs);
    194
    195        return dupe;
    196    }
    197
    198    /**
    199     * Constructs a new JSONObject using a series of String keys and object values.
    200     * Object values sholuld be compatible with {@link #put(String, Object)}. Keys must be strings
    201     * (toString() will be invoked on each key).
    202     *
    203     * Prior to release 5.4, keysAndValues was type String...; changing it to Object... makes
    204     * it much easier to initialize a JSONObject in a single statement, which is more readable.
    205     *
    206     * @since 5.2.0
    207     */
    208    public JSONObject(Object... keysAndValues)
    209    {
    210        this();
    211
    212        int i = 0;
    213
    214        while (i < keysAndValues.length)
    215        {
    216            put(keysAndValues[i++].toString(), keysAndValues[i++]);
    217        }
    218    }
    219
    220    /**
    221     * Returns the number of name/value mappings in this object.
    222     *
    223     * @return the length of this.
    224     */
    225    public int length() {
    226        return nameValuePairs.size();
    227    }
    228
    229    /**
    230     * Maps {@code name} to {@code value}, clobbering any existing name/value
    231     * mapping with the same name. If the value is {@code null}, any existing
    232     * mapping for {@code name} is removed.
    233     *
    234     * @param name  The name of the new value.
    235     * @param value a {@link JSONObject}, {@link JSONArray}, String, Boolean,
    236     *              Integer, Long, Double, {@link #NULL}, or {@code null}. May not be
    237     *              {@link Double#isNaN() NaNs} or {@link Double#isInfinite()
    238     *              infinities}.
    239     * @return this object.
    240     * @throws RuntimeException if the value is an invalid double (infinite or NaN).
    241     */
    242    public JSONObject put(String name, Object value) {
    243        if (value == null) {
    244            nameValuePairs.remove(name);
    245            return this;
    246        }
    247        testValidity(value);
    248        if (value instanceof Number) {
    249            // deviate from the original by checking all Numbers, not just floats & doubles
    250            JSON.checkDouble(((Number) value).doubleValue());
    251        }
    252        nameValuePairs.put(checkName(name), value);
    253        return this;
    254    }
    255
    256    /**
    257     * Appends {@code value} to the array already mapped to {@code name}. If
    258     * this object has no mapping for {@code name}, this inserts a new mapping.
    259     * If the mapping exists but its value is not an array, the existing
    260     * and new values are inserted in order into a new array which is itself
    261     * mapped to {@code name}. In aggregate, this allows values to be added to a
    262     * mapping one at a time.
    263     *
    264     * Note that {@code append(String, Object)} provides better semantics.
    265     * In particular, the mapping for {@code name} will <b>always</b> be a
    266     * {@link JSONArray}. Using {@code accumulate} will result in either a
    267     * {@link JSONArray} or a mapping whose type is the type of {@code value}
    268     * depending on the number of calls to it.
    269     *
    270     * @param name  The name of the field to change.
    271     * @param value a {@link JSONObject}, {@link JSONArray}, String, Boolean,
    272     *              Integer, Long, Double, {@link #NULL} or null. May not be {@link
    273     *              Double#isNaN() NaNs} or {@link Double#isInfinite() infinities}.
    274     * @return this object after mutation.
    275     * @throws RuntimeException If the object being added is an invalid number.
    276     */
    277    // TODO: Change {@code append) to {@link #append} when append is
    278    // unhidden.
    279    public JSONObject accumulate(String name, Object value) {
    280        Object current = nameValuePairs.get(checkName(name));
    281        if (current == null) {
    282            return put(name, value);
    283        }
    284
    285        if (current instanceof JSONArray) {
    286            JSONArray array = (JSONArray) current;
    287            array.checkedPut(value);
    288        } else {
    289            JSONArray array = new JSONArray();
    290            array.checkedPut(current);
    291            array.checkedPut(value);
    292            nameValuePairs.put(name, array);
    293        }
    294        return this;
    295    }
    296
    297    /**
    298     * Appends values to the array mapped to {@code name}. A new {@link JSONArray}
    299     * mapping for {@code name} will be inserted if no mapping exists. If the existing
    300     * mapping for {@code name} is not a {@link JSONArray}, a {@link RuntimeException}
    301     * will be thrown.
    302     *
    303     * @param name  The name of the array to which the value should be appended.
    304     * @param value The value to append.
    305     * @return this object.
    306     * @throws RuntimeException if {@code name} is {@code null} or if the mapping for
    307     *                       {@code name} is non-null and is not a {@link JSONArray}.
    308     */
    309    public JSONObject append(String name, Object value) {
    310        testValidity(value);
    311        Object current = nameValuePairs.get(checkName(name));
    312
    313        final JSONArray array;
    314        if (current instanceof JSONArray) {
    315            array = (JSONArray) current;
    316        } else if (current == null) {
    317            JSONArray newArray = new JSONArray();
    318            nameValuePairs.put(name, newArray);
    319            array = newArray;
    320        } else {
    321            throw new RuntimeException("JSONObject[\"" + name + "\"] is not a JSONArray.");
    322        }
    323
    324        array.checkedPut(value);
    325
    326        return this;
    327    }
    328
    329    String checkName(String name) {
    330        if (name == null) {
    331            throw new RuntimeException("Names must be non-null");
    332        }
    333        return name;
    334    }
    335
    336    /**
    337     * Removes the named mapping if it exists; does nothing otherwise.
    338     *
    339     * @param name The name of the mapping to remove.
    340     * @return the value previously mapped by {@code name}, or null if there was
    341     * no such mapping.
    342     */
    343    public Object remove(String name) {
    344        return nameValuePairs.remove(name);
    345    }
    346
    347    /**
    348     * Returns true if this object has no mapping for {@code name} or if it has
    349     * a mapping whose value is {@link #NULL}.
    350     *
    351     * @param name The name of the value to check on.
    352     * @return true if the field doesn't exist or is null.
    353     */
    354    public boolean isNull(String name) {
    355        Object value = nameValuePairs.get(name);
    356        return value == null || value == NULL;
    357    }
    358
    359    /**
    360     * Returns true if this object has a mapping for {@code name}. The mapping
    361     * may be {@link #NULL}.
    362     *
    363     * @param name The name of the value to check on.
    364     * @return true if this object has a field named {@code name}
    365     */
    366    public boolean has(String name) {
    367        return nameValuePairs.containsKey(name);
    368    }
    369
    370    /**
    371     * Returns the value mapped by {@code name}, or throws if no such mapping exists.
    372     *
    373     * @param name The name of the value to get.
    374     * @return The value.
    375     * @throws RuntimeException if no such mapping exists.
    376     */
    377    public Object get(String name) {
    378        Object result = nameValuePairs.get(name);
    379        if (result == null) {
    380            throw new RuntimeException("JSONObject[\"" + name + "\"] not found.");
    381        }
    382        return result;
    383    }
    384
    385    /**
    386     * Returns the value mapped by {@code name}, or null if no such mapping
    387     * exists.
    388     *
    389     * @param name The name of the value to get.
    390     * @return The value.
    391     */
    392    public Object opt(String name) {
    393        return nameValuePairs.get(name);
    394    }
    395
    396    /**
    397     * Returns the value mapped by {@code name} if it exists and is a boolean or
    398     * can be coerced to a boolean, or throws otherwise.
    399     *
    400     * @param name The name of the field we want.
    401     * @return The selected value if it exists.
    402     * @throws RuntimeException if the mapping doesn't exist or cannot be coerced
    403     *                       to a boolean.
    404     */
    405    public boolean getBoolean(String name) {
    406        Object object = get(name);
    407        Boolean result = JSON.toBoolean(object);
    408        if (result == null) {
    409            throw JSON.typeMismatch(false, name, object, "Boolean");
    410        }
    411        return result;
    412    }
    413
    414    /**
    415     * Returns the value mapped by {@code name} if it exists and is a double or
    416     * can be coerced to a double, or throws otherwise.
    417     *
    418     * @param name The name of the field we want.
    419     * @return The selected value if it exists.
    420     * @throws RuntimeException if the mapping doesn't exist or cannot be coerced
    421     *                       to a double.
    422     */
    423    public double getDouble(String name) {
    424        Object object = get(name);
    425        Double result = JSON.toDouble(object);
    426        if (result == null) {
    427            throw JSON.typeMismatch(false, name, object, "number");
    428        }
    429        return result;
    430    }
    431
    432    /**
    433     * Returns the value mapped by {@code name} if it exists and is an int or
    434     * can be coerced to an int, or throws otherwise.
    435     *
    436     * @param name The name of the field we want.
    437     * @return The selected value if it exists.
    438     * @throws RuntimeException if the mapping doesn't exist or cannot be coerced
    439     *                       to an int.
    440     */
    441    public int getInt(String name) {
    442        Object object = get(name);
    443        Integer result = JSON.toInteger(object);
    444        if (result == null) {
    445            throw JSON.typeMismatch(false, name, object, "int");
    446        }
    447        return result;
    448    }
    449
    450    /**
    451     * Returns the value mapped by {@code name} if it exists and is a long or
    452     * can be coerced to a long, or throws otherwise.
    453     * Note that JSON represents numbers as doubles,
    454     *
    455     * so this is <a href="#lossy">lossy</a>; use strings to transfer numbers
    456     * via JSON without loss.
    457     *
    458     * @param name The name of the field that we want.
    459     * @return The value of the field.
    460     * @throws RuntimeException if the mapping doesn't exist or cannot be coerced
    461     *                       to a long.
    462     */
    463    public long getLong(String name) {
    464        Object object = get(name);
    465        Long result = JSON.toLong(object);
    466        if (result == null) {
    467            throw JSON.typeMismatch(false, name, object, "long");
    468        }
    469        return result;
    470    }
    471
    472    /**
    473     * Returns the value mapped by {@code name} if it exists, coercing it if
    474     * necessary, or throws if no such mapping exists.
    475     *
    476     * @param name The name of the field we want.
    477     * @return The value of the field.
    478     * @throws RuntimeException if no such mapping exists.
    479     */
    480    public String getString(String name) {
    481        Object object = get(name);
    482        String result = JSON.toString(object);
    483        if (result == null) {
    484            throw JSON.typeMismatch(false, name, object, "String");
    485        }
    486        return result;
    487    }
    488
    489    /**
    490     * Returns the value mapped by {@code name} if it exists and is a {@code
    491     * JSONArray}, or throws otherwise.
    492     *
    493     * @param name The field we want to get.
    494     * @return The value of the field (if it is a JSONArray.
    495     * @throws RuntimeException if the mapping doesn't exist or is not a {@code
    496     *                       JSONArray}.
    497     */
    498    public JSONArray getJSONArray(String name) {
    499        Object object = get(name);
    500        if (object instanceof JSONArray) {
    501            return (JSONArray) object;
    502        } else {
    503            throw JSON.typeMismatch(false, name, object, "JSONArray");
    504        }
    505    }
    506
    507    /**
    508     * Returns the value mapped by {@code name} if it exists and is a {@code
    509     * JSONObject}, or throws otherwise.
    510     *
    511     * @param name The name of the field that we want.
    512     * @return a specified field value (if it is a JSONObject)
    513     * @throws RuntimeException if the mapping doesn't exist or is not a {@code
    514     *                       JSONObject}.
    515     */
    516    public JSONObject getJSONObject(String name) {
    517        Object object = get(name);
    518        if (object instanceof JSONObject) {
    519            return (JSONObject) object;
    520        } else {
    521            throw JSON.typeMismatch(false, name, object, "JSONObject");
    522        }
    523    }
    524
    525    /**
    526     * Returns the set of {@code String} names in this object. The returned set
    527     * is a view of the keys in this object. {@link Set#remove(Object)} will remove
    528     * the corresponding mapping from this object and set iterator behaviour
    529     * is undefined if this object is modified after it is returned.
    530     *
    531     * See {@link #keys()}.
    532     *
    533     * @return The names in this object.
    534     */
    535    public Set<String> keys() {
    536        return nameValuePairs.keySet();
    537    }
    538
    539    /**
    540     * Returns an array containing the string names in this object. This method
    541     * returns null if this object contains no mappings.
    542     *
    543     * @return the names.
    544     */
    545    public JSONArray names() {
    546        return nameValuePairs.isEmpty()
    547                ? null
    548                : JSONArray.from(nameValuePairs.keySet());
    549    }
    550
    551    /**
    552     * Encodes the number as a JSON string.
    553     *
    554     * @param number a finite value. May not be {@link Double#isNaN() NaNs} or
    555     *               {@link Double#isInfinite() infinities}.
    556     * @return The encoded number in string form.
    557     * @throws RuntimeException On internal errors. Shouldn't happen.
    558     */
    559    public static String numberToString(Number number) {
    560        if (number == null) {
    561            throw new RuntimeException("Number must be non-null");
    562        }
    563
    564        double doubleValue = number.doubleValue();
    565        JSON.checkDouble(doubleValue);
    566
    567        // the original returns "-0" instead of "-0.0" for negative zero
    568        if (number.equals(NEGATIVE_ZERO)) {
    569            return "-0";
    570        }
    571
    572        long longValue = number.longValue();
    573        if (doubleValue == (double) longValue) {
    574            return Long.toString(longValue);
    575        }
    576
    577        return number.toString();
    578    }
    579
    580    static String doubleToString(double d)
    581    {
    582        if (Double.isInfinite(d) || Double.isNaN(d))
    583        {
    584            return "null";
    585        }
    586
    587        return numberToString(d);
    588    }
    589
    590    /**
    591     * Encodes {@code data} as a JSON string. This applies quotes and any
    592     * necessary character escaping.
    593     *
    594     * @param data the string to encode. Null will be interpreted as an empty
    595     *             string.
    596     * @return the quoted string.
    597     */
    598    public static String quote(String data) {
    599        if (data == null) {
    600            return "\"\"";
    601        }
    602        try {
    603            JSONStringer stringer = new JSONStringer();
    604            stringer.open(JSONStringer.Scope.NULL, "");
    605            stringer.string(data);
    606            stringer.close(JSONStringer.Scope.NULL, JSONStringer.Scope.NULL, "");
    607            return stringer.toString();
    608        } catch (RuntimeException e) {
    609            throw new AssertionError();
    610        }
    611    }
    612
    613
    614
    615    /**
    616     * Prints the JSONObject using the session.
    617     *
    618     * @since 5.2.0
    619     */
    620    @Override
    621    void print(JSONPrintSession session)
    622    {
    623        session.printSymbol('{');
    624
    625        session.indent();
    626
    627        boolean comma = false;
    628
    629        for (String key : keys())
    630        {
    631            if (comma)
    632                session.printSymbol(',');
    633
    634            session.newline();
    635
    636            session.printQuoted(key);
    637
    638            session.printSymbol(':');
    639
    640            printValue(session, nameValuePairs.get(key));
    641
    642            comma = true;
    643        }
    644
    645        session.outdent();
    646
    647        if (comma)
    648            session.newline();
    649
    650        session.printSymbol('}');
    651    }
    652
    653
    654    /**
    655     * Prints a value (a JSONArray or JSONObject, or a value stored in an array or object) using
    656     * the session.
    657     *
    658     * @since 5.2.0
    659     */
    660    static void printValue(JSONPrintSession session, Object value)
    661    {
    662
    663        if (value == null || value == NULL)
    664        {
    665            session.print("null");
    666            return;
    667        }
    668        if (value instanceof JSONObject)
    669        {
    670            ((JSONObject) value).print(session);
    671            return;
    672        }
    673
    674        if (value instanceof JSONArray)
    675        {
    676            ((JSONArray) value).print(session);
    677            return;
    678        }
    679
    680        if (value instanceof JSONString)
    681        {
    682            String printValue = ((JSONString) value).toJSONString();
    683
    684            session.print(printValue);
    685
    686            return;
    687        }
    688
    689        if (value instanceof Number)
    690        {
    691            String printValue = numberToString((Number) value);
    692            session.print(printValue);
    693            return;
    694        }
    695
    696        if (value instanceof Boolean)
    697        {
    698            session.print(value.toString());
    699
    700            return;
    701        }
    702
    703        // Otherwise it really should just be a string. Nothing else can go in.
    704        session.printQuoted(value.toString());
    705    }
    706
    707    public boolean equals(Object obj)
    708    {
    709        if (obj == null)
    710            return false;
    711
    712        if (!(obj instanceof JSONObject))
    713            return false;
    714
    715        JSONObject other = (JSONObject) obj;
    716
    717        return nameValuePairs.equals(other.nameValuePairs);
    718    }
    719
    720    /**
    721     * Returns a Map of the keys and values of the JSONObject. The returned map is unmodifiable.
    722     * Note that changes to the JSONObject will be reflected in the map. In addition, null values in the JSONObject
    723     * are represented as {@link JSONObject#NULL} in the map.
    724     *
    725     * @return unmodifiable map of properties and values
    726     * @since 5.4
    727     */
    728    public Map<String, Object> toMap()
    729    {
    730        return Collections.unmodifiableMap(nameValuePairs);
    731    }
    732
    733    /**
    734     * Invokes {@link #put(String, Object)} for each value from the map.
    735     *
    736     * @param newProperties
    737     *         to add to this JSONObject
    738     * @return this JSONObject
    739     * @since 5.4
    740     */
    741    public JSONObject putAll(Map<String, ?> newProperties)
    742    {
    743        assert newProperties != null;
    744
    745        for (Map.Entry<String, ?> e : newProperties.entrySet())
    746        {
    747            put(e.getKey(), e.getValue());
    748        }
    749
    750        return this;
    751    }
    752
    753
    754    /**
    755     * Navigates into a nested JSONObject, creating the JSONObject if necessary. They key must not exist,
    756     * or must be a JSONObject.
    757     *
    758     * @param key
    759     * @return the nested JSONObject
    760     * @throws IllegalStateException
    761     *         if the current value for the key is not null and not JSONObject
    762     */
    763    public JSONObject in(String key)
    764    {
    765        assert key != null;
    766
    767        Object nested = nameValuePairs.get(key);
    768
    769        if (nested != null && !(nested instanceof JSONObject))
    770        {
    771            throw new IllegalStateException(String.format("JSONObject[%s] is not a JSONObject.", quote(key)));
    772        }
    773
    774        if (nested == null)
    775        {
    776            nested = new JSONObject();
    777            nameValuePairs.put(key, nested);
    778        }
    779
    780        return (JSONObject) nested;
    781    }
    782
    783    static void testValidity(Object value)
    784    {
    785        if (value == null)
    786          throw new IllegalArgumentException("null isn't valid in JSONObject and JSONArray. Use JSONObject.NULL instead.");
    787        if (value == NULL)
    788        {
    789            return;
    790        }
    791        Class<? extends Object> clazz = value.getClass();
    792        if (Boolean.class.isAssignableFrom(clazz)
    793            || Number.class.isAssignableFrom(clazz)
    794            || String.class.isAssignableFrom(clazz)
    795            || JSONArray.class.isAssignableFrom(clazz)
    796            || JSONLiteral.class.isAssignableFrom(clazz)
    797            || JSONObject.class.isAssignableFrom(clazz)
    798            || JSONString.class.isAssignableFrom(clazz))
    799        {
    800            return;
    801        }
    802
    803        throw new RuntimeException("JSONObject properties may be one of Boolean, Number, String, org.apache.tapestry5.json.JSONArray, org.apache.tapestry5.json.JSONLiteral, org.apache.tapestry5.json.JSONObject, org.apache.tapestry5.json.JSONObject$Null, org.apache.tapestry5.json.JSONString. Type "+clazz.getName()+" is not allowed.");
    804    }
    805
    806}