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
015package 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
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 * <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"})
095public 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}