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> <small>(comma)</small> may appear just
054 * before the closing brace.</li>
055 * <li>Strings may be quoted with <code>'</code> <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> <small>(left
242 * brace)</small> and ending with <code>}</code> <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> <small>(left
689 * brace)</small> and ending with <code>}</code> <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> <small>(left
754 * brace)</small> and ending with <code>}</code> <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> <small>(left
780 * brace)</small> and ending with <code>}</code> <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 }