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 }