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