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