001    package org.apache.tapestry.json;
002    
003    /*
004     Copyright (c) 2002 JSON.org
005    
006     Permission is hereby granted, free of charge, to any person obtaining a copy
007     of this software and associated documentation files (the "Software"), to deal
008     in the Software without restriction, including without limitation the rights
009     to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
010     copies of the Software, and to permit persons to whom the Software is
011     furnished to do so, subject to the following conditions:
012    
013     The above copyright notice and this permission notice shall be included in all
014     copies or substantial portions of the Software.
015    
016     The Software shall be used for Good, not Evil.
017    
018     THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
019     IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
020     FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
021     AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
022     LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
023     OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
024     SOFTWARE.
025     */
026    
027    import java.text.ParseException;
028    import java.util.*;
029    
030    /**
031     * A JSONObject is an unordered collection of name/value pairs. Its external
032     * form is a string wrapped in curly braces with colons between the names and
033     * values, and commas between the values and names. The internal form is an
034     * object having get() and opt() methods for accessing the values by name, and
035     * put() methods for adding or replacing values by name. The values can be any
036     * of these types: Boolean, JSONArray, JSONObject, Number, String, or the
037     * JSONObject.NULL object.
038     * <p>
039     * The constructor can convert an external form string into an internal form
040     * Java object. The toString() method creates an external form string.
041     * <p>
042     * A get() method returns a value if one can be found, and throws an exception
043     * if one cannot be found. An opt() method returns a default value instead of
044     * throwing an exception, and so is useful for obtaining optional values.
045     * <p>
046     * The generic get() and opt() methods return an object, which you can cast or
047     * query for type. There are also typed get() and opt() methods that do type
048     * checking and type coersion for you.
049     * <p>
050     * The texts produced by the toString() methods are very strict. The
051     * constructors are more forgiving in the texts they will accept:
052     * <ul>
053     * <li>An extra <code>,</code>&nbsp;<small>(comma)</small> may appear just
054     * before the closing brace.</li>
055     * <li>Strings may be quoted with <code>'</code>&nbsp;<small>(single quote)</small>.</li>
056     * <li>Strings do not need to be quoted at all if they do not begin with a
057     * quote or single quote, and if they do not contain leading or trailing spaces,
058     * and if they do not contain any of these characters:
059     * <code>{ } [ ] / \ : , = ; #</code> and if they do not look like numbers and
060     * if they are not the reserved words <code>true</code>, <code>false</code>,
061     * or <code>null</code>.</li>
062     * <li>Keys can be followed by <code>=</code> or <code>=></code> as well as
063     * by <code>:</code></li>
064     * <li>Values can be followed by <code>;</code> as well as by <code>,</code></li>
065     * <li>Numbers may have the <code>0-</code> <small>(octal)</small> or
066     * <code>0x-</code> <small>(hex)</small> prefix.</li>
067     * <li>Line comments can begin with <code>#</code></li>
068     * </ul>
069     * 
070     * @author JSON.org
071     * @version 1
072     */
073    public class JSONObject
074    {
075    
076        /**
077         * It is sometimes more convenient and less ambiguous to have a NULL object
078         * than to use Java's null value. JSONObject.NULL.equals(null) returns true.
079         * JSONObject.NULL.toString() returns "null".
080         */
081        public static final Object NULL = new Null();
082    
083        /**
084         * JSONObject.NULL is equivalent to the value that JavaScript calls null,
085         * whilst Java's null is equivalent to the value that JavaScript calls
086         * undefined.
087         */
088        private static final class Null
089        {
090    
091            /**
092             * There is only intended to be a single instance of the NULL object, so
093             * the clone method returns itself.
094             * CHECKSTYLE:OFF
095             * @return NULL.
096             */
097            protected Object clone()
098            {
099                return this;
100            }
101    
102            /**
103             * A Null object is equal to the null value and to itself.
104             * 
105             * @param object
106             *            An object to test for nullness.
107             * @return true if the object parameter is the JSONObject.NULL object or
108             *         null.
109             */
110            public boolean equals(Object object)
111            {
112                return object == null || object == this;
113            }
114    
115            /**
116             * Get the "null" string value.
117             * 
118             * @return The string "null".
119             */
120            public String toString()
121            {
122                return "null";
123            }
124        }
125    
126        /**
127         * The hash map where the JSONObject's properties are kept.
128         */
129        private HashMap myHashMap;
130    
131        /**
132         * Construct an empty JSONObject.
133         */
134        public JSONObject()
135        {
136            this.myHashMap = new LinkedHashMap();
137        }
138    
139        /**
140         * Construct a JSONObject from a subset of another JSONObject. An array of
141         * strings is used to identify the keys that should be copied. Missing keys
142         * are ignored.
143         * 
144         * @param jo
145         *            A JSONObject.
146         * @param sa
147         *            An array of strings.
148         */
149        public JSONObject(JSONObject jo, String[] sa)
150        {
151            this();
152            for(int i = 0; i < sa.length; i += 1)
153            {
154                putOpt(sa[i], jo.opt(sa[i]));
155            }
156        }
157    
158        /**
159         * Construct a JSONObject from a JSONTokener.
160         * 
161         * @param x
162         *            A JSONTokener object containing the source string.
163         * @throws ParseException
164         *             if there is a syntax error in the source string.
165         */
166        public JSONObject(JSONTokener x)
167            throws ParseException
168        {
169            this();
170            char c;
171            String key;
172    
173            if (x.nextClean() != '{') { throw x
174                    .syntaxError("A JSONObject must begin with '{'"); }
175            while(true)
176            {
177                c = x.nextClean();
178                switch(c)
179                {
180                case 0:
181                    throw x.syntaxError("A JSONObject must end with '}'");
182                case '}':
183                    return;
184                default:
185                    x.back();
186                    key = x.nextValue().toString();
187                }
188    
189                /*
190                 * The key is followed by ':'. We will also tolerate '=' or '=>'.
191                 */
192    
193                c = x.nextClean();
194                if (c == '=')
195                {
196                    if (x.next() != '>')
197                    {
198                        x.back();
199                    }
200                }
201                else if (c != ':') { throw x
202                        .syntaxError("Expected a ':' after a key"); }
203                this.myHashMap.put(key, x.nextValue());
204    
205                /*
206                 * Pairs are separated by ','. We will also tolerate ';'.
207                 */
208    
209                switch(x.nextClean())
210                {
211                case ';':
212                case ',':
213                    if (x.nextClean() == '}') { return; }
214                    x.back();
215                    break;
216                case '}':
217                    return;
218                default:
219                    throw x.syntaxError("Expected a ',' or '}'");
220                }
221            }
222        }
223    
224        /**
225         * Construct a JSONObject from a Map.
226         * 
227         * @param map
228         *            A map object that can be used to initialize the contents of
229         *            the JSONObject.
230         */
231        public JSONObject(Map map)
232        {
233            this.myHashMap = new HashMap(map);
234        }
235    
236        /**
237         * Construct a JSONObject from a string. This is the most commonly used
238         * JSONObject constructor.
239         * 
240         * @param string
241         *            A string beginning with <code>{</code>&nbsp;<small>(left
242         *            brace)</small> and ending with <code>}</code>&nbsp;<small>(right
243         *            brace)</small>.
244         * @exception ParseException
245         *                The string must be properly formatted.
246         */
247        public JSONObject(String string)
248            throws ParseException
249        {
250            this(new JSONTokener(string));
251        }
252    
253        public JSONObject accumulate(String key, Object value)
254        {
255            JSONArray a;
256            Object o = opt(key);
257            if (o == null)
258            {
259                a = new JSONArray();
260                a.put(value);
261                put(key, a);
262            }
263            else if (o instanceof JSONArray)
264            {
265                a = (JSONArray) o;
266                a.put(value);
267            }
268            
269            return this;
270        }
271    
272        public Object get(String key)
273        {
274            Object o = opt(key);
275            if (o == null) { throw new NoSuchElementException("JSONObject["
276                    + quote(key) + "] not found."); }
277            return o;
278        }
279    
280        public boolean getBoolean(String key)
281        {
282            Object o = get(key);
283            if (o.equals(Boolean.FALSE)
284                    || (o instanceof String && ((String) o)
285                            .equalsIgnoreCase("false")))
286            {
287                return false;
288            }
289            else if (o.equals(Boolean.TRUE)
290                    || (o instanceof String && ((String) o)
291                            .equalsIgnoreCase("true"))) { return true; }
292            throw new ClassCastException("JSONObject[" + quote(key)
293                    + "] is not a Boolean.");
294        }
295    
296        public double getDouble(String key)
297        {
298            Object o = get(key);
299            if (o instanceof Number) { return ((Number) o).doubleValue(); }
300            if (o instanceof String) { return new Double((String) o).doubleValue(); }
301            throw new NumberFormatException("JSONObject[" + quote(key)
302                    + "] is not a number.");
303        }
304    
305        /**
306         * Get the Map the holds that contents of the JSONObject.
307         * 
308         * @return The getHashMap.
309         */
310        Map getMap()
311        {
312            return this.myHashMap;
313        }
314    
315        public int getInt(String key)
316        {
317            Object o = get(key);
318            return o instanceof Number ? ((Number) o).intValue()
319                    : (int) getDouble(key);
320        }
321    
322        public JSONArray getJSONArray(String key)
323        {
324            Object o = get(key);
325            if (o instanceof JSONArray) { return (JSONArray) o; }
326            throw new NoSuchElementException("JSONObject[" + quote(key)
327                    + "] is not a JSONArray.");
328        }
329    
330        public JSONObject getJSONObject(String key)
331        {
332            Object o = get(key);
333            if (o instanceof JSONObject) { return (JSONObject) o; }
334            throw new NoSuchElementException("JSONObject[" + quote(key)
335                    + "] is not a JSONObject.");
336        }
337    
338        public String getString(String key)
339        {
340            return get(key).toString();
341        }
342    
343        public boolean has(String key)
344        {
345            return this.myHashMap.containsKey(key);
346        }
347    
348        public boolean isNull(String key)
349        {
350            return JSONObject.NULL.equals(opt(key));
351        }
352    
353        public Iterator keys()
354        {
355            return this.myHashMap.keySet().iterator();
356        }
357    
358        public int length()
359        {
360            return this.myHashMap.size();
361        }
362    
363        public JSONArray names()
364        {
365            JSONArray ja = new JSONArray();
366            Iterator keys = keys();
367            while(keys.hasNext())
368            {
369                ja.put(keys.next());
370            }
371            return ja.length() == 0 ? null : ja;
372        }
373    
374        /**
375         * Produce a string from a number.
376         * 
377         * @param n A Number
378         * @return A String.
379         * @exception ArithmeticException
380         *                JSON can only serialize finite numbers.
381         */
382        public static String numberToString(Number n)
383        {
384            if ((n instanceof Float && (((Float) n).isInfinite() || ((Float) n).isNaN()))
385                    || (n instanceof Double && (((Double) n).isInfinite() || ((Double) n).isNaN()))) { 
386                throw new ArithmeticException("JSON can only serialize finite numbers."); 
387            }
388            
389            // Shave off trailing zeros and decimal point, if possible.
390            
391            String s = n.toString();
392            if (s.indexOf('.') > 0 && s.indexOf('e') < 0 && s.indexOf('E') < 0)
393            {
394                while(s.endsWith("0"))
395                {
396                    s = s.substring(0, s.length() - 1);
397                }
398                if (s.endsWith("."))
399                {
400                    s = s.substring(0, s.length() - 1);
401                }
402            }
403            return s;
404        }
405    
406        public Object opt(String key)
407        {
408            if (key == null) { throw new NullPointerException("Null key"); }
409            return this.myHashMap.get(key);
410        }
411    
412        public boolean optBoolean(String key)
413        {
414            return optBoolean(key, false);
415        }
416    
417        public boolean optBoolean(String key, boolean defaultValue)
418        {
419            Object o = opt(key);
420            if (o != null)
421            {
422                if (o.equals(Boolean.FALSE)
423                        || (o instanceof String && ((String) o)
424                                .equalsIgnoreCase("false")))
425                {
426                    return false;
427                }
428                else if (o.equals(Boolean.TRUE)
429                        || (o instanceof String && ((String) o)
430                                .equalsIgnoreCase("true"))) { return true; }
431            }
432            return defaultValue;
433        }
434    
435        public double optDouble(String key)
436        {
437            return optDouble(key, Double.NaN);
438        }
439    
440        public double optDouble(String key, double defaultValue)
441        {
442            Object o = opt(key);
443            if (o != null)
444            {
445                if (o instanceof Number) { return ((Number) o).doubleValue(); }
446                try
447                {
448                    return new Double((String) o).doubleValue();
449                }
450                catch (Exception e)
451                {
452                    return defaultValue;
453                }
454            }
455            return defaultValue;
456        }
457    
458        public int optInt(String key)
459        {
460            return optInt(key, 0);
461        }
462    
463        public int optInt(String key, int defaultValue)
464        {
465            Object o = opt(key);
466            if (o != null)
467            {
468                if (o instanceof Number) { return ((Number) o).intValue(); }
469                try
470                {
471                    return Integer.parseInt((String) o);
472                }
473                catch (Exception e)
474                {
475                    return defaultValue;
476                }
477            }
478            return defaultValue;
479        }
480    
481        public JSONArray optJSONArray(String key)
482        {
483            Object o = opt(key);
484            return o instanceof JSONArray ? (JSONArray) o : null;
485        }
486    
487        public JSONObject optJSONObject(String key)
488        {
489            Object o = opt(key);
490            return o instanceof JSONObject ? (JSONObject) o : null;
491        }
492    
493        public String optString(String key)
494        {
495            return optString(key, "");
496        }
497    
498        public String optString(String key, String defaultValue)
499        {
500            Object o = opt(key);
501            return o != null ? o.toString() : defaultValue;
502        }
503    
504        public JSONObject put(Object key, boolean value)
505        {
506            put(key, Boolean.valueOf(value));
507            return this;
508        }
509    
510        public JSONObject put(Object key, double value)
511        {
512            put(key, new Double(value));
513            return this;
514        }
515    
516        public JSONObject put(Object key, long value)
517        {
518            put(key, new Long(value));
519            return this;
520        }
521        
522        public JSONObject put(Object key, int value)
523        {
524            put(key, new Integer(value));
525            return this;
526        }
527        
528        public JSONObject put(Object key, Object value)
529        {
530            if (key == null)
531                throw new NullPointerException("Null key.");
532            
533            if (value != null)
534            {
535                this.myHashMap.put(key, value);
536            }
537            else
538            {
539                remove(key.toString());
540            }
541            
542            return this;
543        }
544        
545        public JSONObject putOpt(String key, Object value)
546        {
547            if (value != null)
548            {
549                put(key, value);
550            }
551            return this;
552        }
553    
554        /**
555         * @see #quote(String) .
556         * @param value
557         * 
558         * @return The character quoted.
559         */
560        public static String quote(char value)
561        {
562            return quote(new String(new char[]{value}));
563        }
564        
565        /**
566         * Produce a string in double quotes with backslash sequences in all the
567         * right places.
568         * 
569         * @param string
570         *            A String
571         * @return A String correctly formatted for insertion in a JSON message.
572         */
573        public static String quote(String string)
574        {
575            if (string == null || string.length() == 0) { return "\"\""; }
576    
577            char b;
578            char c = 0;
579            int i;
580            int len = string.length();
581            StringBuffer sb = new StringBuffer(len + 4);
582            String t;
583    
584            sb.append('"');
585            for(i = 0; i < len; i += 1)
586            {
587                b = c;
588                c = string.charAt(i);
589                switch(c)
590                {
591                case '\\':
592                case '"':
593                    sb.append('\\');
594                    sb.append(c);
595                    break;
596                case '/':
597                    if (b == '<')
598                    {
599                        sb.append('\\');
600                    }
601                    sb.append(c);
602                    break;
603                case '\b':
604                    sb.append("\\b");
605                    break;
606                case '\t':
607                    sb.append("\\t");
608                    break;
609                case '\n':
610                    sb.append("\\n");
611                    break;
612                case '\f':
613                    sb.append("\\f");
614                    break;
615                case '\r':
616                    sb.append("\\r");
617                    break;
618                default:
619                    if (c < ' ')
620                    {
621                        t = "000" + Integer.toHexString(c);
622                        sb.append("\\u" + t.substring(t.length() - 4));
623                    }
624                    else
625                    {
626                        sb.append(c);
627                    }
628                }
629            }
630            sb.append('"');
631            return sb.toString();
632        }
633    
634        public Object remove(String key)
635        {
636            return this.myHashMap.remove(key);
637        }
638    
639        public JSONArray toJSONArray(JSONArray names)
640        {
641            if (names == null || names.length() == 0) { return null; }
642            JSONArray ja = new JSONArray();
643            for(int i = 0; i < names.length(); i += 1)
644            {
645                ja.put(this.opt(names.getString(i)));
646            }
647            return ja;
648        }
649    
650        /** 
651         * {@inheritDoc}
652         */
653        public String toString()
654        {
655            Iterator keys = keys();
656            StringBuffer sb = new StringBuffer("{");
657    
658            while(keys.hasNext())
659            {
660                if (sb.length() > 1)
661                {
662                    sb.append(',');
663                }
664                Object o = keys.next();
665                sb.append(valueToString(o));
666                sb.append(':');
667                sb.append(valueToString(this.myHashMap.get(o)));
668            }
669            sb.append('}');
670            return sb.toString();
671        }
672        
673        public String toString(int indentFactor)
674        {
675            return toString(indentFactor, 0);
676        }
677    
678        /**
679         * Make a prettyprinted JSON string of this JSONObject.
680         * <p>
681         * Warning: This method assumes that the data structure is acyclical.
682         * 
683         * @param indentFactor
684         *            The number of spaces to add to each level of indentation.
685         * @param indent
686         *            The indentation of the top level.
687         * @return a printable, displayable, transmittable representation of the
688         *         object, beginning with <code>{</code>&nbsp;<small>(left
689         *         brace)</small> and ending with <code>}</code>&nbsp;<small>(right
690         *         brace)</small>.
691         */
692        String toString(int indentFactor, int indent)
693        {
694            int i;
695            int n = length();
696            if (n == 0) { return "{}"; }
697            Iterator keys = keys();
698            StringBuffer sb = new StringBuffer("{");
699            int newindent = indent + indentFactor;
700            Object o;
701            if (n == 1)
702            {
703                o = keys.next();
704                sb.append(quote(o.toString()));
705                sb.append(": ");
706                sb
707                        .append(valueToString(this.myHashMap.get(o), indentFactor,
708                                indent));
709            }
710            else
711            {
712                while(keys.hasNext())
713                {
714                    o = keys.next();
715                    if (sb.length() > 1)
716                    {
717                        sb.append(",\n");
718                    }
719                    else
720                    {
721                        sb.append('\n');
722                    }
723                    for(i = 0; i < newindent; i += 1)
724                    {
725                        sb.append(' ');
726                    }
727                    sb.append(quote(o.toString()));
728                    sb.append(": ");
729                    sb.append(valueToString(this.myHashMap.get(o), indentFactor,
730                            newindent));
731                }
732                if (sb.length() > 1)
733                {
734                    sb.append('\n');
735                    for(i = 0; i < indent; i += 1)
736                    {
737                        sb.append(' ');
738                    }
739                }
740            }
741            sb.append('}');
742            return sb.toString();
743        }
744    
745        /**
746         * Make JSON string of an object value.
747         * <p>
748         * Warning: This method assumes that the data structure is acyclical.
749         * 
750         * @param value
751         *            The value to be serialized.
752         * @return a printable, displayable, transmittable representation of the
753         *         object, beginning with <code>{</code>&nbsp;<small>(left
754         *         brace)</small> and ending with <code>}</code>&nbsp;<small>(right
755         *         brace)</small>.
756         */
757        static String valueToString(Object value)
758        {
759            if (value == null || value.equals(null)) { return "null"; }
760            if (value instanceof Number) { return numberToString((Number) value); }
761            if (value instanceof Boolean || value instanceof JSONObject
762                    || value instanceof JSONArray
763                    || value instanceof JSONLiteral) { return value.toString(); }
764            return quote(value.toString());
765        }
766    
767        /**
768         * Make a prettyprinted JSON string of an object value.
769         * <p>
770         * Warning: This method assumes that the data structure is acyclical.
771         * 
772         * @param value
773         *            The value to be serialized.
774         * @param indentFactor
775         *            The number of spaces to add to each level of indentation.
776         * @param indent
777         *            The indentation of the top level.
778         * @return a printable, displayable, transmittable representation of the
779         *         object, beginning with <code>{</code>&nbsp;<small>(left
780         *         brace)</small> and ending with <code>}</code>&nbsp;<small>(right
781         *         brace)</small>.
782         */
783        static String valueToString(Object value, int indentFactor, int indent)
784        {
785            if (value == null || value.equals(null)) { return "null"; }
786            if (value instanceof Number) { return numberToString((Number) value); }
787            if (value instanceof Boolean) { return value.toString(); }
788            if (value instanceof JSONObject) { return (((JSONObject) value)
789                    .toString(indentFactor, indent)); }
790            if (value instanceof JSONArray) { return (((JSONArray) value).toString(
791                    indentFactor, indent)); }
792            if (JSONLiteral.class.isAssignableFrom(value.getClass()))
793                return value.toString();
794            return quote(value.toString());
795        }
796    }