    001    // Copyright 2007, 2010, 2011 The Apache Software Foundation
    002    //
    003    // Licensed under the Apache License, Version 2.0 (the "License");
    004    // you may not use this file except in compliance with the License.
    005    // You may obtain a copy of the License at
    006    //
    007    // http://www.apache.org/licenses/LICENSE-2.0
    008    //
    009    // Unless required by applicable law or agreed to in writing, software
    010    // distributed under the License is distributed on an "AS IS" BASIS,
    011    // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    012    // See the License for the specific language governing permissions and
    013    // limitations under the License.
    014
    015    package org.apache.tapestry5.json;
    016
    017    /*
    018     * Copyright (c) 2002 JSON.org
    019     * Permission is hereby granted, free of charge, to any person obtaining a copy
    020     * of this software and associated documentation files (the "Software"), to deal
    021     * in the Software without restriction, including without limitation the rights
    022     * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    023     * copies of the Software, and to permit persons to whom the Software is
    024     * furnished to do so, subject to the following conditions:
    025     * The above copyright notice and this permission notice shall be included in all
    026     * copies or substantial portions of the Software.
    027     * The Software shall be used for Good, not Evil.
    028     * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    029     * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    030     * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    031     * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    032     * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    033     * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
    034     * SOFTWARE.
    035     */
    036
    037    import java.util.*;
    038
    039    /**
    040     * A JSONObject is an unordered collection of name/value pairs. Its external form is a string wrapped in curly braces
    041     * with colons between the names and values, and commas between the values and names. The internal form is an object
    042     * having <code>get</code> and <code>opt</code> methods for accessing the values by name, and <code>put</code> methods
    043     * for adding or replacing values by name. The values can be any of these types: <code>Boolean</code>,
    044     * {@link org.apache.tapestry5.json.JSONArray}, {@link org.apache.tapestry5.json.JSONLiteral}, <code>JSONObject</code>,
    045     * <code>Number</code>, <code>String</code>, or the <code>JSONObject.NULL</code> object. A JSONObject constructor can be
    046     * used to convert an external form JSON text into
    047     * an internal form whose values can be retrieved with the <code>get</code> and <code>opt</code> methods, or to convert
    048     * values into a JSON text using the <code>put</code> and <code>toString</code> methods. A <code>get</code> method
    049     * returns a value if one can be found, and throws an exception if one cannot be found. An <code>opt</code> method
    050     * returns a default value instead of throwing an exception, and so is useful for obtaining optional values.
    051     * <p/>
    052     * The generic <code>get()</code> and <code>opt()</code> methods return an object, which you can cast or query for type.
    053     * There are also typed <code>get</code> and <code>opt</code> methods that do type checking and type coersion for you.
    054     * <p/>
    055     * The <code>put</code> methods adds values to an object. For example,
    056     * <p/>
    057     * <p/>
    058     * <pre>
    059     * myString = new JSONObject().put(&quot;JSON&quot;, &quot;Hello, World!&quot;).toString();
    060     * </pre>
    061     * <p/>
    062     * produces the string <code>{"JSON": "Hello, World"}</code>.
    063     * <p/>
    064     * The texts produced by the <code>toString</code> methods strictly conform to the JSON syntax rules. The constructors
    065     * are more forgiving in the texts they will accept:
    066     * <ul>
    067     * <li>An extra <code>,</code>&nbsp;<small>(comma)</small> may appear just before the closing brace.</li>
    068     * <li>Strings may be quoted with <code>'</code>&nbsp;<small>(single quote)</small>.</li>
    069     * <li>Strings do not need to be quoted at all if they do not begin with a quote or single quote, and if they do not
    070     * contain leading or trailing spaces, and if they do not contain any of these characters: <code>{ }
    071     * [ ] / \ : , = ; #</code> and if they do not look like numbers and if they are not the reserved words
    072     * <code>true</code>, <code>false</code>, or <code>null</code>.</li>
    073     * <li>Keys can be followed by <code>=</code> or <code>=></code> as well as by <code>:</code>.</li>
    074     * <li>Values can be followed by <code>;</code> <small>(semicolon)</small> as well as by <code>,</code>
    075     * <small>(comma)</small>.</li>
    076     * <li>Numbers may have the <code>0-</code> <small>(octal)</small> or <code>0x-</code> <small>(hex)</small> prefix.</li>
    077     * <li>Comments written in the slashshlash, slashstar, and hash conventions will be ignored.</li>
    078     * </ul>
    079     * <hr/>
    080     * <p/>
    081     * This class, and the other related classes, have been heavily modified from the original source, to fit Tapestry
    082     * standards and to make use of JDK 1.5 features such as generics. Further, since the interest of Tapestry is primarily
    083     * constructing JSON (and not parsing it), many of the non-essential methods have been removed (since the original code
    084     * came with no tests).
    085     * <p/>
    086     * Finally, support for the {@link org.apache.tapestry5.json.JSONLiteral} type has been added, which allow the exact
    087     * output to be controlled; useful when a JSONObject is being used as a configuration object, and must contain values
    088     * that are not simple data, such as an inline function (technically making the result not JSON).
    089     *
    090     * @author JSON.org
    091     * @version 2
    092     */
    093    @SuppressWarnings(
    094            {"CloneDoesntCallSuperClone"})
    095    public final class JSONObject extends JSONCollection
    096    {
    097
    098        /**
    099         * JSONObject.NULL is equivalent to the value that JavaScript calls null, whilst Java's null is equivalent to the
    100         * value that JavaScript calls undefined.
    101         */
    102        private static final class Null implements JSONString
    103        {
    104            /**
    105             * A Null object is equal to the null value and to itself.
    106             *
    107             * @param object An object to test for nullness.
    108             * @return true if the object parameter is the JSONObject.NULL object or null.
    109             */
    110            @Override
    111            public boolean equals(Object object)
    112            {
    113                return object == null || object == this;
    114            }
    115
    116            /**
    117             * Get the "null" string value.
    118             *
    119             * @return The string "null".
    120             */
    121            @Override
    122            public String toString()
    123            {
    124                return "null";
    125            }
    126
    127            public String toJSONString()
    128            {
    129                return "null";
    130            }
    131        }
    132
    133        /**
    134         * The map where the JSONObject's properties are kept.
    135         */
    136        private final Map<String, Object> properties = new HashMap<String, Object>();
    137
    138        /**
    139         * It is sometimes more convenient and less ambiguous to have a <code>NULL</code> object than to use Java's
    140         * <code>null</code> value. <code>JSONObject.NULL.equals(null)</code> returns <code>true</code>.
    141         * <code>JSONObject.NULL.toString()</code> returns <code>"null"</code>.
    142         */
    143        public static final Object NULL = new Null();
    144
    145        /**
    146         * Construct an empty JSONObject.
    147         */
    148        public JSONObject()
    149        {
    150        }
    151
    152        /**
    153         * Constructs a new JSONObject using a series of String keys and values.
    154         *
    155         * @since 5.2.0
    156         */
    157        public JSONObject(String... keysAndValues)
    158        {
    159            int i = 0;
    160
    161            while (i < keysAndValues.length)
    162            {
    163                put(keysAndValues[i++], keysAndValues[i++]);
    164            }
    165        }
    166
    167        /**
    168         * Construct a JSONObject from a subset of another JSONObject. An array of strings is used to identify the keys that
    169         * should be copied. Missing keys are ignored.
    170         *
    171         * @param source        A JSONObject.
    172         * @param propertyNames The strings to copy.
    173         * @throws RuntimeException If a value is a non-finite number.
    174         */
    175        public JSONObject(JSONObject source, String... propertyNames)
    176        {
    177            for (String name : propertyNames)
    178            {
    179                Object value = source.opt(name);
    180
    181                if (value != null)
    182                    put(name, value);
    183            }
    184        }
    185
    186        /**
    187         * Construct a JSONObject from a JSONTokener.
    188         *
    189         * @param x A JSONTokener object containing the source string. @ If there is a syntax error in the source string.
    190         */
    191        JSONObject(JSONTokener x)
    192        {
    193            String key;
    194
    195            if (x.nextClean() != '{')
    196            {
    197                throw x.syntaxError("A JSONObject text must begin with '{'");
    198            }
    199
    200            while (true)
    201            {
    202                char c = x.nextClean();
    203                switch (c)
    204                {
    205                    case 0:
    206                        throw x.syntaxError("A JSONObject text must end with '}'");
    207                    case '}':
    208                        return;
    209                    default:
    210                        x.back();
    211                        key = x.nextValue().toString();
    212                }
    213
    214                /*
    215                 * The key is followed by ':'. We will also tolerate '=' or '=>'.
    216                 */
    217
    218                c = x.nextClean();
    219                if (c == '=')
    220                {
    221                    if (x.next() != '>')
    222                    {
    223                        x.back();
    224                    }
    225                } else if (c != ':')
    226                {
    227                    throw x.syntaxError("Expected a ':' after a key");
    228                }
    229                put(key, x.nextValue());
    230
    231                /*
    232                 * Pairs are separated by ','. We will also tolerate ';'.
    233                 */
    234
    235                switch (x.nextClean())
    236                {
    237                    case ';':
    238                    case ',':
    239                        if (x.nextClean() == '}')
    240                        {
    241                            return;
    242                        }
    243                        x.back();
    244                        break;
    245                    case '}':
    246                        return;
    247                    default:
    248                        throw x.syntaxError("Expected a ',' or '}'");
    249                }
    250            }
    251        }
    252
    253        /**
    254         * Construct a JSONObject from a string. This is the most commonly used JSONObject constructor.
    255         *
    256         * @param string A string beginning with <code>{</code>&nbsp;<small>(left brace)</small> and ending with <code>}</code>
    257         *               &nbsp;<small>(right brace)</small>.
    258         * @throws RuntimeException If there is a syntax error in the source string.
    259         */
    260        public JSONObject(String string)
    261        {
    262            this(new JSONTokener(string));
    263        }
    264
    265        /**
    266         * Accumulate values under a key. It is similar to the put method except that if there is already an object stored
    267         * under the key then a JSONArray is stored under the key to hold all of the accumulated values. If there is already
    268         * a JSONArray, then the new value is appended to it. In contrast, the put method replaces the previous value.
    269         *
    270         * @param key   A key string.
    271         * @param value An object to be accumulated under the key.
    272         * @return this.
    273         * @throws {@link RuntimeException} If the value is an invalid number or if the key is null.
    274         */
    275        public JSONObject accumulate(String key, Object value)
    276        {
    277            testValidity(value);
    278
    279            Object existing = opt(key);
    280
    281            if (existing == null)
    282            {
    283                // Note that the original implementation of this method contradicited the method
    284                // documentation.
    285                put(key, value);
    286                return this;
    287            }
    288
    289            if (existing instanceof JSONArray)
    290            {
    291                ((JSONArray) existing).put(value);
    292                return this;
    293            }
    294
    295            // Replace the existing value, of any type, with an array that includes both the
    296            // existing and the new value.
    297
    298            put(key, new JSONArray().put(existing).put(value));
    299
    300            return this;
    301        }
    302
    303        /**
    304         * Append values to the array under a key. If the key does not exist in the JSONObject, then the key is put in the
    305         * JSONObject with its value being a JSONArray containing the value parameter. If the key was already associated
    306         * with a JSONArray, then the value parameter is appended to it.
    307         *
    308         * @param key   A key string.
    309         * @param value An object to be accumulated under the key.
    310         * @return this. @ If the key is null or if the current value associated with the key is not a JSONArray.
    311         */
    312        public JSONObject append(String key, Object value)
    313        {
    314            testValidity(value);
    315            Object o = opt(key);
    316            if (o == null)
    317            {
    318                put(key, new JSONArray().put(value));
    319            } else if (o instanceof JSONArray)
    320            {
    321                put(key, ((JSONArray) o).put(value));
    322            } else
    323            {
    324                throw new RuntimeException("JSONObject[" + quote(key) + "] is not a JSONArray.");
    325            }
    326
    327            return this;
    328        }
    329
    330        /**
    331         * Produce a string from a double. The string "null" will be returned if the number is not finite.
    332         *
    333         * @param d A double.
    334         * @return A String.
    335         */
    336        static String doubleToString(double d)
    337        {
    338            if (Double.isInfinite(d) || Double.isNaN(d))
    339            {
    340                return "null";
    341            }
    342
    343            // Shave off trailing zeros and decimal point, if possible.
    344
    345            String s = Double.toString(d);
    346            if (s.indexOf('.') > 0 && s.indexOf('e') < 0 && s.indexOf('E') < 0)
    347            {
    348                while (s.endsWith("0"))
    349                {
    350                    s = s.substring(0, s.length() - 1);
    351                }
    352                if (s.endsWith("."))
    353                {
    354                    s = s.substring(0, s.length() - 1);
    355                }
    356            }
    357            return s;
    358        }
    359
    360        /**
    361         * Get the value object associated with a key.
    362         *
    363         * @param key A key string.
    364         * @return The object associated with the key.
    365         * @throws RuntimeException if the key is not found.
    366         * @see #opt(String)
    367         */
    368        public Object get(String key)
    369        {
    370            Object o = opt(key);
    371            if (o == null)
    372            {
    373                throw new RuntimeException("JSONObject[" + quote(key) + "] not found.");
    374            }
    375
    376            return o;
    377        }
    378
    379        /**
    380         * Get the boolean value associated with a key.
    381         *
    382         * @param key A key string.
    383         * @return The truth.
    384         * @throws RuntimeException if the value does not exist, is not a Boolean or the String "true" or "false".
    385         */
    386        public boolean getBoolean(String key)
    387        {
    388            Object o = get(key);
    389
    390            if (o instanceof Boolean)
    391                return o.equals(Boolean.TRUE);
    392
    393            if (o instanceof String)
    394            {
    395                String value = (String) o;
    396
    397                if (value.equalsIgnoreCase("true"))
    398                    return true;
    399
    400                if (value.equalsIgnoreCase("false"))
    401                    return false;
    402            }
    403
    404            throw new RuntimeException("JSONObject[" + quote(key) + "] is not a Boolean.");
    405        }
    406
    407        /**
    408         * Get the double value associated with a key.
    409         *
    410         * @param key A key string.
    411         * @return The numeric value. @throws RuntimeException if the key is not found or if the value is not a Number object and cannot be
    412         *         converted to a number.
    413         */
    414        public double getDouble(String key)
    415        {
    416            Object value = get(key);
    417
    418            try
    419            {
    420                if (value instanceof Number)
    421                    return ((Number) value).doubleValue();
    422
    423                // This is a bit sloppy for the case where value is not a string.
    424
    425                return Double.valueOf((String) value);
    426            } catch (Exception e)
    427            {
    428                throw new RuntimeException("JSONObject[" + quote(key) + "] is not a number.");
    429            }
    430        }
    431
    432        /**
    433         * Get the int value associated with a key. If the number value is too large for an int, it will be clipped.
    434         *
    435         * @param key A key string.
    436         * @return The integer value.
    437         * @throws RuntimeException if the key is not found or if the value cannot be converted to an integer.
    438         */
    439        public int getInt(String key)
    440        {
    441            Object value = get(key);
    442
    443            if (value instanceof Number)
    444                return ((Number) value).intValue();
    445
    446            // Very inefficient way to do this!
    447            return (int) getDouble(key);
    448        }
    449
    450        /**
    451         * Get the JSONArray value associated with a key.
    452         *
    453         * @param key A key string.
    454         * @return A JSONArray which is the value.
    455         * @throws RuntimeException if the key is not found or if the value is not a JSONArray.
    456         */
    457        public JSONArray getJSONArray(String key)
    458        {
    459            Object o = get(key);
    460            if (o instanceof JSONArray)
    461            {
    462                return (JSONArray) o;
    463            }
    464
    465            throw new RuntimeException("JSONObject[" + quote(key) + "] is not a JSONArray.");
    466        }
    467
    468        /**
    469         * Get the JSONObject value associated with a key.
    470         *
    471         * @param key A key string.
    472         * @return A JSONObject which is the value.
    473         * @throws RuntimeException if the key is not found or if the value is not a JSONObject.
    474         */
    475        public JSONObject getJSONObject(String key)
    476        {
    477            Object o = get(key);
    478            if (o instanceof JSONObject)
    479            {
    480                return (JSONObject) o;
    481            }
    482
    483            throw new RuntimeException("JSONObject[" + quote(key) + "] is not a JSONObject.");
    484        }
    485
    486        /**
    487         * Get the long value associated with a key. If the number value is too long for a long, it will be clipped.
    488         *
    489         * @param key A key string.
    490         * @return The long value.
    491         * @throws RuntimeException if the key is not found or if the value cannot be converted to a long.
    492         */
    493        public long getLong(String key)
    494        {
    495            Object o = get(key);
    496            return o instanceof Number ? ((Number) o).longValue() : (long) getDouble(key);
    497        }
    498
    499        /**
    500         * Get the string associated with a key.
    501         *
    502         * @param key A key string.
    503         * @return A string which is the value.
    504         * @throws RuntimeException if the key is not found.
    505         */
    506        public String getString(String key)
    507        {
    508            return get(key).toString();
    509        }
    510
    511        /**
    512         * Determine if the JSONObject contains a specific key.
    513         *
    514         * @param key A key string.
    515         * @return true if the key exists in the JSONObject.
    516         */
    517        public boolean has(String key)
    518        {
    519            return properties.containsKey(key);
    520        }
    521
    522        /**
    523         * Determine if the value associated with the key is null or if there is no value.
    524         *
    525         * @param key A key string.
    526         * @return true if there is no value associated with the key or if the value is the JSONObject.NULL object.
    527         */
    528        public boolean isNull(String key)
    529        {
    530            return JSONObject.NULL.equals(opt(key));
    531        }
    532
    533        /**
    534         * Get an enumeration of the keys of the JSONObject. Caution: the set should not be modified.
    535         *
    536         * @return An iterator of the keys.
    537         */
    538        public Set<String> keys()
    539        {
    540            return properties.keySet();
    541        }
    542
    543        /**
    544         * Get the number of keys stored in the JSONObject.
    545         *
    546         * @return The number of keys in the JSONObject.
    547         */
    548        public int length()
    549        {
    550            return properties.size();
    551        }
    552
    553        /**
    554         * Produce a JSONArray containing the names of the elements of this JSONObject.
    555         *
    556         * @return A JSONArray containing the key strings, or null if the JSONObject is empty.
    557         */
    558        public JSONArray names()
    559        {
    560            JSONArray ja = new JSONArray();
    561
    562            for (String key : keys())
    563            {
    564                ja.put(key);
    565            }
    566
    567            return ja.length() == 0 ? null : ja;
    568        }
    569
    570        /**
    571         * Produce a string from a Number.
    572         *
    573         * @param n A Number
    574         * @return A String. @ If n is a non-finite number.
    575         */
    576        static String numberToString(Number n)
    577        {
    578            assert n != null;
    579
    580            testValidity(n);
    581
    582            // Shave off trailing zeros and decimal point, if possible.
    583
    584            String s = n.toString();
    585            if (s.indexOf('.') > 0 && s.indexOf('e') < 0 && s.indexOf('E') < 0)
    586            {
    587                while (s.endsWith("0"))
    588                {
    589                    s = s.substring(0, s.length() - 1);
    590                }
    591                if (s.endsWith("."))
    592                {
    593                    s = s.substring(0, s.length() - 1);
    594                }
    595            }
    596            return s;
    597        }
    598
    599        /**
    600         * Get an optional value associated with a key.
    601         *
    602         * @param key A key string.
    603         * @return An object which is the value, or null if there is no value.
    604         * @see #get(String)
    605         */
    606        public Object opt(String key)
    607        {
    608            return properties.get(key);
    609        }
    610
    611        /**
    612         * Put a key/value pair in the JSONObject. If the value is null, then the key will be removed from the JSONObject if
    613         * it is present.
    614         *
    615         * @param key   A key string.
    616         * @param value An object which is the value. It should be of one of these types: Boolean, Double, Integer,
    617         *              JSONArray, JSONObject, JSONLiteral, Long, String, or the JSONObject.NULL object.
    618         * @return this.
    619         * @throws RuntimeException If the value is non-finite number or if the key is null.
    620         */
    621        public JSONObject put(String key, Object value)
    622        {
    623            assert key != null;
    624
    625            if (value != null)
    626            {
    627                testValidity(value);
    628                properties.put(key, value);
    629            } else
    630            {
    631                remove(key);
    632            }
    633
    634            return this;
    635        }
    636
    637        /**
    638         * Produce a string in double quotes with backslash sequences in all the right places. A backslash will be inserted
    639         * within </, allowing JSON text to be delivered in HTML. In JSON text, a string cannot contain a control character
    640         * or an unescaped quote or backslash.
    641         *
    642         * @param string A String
    643         * @return A String correctly formatted for insertion in a JSON text.
    644         */
    645        public static String quote(String string)
    646        {
    647            if (string == null || string.length() == 0)
    648            {
    649                return "\"\"";
    650            }
    651
    652            char b;
    653            char c = 0;
    654            int i;
    655            int len = string.length();
    656            StringBuilder buffer = new StringBuilder(len + 4);
    657            String t;
    658
    659            buffer.append('"');
    660            for (i = 0; i < len; i += 1)
    661            {
    662                b = c;
    663                c = string.charAt(i);
    664                switch (c)
    665                {
    666                    case '\\':
    667                    case '"':
    668                        buffer.append('\\');
    669                        buffer.append(c);
    670                        break;
    671                    case '/':
    672                        if (b == '<')
    673                        {
    674                            buffer.append('\\');
    675                        }
    676                        buffer.append(c);
    677                        break;
    678                    case '\b':
    679                        buffer.append("\\b");
    680                        break;
    681                    case '\t':
    682                        buffer.append("\\t");
    683                        break;
    684                    case '\n':
    685                        buffer.append("\\n");
    686                        break;
    687                    case '\f':
    688                        buffer.append("\\f");
    689                        break;
    690                    case '\r':
    691                        buffer.append("\\r");
    692                        break;
    693                    default:
    694                        if (c < ' ' || (c >= '\u0080' && c < '\u00a0') || (c >= '\u2000' && c < '\u2100'))
    695                        {
    696                            t = "000" + Integer.toHexString(c);
    697                            buffer.append("\\u").append(t.substring(t.length() - 4));
    698                        } else
    699                        {
    700                            buffer.append(c);
    701                        }
    702                }
    703            }
    704            buffer.append('"');
    705            return buffer.toString();
    706        }
    707
    708        /**
    709         * Remove a name and its value, if present.
    710         *
    711         * @param key The name to be removed.
    712         * @return The value that was associated with the name, or null if there was no value.
    713         */
    714        public Object remove(String key)
    715        {
    716            return properties.remove(key);
    717        }
    718
    719        private static final Class[] ALLOWED = new Class[]
    720                {String.class, Boolean.class, Number.class, JSONObject.class, JSONArray.class, JSONString.class,
    721                        JSONLiteral.class, Null.class};
    722
    723        /**
    724         * Throw an exception if the object is an NaN or infinite number, or not a type which may be stored.
    725         *
    726         * @param value The object to test. @ If o is a non-finite number.
    727         */
    728        @SuppressWarnings("unchecked")
    729        static void testValidity(Object value)
    730        {
    731            if (value == null)
    732                return;
    733
    734            boolean found = false;
    735            Class actual = value.getClass();
    736
    737            for (Class allowed : ALLOWED)
    738            {
    739                if (allowed.isAssignableFrom(actual))
    740                {
    741                    found = true;
    742                    break;
    743                }
    744            }
    745
    746            if (!found)
    747            {
    748                List<String> typeNames = new ArrayList<String>();
    749
    750                for (Class c : ALLOWED)
    751                {
    752                    String name = c.getName();
    753
    754                    if (name.startsWith("java.lang."))
    755                        name = name.substring(10);
    756
    757                    typeNames.add(name);
    758                }
    759
    760                Collections.sort(typeNames);
    761
    762                StringBuilder joined = new StringBuilder();
    763                String sep = "";
    764
    765                for (String name : typeNames)
    766                {
    767                    joined.append(sep);
    768                    joined.append(name);
    769
    770                    sep = ", ";
    771                }
    772
    773                String message = String.format("JSONObject properties may be one of %s. Type %s is not allowed.",
    774                        joined.toString(), actual.getName());
    775
    776                throw new RuntimeException(message);
    777            }
    778
    779            if (value instanceof Double)
    780            {
    781                Double asDouble = (Double) value;
    782
    783                if (asDouble.isInfinite() || asDouble.isNaN())
    784                {
    785                    throw new RuntimeException(
    786                            "JSON does not allow non-finite numbers.");
    787                }
    788
    789                return;
    790            }
    791
    792            if (value instanceof Float)
    793            {
    794                Float asFloat = (Float) value;
    795
    796                if (asFloat.isInfinite() || asFloat.isNaN())
    797                {
    798                    throw new RuntimeException(
    799                            "JSON does not allow non-finite numbers.");
    800                }
    801
    802            }
    803
    804        }
    805
    806        /**
    807         * Prints the JSONObject using the session.
    808         *
    809         * @since 5.2.0
    810         */
    811        void print(JSONPrintSession session)
    812        {
    813            session.printSymbol('{');
    814
    815            session.indent();
    816
    817            boolean comma = false;
    818
    819            for (String key : keys())
    820            {
    821                if (comma)
    822                    session.printSymbol(',');
    823
    824                session.newline();
    825
    826                session.printQuoted(key);
    827
    828                session.printSymbol(':');
    829
    830                printValue(session, properties.get(key));
    831
    832                comma = true;
    833            }
    834
    835            session.outdent();
    836
    837            if (comma)
    838                session.newline();
    839
    840            session.printSymbol('}');
    841        }
    842
    843        /**
    844         * Prints a value (a JSONArray or JSONObject, or a value stored in an array or object) using
    845         * the session.
    846         *
    847         * @since 5.2.0
    848         */
    849        static void printValue(JSONPrintSession session, Object value)
    850        {
    851            if (value instanceof JSONObject)
    852            {
    853                ((JSONObject) value).print(session);
    854                return;
    855            }
    856
    857            if (value instanceof JSONArray)
    858            {
    859                ((JSONArray) value).print(session);
    860                return;
    861            }
    862
    863            if (value instanceof JSONString)
    864            {
    865                String printValue = ((JSONString) value).toJSONString();
    866
    867                session.print(printValue);
    868
    869                return;
    870            }
    871
    872            if (value instanceof Number)
    873            {
    874                String printValue = numberToString((Number) value);
    875                session.print(printValue);
    876                return;
    877            }
    878
    879            if (value instanceof Boolean)
    880            {
    881                session.print(value.toString());
    882
    883                return;
    884            }
    885
    886            // Otherwise it really should just be a string. Nothing else can go in.
    887            session.printQuoted(value.toString());
    888        }
    889
    890        /**
    891         * Returns true if the other object is a JSONObject and its set of properties matches this object's properties.
    892         */
    893        @Override
    894        public boolean equals(Object obj)
    895        {
    896            if (obj == null)
    897                return false;
    898
    899            if (!(obj instanceof JSONObject))
    900                return false;
    901
    902            JSONObject other = (JSONObject) obj;
    903
    904            return properties.equals(other.properties);
    905        }
    906    }