001// Licensed under the Apache License, Version 2.0 (the "License"); 002// you may not use this file except in compliance with the License. 003// You may obtain a copy of the License at 004// 005// http://www.apache.org/licenses/LICENSE-2.0 006// 007// Unless required by applicable law or agreed to in writing, software 008// distributed under the License is distributed on an "AS IS" BASIS, 009// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 010// See the License for the specific language governing permissions and 011// limitations under the License. 012 013package org.apache.tapestry5.json; 014 015/* 016 * Copyright (c) 2002 JSON.org 017 * Permission is hereby granted, free of charge, to any person obtaining a copy 018 * of this software and associated documentation files (the "Software"), to deal 019 * in the Software without restriction, including without limitation the rights 020 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 021 * copies of the Software, and to permit persons to whom the Software is 022 * furnished to do so, subject to the following conditions: 023 * The above copyright notice and this permission notice shall be included in all 024 * copies or substantial portions of the Software. 025 * The Software shall be used for Good, not Evil. 026 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 027 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 028 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 029 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 030 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 031 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 032 * SOFTWARE. 033 */ 034 035import java.io.ObjectStreamException; 036import java.io.Serializable; 037import java.util.*; 038 039/** 040 * A JSONObject is an unordered collection of name/value pairs. Its external form is a string wrapped in curly braces 041 * with colons between the names and values, and commas between the values and names. The internal form is an object 042 * having <code>get</code> and <code>opt</code> methods for accessing the values by name, and <code>put</code> methods 043 * for adding or replacing values by name. The values can be any of these types: <code>Boolean</code>, 044 * {@link org.apache.tapestry5.json.JSONArray}, {@link org.apache.tapestry5.json.JSONLiteral}, <code>JSONObject</code>, 045 * <code>Number</code>, <code>String</code>, or the <code>JSONObject.NULL</code> object. A JSONObject constructor can be 046 * used to convert an external form JSON text into 047 * an internal form whose values can be retrieved with the <code>get</code> and <code>opt</code> methods, or to convert 048 * values into a JSON text using the <code>put</code> and <code>toString</code> methods. A <code>get</code> method 049 * returns a value if one can be found, and throws an exception if one cannot be found. An <code>opt</code> method 050 * returns a default value instead of throwing an exception, and so is useful for obtaining optional values. 051 * 052 * The generic <code>get()</code> and <code>opt()</code> methods return an object, which you can cast or query for type. 053 * There are also typed <code>get</code> and <code>opt</code> methods that do type checking and type coersion for you. 054 * 055 * The <code>put</code> methods adds values to an object. For example, 056 * 057 * <pre> 058 * myString = new JSONObject().put("JSON", "Hello, World!").toString(); 059 * </pre> 060 * 061 * produces the string <code>{"JSON": "Hello, World"}</code>. 062 * 063 * The texts produced by the <code>toString</code> methods strictly conform to the JSON syntax rules. The constructors 064 * are more forgiving in the texts they will accept: 065 * <ul> 066 * <li>An extra <code>,</code> <small>(comma)</small> may appear just before the closing brace.</li> 067 * <li>Strings may be quoted with <code>'</code> <small>(single quote)</small>.</li> 068 * <li>Strings do not need to be quoted at all if they do not begin with a quote or single quote, and if they do not 069 * contain leading or trailing spaces, and if they do not contain any of these characters: <code>{ } 070 * [ ] / \ : , = ; #</code> and if they do not look like numbers and if they are not the reserved words 071 * <code>true</code>, <code>false</code>, or <code>null</code>.</li> 072 * <li>Keys can be followed by <code>=</code> or {@code =>} as well as by {@code :}.</li> 073 * <li>Values can be followed by <code>;</code> <small>(semicolon)</small> as well as by <code>,</code> 074 * <small>(comma)</small>.</li> 075 * <li>Numbers may have the <code>0-</code> <small>(octal)</small> or <code>0x-</code> <small>(hex)</small> prefix.</li> 076 * <li>Comments written in the slashshlash, slashstar, and hash conventions will be ignored.</li> 077 * </ul> 078 * <hr> 079 * 080 * This class, and the other related classes, have been heavily modified from the original source, to fit Tapestry 081 * standards and to make use of JDK 1.5 features such as generics. Further, since the interest of Tapestry is primarily 082 * constructing JSON (and not parsing it), many of the non-essential methods have been removed (since the original code 083 * came with no tests). 084 * 085 * Finally, support for the {@link org.apache.tapestry5.json.JSONLiteral} type has been added, which allows the exact 086 * output to be controlled; useful when a JSONObject is being used as a configuration object, and must contain values 087 * that are not simple data, such as an inline function (making the result not JSON). 088 * 089 * @author JSON.org 090 * @version 2 091 */ 092@SuppressWarnings( 093 {"CloneDoesntCallSuperClone"}) 094public final class JSONObject extends JSONCollection 095{ 096 097 /** 098 * JSONObject.NULL is equivalent to the value that JavaScript calls null, whilst Java's null is equivalent to the 099 * value that JavaScript calls undefined. 100 */ 101 private static final class Null implements JSONString, Serializable 102 { 103 /** 104 * A Null object is equal to the null value and to itself. 105 * 106 * @param object 107 * An object to test for nullness. 108 * @return true if the object parameter is the JSONObject.NULL object or null. 109 */ 110 @Override 111 public boolean equals(Object object) 112 { 113 return object == null || object == this; 114 } 115 116 /** 117 * Get the "null" string value. 118 * 119 * @return The string "null". 120 */ 121 @Override 122 public String toString() 123 { 124 return "null"; 125 } 126 127 @Override 128 public String toJSONString() 129 { 130 return "null"; 131 } 132 133 // Serialization magic: after de-serializing, it will be back to the singleton instance of NULL. 134 private Object readResolve() throws ObjectStreamException 135 { 136 return NULL; 137 } 138 } 139 140 /** 141 * The map where the JSONObject's properties are kept. 142 */ 143 private final Map<String, Object> properties = new LinkedHashMap<String, Object>(); 144 145 /** 146 * It is sometimes more convenient and less ambiguous to have a <code>NULL</code> object than to use Java's 147 * <code>null</code> value. <code>JSONObject.NULL.equals(null)</code> returns <code>true</code>. 148 * <code>JSONObject.NULL.toString()</code> returns <code>"null"</code>. 149 */ 150 public static final Object NULL = new Null(); 151 152 /** 153 * Construct an empty JSONObject. 154 */ 155 public JSONObject() 156 { 157 } 158 159 /** 160 * Returns a new JSONObject that is a shallow copy of this JSONObject. 161 * 162 * @since 5.4 163 */ 164 public JSONObject copy() 165 { 166 JSONObject dupe = new JSONObject(); 167 dupe.properties.putAll(properties); 168 169 return dupe; 170 } 171 172 /** 173 * Constructs a new JSONObject using a series of String keys and object values. 174 * Object values sholuld be compatible with {@link #put(String, Object)}. Keys must be strings 175 * (toString() will be invoked on each key). 176 * 177 * Prior to release 5.4, keysAndValues was type String...; changing it to Object... makes 178 * it much easier to initialize a JSONObject in a single statement, which is more readable. 179 * 180 * @since 5.2.0 181 */ 182 public JSONObject(Object... keysAndValues) 183 { 184 int i = 0; 185 186 while (i < keysAndValues.length) 187 { 188 put(keysAndValues[i++].toString(), keysAndValues[i++]); 189 } 190 } 191 192 /** 193 * Construct a JSONObject from a subset of another JSONObject. An array of strings is used to identify the keys that 194 * should be copied. Missing keys are ignored. 195 * 196 * @param source 197 * A JSONObject. 198 * @param propertyNames 199 * The strings to copy. 200 * @throws RuntimeException 201 * If a value is a non-finite number. 202 */ 203 public JSONObject(JSONObject source, String... propertyNames) 204 { 205 for (String name : propertyNames) 206 { 207 Object value = source.opt(name); 208 209 if (value != null) 210 put(name, value); 211 } 212 } 213 214 /** 215 * Construct a JSONObject from a JSONTokener. 216 * 217 * @param x 218 * A JSONTokener object containing the source string. @ If there is a syntax error in the source string. 219 */ 220 JSONObject(JSONTokener x) 221 { 222 String key; 223 224 if (x.nextClean() != '{') 225 { 226 throw x.syntaxError("A JSONObject text must begin with '{'"); 227 } 228 229 while (true) 230 { 231 char c = x.nextClean(); 232 switch (c) 233 { 234 case 0: 235 throw x.syntaxError("A JSONObject text must end with '}'"); 236 case '}': 237 return; 238 default: 239 x.back(); 240 key = x.nextValue().toString(); 241 } 242 243 /* 244 * The key is followed by ':'. We will also tolerate '=' or '=>'. 245 */ 246 247 c = x.nextClean(); 248 if (c == '=') 249 { 250 if (x.next() != '>') 251 { 252 x.back(); 253 } 254 } else if (c != ':') 255 { 256 throw x.syntaxError("Expected a ':' after a key"); 257 } 258 put(key, x.nextValue()); 259 260 /* 261 * Pairs are separated by ','. We will also tolerate ';'. 262 */ 263 264 switch (x.nextClean()) 265 { 266 case ';': 267 case ',': 268 if (x.nextClean() == '}') 269 { 270 return; 271 } 272 x.back(); 273 break; 274 case '}': 275 return; 276 default: 277 throw x.syntaxError("Expected a ',' or '}'"); 278 } 279 } 280 } 281 282 /** 283 * Construct a JSONObject from a string. This is the most commonly used JSONObject constructor. 284 * 285 * @param string 286 * A string beginning with <code>{</code> <small>(left brace)</small> and ending with <code>}</code> 287 * <small>(right brace)</small>. 288 * @throws RuntimeException 289 * If there is a syntax error in the source string. 290 */ 291 public JSONObject(String string) 292 { 293 this(new JSONTokener(string)); 294 } 295 296 /** 297 * Accumulate values under a key. It is similar to the put method except that if there is already an object stored 298 * under the key then a JSONArray is stored under the key to hold all of the accumulated values. If there is already 299 * a JSONArray, then the new value is appended to it. In contrast, the put method replaces the previous value. 300 * 301 * @param key 302 * A key string. 303 * @param value 304 * An object to be accumulated under the key. 305 * @return this. 306 * @throws RuntimeException if the value is an invalid number or if the key is null. 307 */ 308 public JSONObject accumulate(String key, Object value) 309 { 310 testValidity(value); 311 312 Object existing = opt(key); 313 314 if (existing == null) 315 { 316 // Note that the original implementation of this method contradicted the method 317 // documentation. 318 put(key, value); 319 return this; 320 } 321 322 if (existing instanceof JSONArray) 323 { 324 ((JSONArray) existing).put(value); 325 return this; 326 } 327 328 // Replace the existing value, of any type, with an array that includes both the 329 // existing and the new value. 330 331 put(key, new JSONArray().put(existing).put(value)); 332 333 return this; 334 } 335 336 /** 337 * Append values to the array under a key. If the key does not exist in the JSONObject, then the key is put in the 338 * JSONObject with its value being a JSONArray containing the value parameter. If the key was already associated 339 * with a JSONArray, then the value parameter is appended to it. 340 * 341 * @param key 342 * A key string. 343 * @param value 344 * An object to be accumulated under the key. 345 * @return this. @ If the key is null or if the current value associated with the key is not a JSONArray. 346 */ 347 public JSONObject append(String key, Object value) 348 { 349 testValidity(value); 350 Object o = opt(key); 351 if (o == null) 352 { 353 put(key, new JSONArray().put(value)); 354 } else if (o instanceof JSONArray) 355 { 356 put(key, ((JSONArray) o).put(value)); 357 } else 358 { 359 throw new RuntimeException("JSONObject[" + quote(key) + "] is not a JSONArray."); 360 } 361 362 return this; 363 } 364 365 /** 366 * Produce a string from a double. The string "null" will be returned if the number is not finite. 367 * 368 * @param d 369 * A double. 370 * @return A String. 371 */ 372 static String doubleToString(double d) 373 { 374 if (Double.isInfinite(d) || Double.isNaN(d)) 375 { 376 return "null"; 377 } 378 379 // Shave off trailing zeros and decimal point, if possible. 380 381 String s = Double.toString(d); 382 if (s.indexOf('.') > 0 && s.indexOf('e') < 0 && s.indexOf('E') < 0) 383 { 384 while (s.endsWith("0")) 385 { 386 s = s.substring(0, s.length() - 1); 387 } 388 if (s.endsWith(".")) 389 { 390 s = s.substring(0, s.length() - 1); 391 } 392 } 393 return s; 394 } 395 396 /** 397 * Get the value object associated with a key. 398 * 399 * @param key 400 * A key string. 401 * @return The object associated with the key. 402 * @throws RuntimeException 403 * if the key is not found. 404 * @see #opt(String) 405 */ 406 public Object get(String key) 407 { 408 Object o = opt(key); 409 if (o == null) 410 { 411 throw new RuntimeException("JSONObject[" + quote(key) + "] not found."); 412 } 413 414 return o; 415 } 416 417 /** 418 * Get the boolean value associated with a key. 419 * 420 * @param key 421 * A key string. 422 * @return The truth. 423 * @throws RuntimeException 424 * if the value does not exist, is not a Boolean or the String "true" or "false". 425 */ 426 public boolean getBoolean(String key) 427 { 428 Object o = get(key); 429 430 if (o instanceof Boolean) 431 return o.equals(Boolean.TRUE); 432 433 if (o instanceof String) 434 { 435 String value = (String) o; 436 437 if (value.equalsIgnoreCase("true")) 438 return true; 439 440 if (value.equalsIgnoreCase("false")) 441 return false; 442 } 443 444 throw new RuntimeException("JSONObject[" + quote(key) + "] is not a Boolean."); 445 } 446 447 /** 448 * Get the double value associated with a key. 449 * 450 * @param key 451 * A key string. 452 * @return The numeric value. @throws RuntimeException if the key is not found or if the value is not a Number object and cannot be 453 * converted to a number. 454 */ 455 public double getDouble(String key) 456 { 457 Object value = get(key); 458 459 try 460 { 461 if (value instanceof Number) 462 return ((Number) value).doubleValue(); 463 464 // This is a bit sloppy for the case where value is not a string. 465 466 return Double.valueOf((String) value); 467 } catch (Exception e) 468 { 469 throw new RuntimeException("JSONObject[" + quote(key) + "] is not a number."); 470 } 471 } 472 473 /** 474 * Get the int value associated with a key. If the number value is too large for an int, it will be clipped. 475 * 476 * @param key 477 * A key string. 478 * @return The integer value. 479 * @throws RuntimeException 480 * if the key is not found or if the value cannot be converted to an integer. 481 */ 482 public int getInt(String key) 483 { 484 Object value = get(key); 485 486 if (value instanceof Number) 487 return ((Number) value).intValue(); 488 489 // Very inefficient way to do this! 490 return (int) getDouble(key); 491 } 492 493 /** 494 * Get the JSONArray value associated with a key. 495 * 496 * @param key 497 * A key string. 498 * @return A JSONArray which is the value. 499 * @throws RuntimeException 500 * if the key is not found or if the value is not a JSONArray. 501 */ 502 public JSONArray getJSONArray(String key) 503 { 504 Object o = get(key); 505 if (o instanceof JSONArray) 506 { 507 return (JSONArray) o; 508 } 509 510 throw new RuntimeException("JSONObject[" + quote(key) + "] is not a JSONArray."); 511 } 512 513 /** 514 * Get the JSONObject value associated with a key. 515 * 516 * @param key 517 * A key string. 518 * @return A JSONObject which is the value. 519 * @throws RuntimeException 520 * if the key is not found or if the value is not a JSONObject. 521 */ 522 public JSONObject getJSONObject(String key) 523 { 524 Object o = get(key); 525 if (o instanceof JSONObject) 526 { 527 return (JSONObject) o; 528 } 529 530 throw new RuntimeException("JSONObject[" + quote(key) + "] is not a JSONObject."); 531 } 532 533 /** 534 * Get the long value associated with a key. If the number value is too long for a long, it will be clipped. 535 * 536 * @param key 537 * A key string. 538 * @return The long value. 539 * @throws RuntimeException 540 * if the key is not found or if the value cannot be converted to a long. 541 */ 542 public long getLong(String key) 543 { 544 Object o = get(key); 545 return o instanceof Number ? ((Number) o).longValue() : (long) getDouble(key); 546 } 547 548 /** 549 * Get the string associated with a key. 550 * 551 * @param key 552 * A key string. 553 * @return A string which is the value. 554 * @throws RuntimeException 555 * if the key is not found. 556 */ 557 public String getString(String key) 558 { 559 return get(key).toString(); 560 } 561 562 /** 563 * Determine if the JSONObject contains a specific key. 564 * 565 * @param key 566 * A key string. 567 * @return true if the key exists in the JSONObject. 568 */ 569 public boolean has(String key) 570 { 571 return properties.containsKey(key); 572 } 573 574 /** 575 * Determine if the value associated with the key is null or if there is no value. 576 * 577 * @param key 578 * A key string. 579 * @return true if there is no value associated with the key or if the value is the JSONObject.NULL object. 580 */ 581 public boolean isNull(String key) 582 { 583 return JSONObject.NULL.equals(opt(key)); 584 } 585 586 /** 587 * Get an enumeration of the keys of the JSONObject. Caution: the set should not be modified. 588 * 589 * @return An iterator of the keys. 590 */ 591 public Set<String> keys() 592 { 593 return properties.keySet(); 594 } 595 596 /** 597 * Get the number of keys stored in the JSONObject. 598 * 599 * @return The number of keys in the JSONObject. 600 */ 601 public int length() 602 { 603 return properties.size(); 604 } 605 606 /** 607 * Produce a JSONArray containing the names of the elements of this JSONObject. 608 * 609 * @return A JSONArray containing the key strings, or null if the JSONObject is empty. 610 */ 611 public JSONArray names() 612 { 613 JSONArray ja = new JSONArray(); 614 615 for (String key : keys()) 616 { 617 ja.put(key); 618 } 619 620 return ja.length() == 0 ? null : ja; 621 } 622 623 /** 624 * Produce a string from a Number. 625 * 626 * @param n 627 * A Number 628 * @return A String. @ If n is a non-finite number. 629 */ 630 static String numberToString(Number n) 631 { 632 assert n != null; 633 634 testValidity(n); 635 636 // Shave off trailing zeros and decimal point, if possible. 637 638 String s = n.toString(); 639 if (s.indexOf('.') > 0 && s.indexOf('e') < 0 && s.indexOf('E') < 0) 640 { 641 while (s.endsWith("0")) 642 { 643 s = s.substring(0, s.length() - 1); 644 } 645 if (s.endsWith(".")) 646 { 647 s = s.substring(0, s.length() - 1); 648 } 649 } 650 return s; 651 } 652 653 /** 654 * Get an optional value associated with a key. 655 * 656 * @param key 657 * A key string. 658 * @return An object which is the value, or null if there is no value. 659 * @see #get(String) 660 */ 661 public Object opt(String key) 662 { 663 return properties.get(key); 664 } 665 666 /** 667 * Put a key/value pair in the JSONObject. If the value is null, then the key will be removed from the JSONObject if 668 * it is present. 669 * 670 * @param key 671 * A key string. 672 * @param value 673 * An object which is the value. It should be of one of these types: Boolean, Double, Integer, 674 * JSONArray, JSONObject, JSONLiteral, Long, String, or the JSONObject.NULL object. 675 * @return this. 676 * @throws RuntimeException 677 * If the value is non-finite number or if the key is null. 678 */ 679 public JSONObject put(String key, Object value) 680 { 681 assert key != null; 682 683 if (value != null) 684 { 685 testValidity(value); 686 properties.put(key, value); 687 } else 688 { 689 remove(key); 690 } 691 692 return this; 693 } 694 695 /** 696 * Produce a string in double quotes with backslash sequences in all the right places, 697 * allowing JSON text to be delivered in HTML. In JSON text, a string cannot contain a control character 698 * or an unescaped quote or backslash. 699 * 700 * @param string 701 * A String 702 * @return A String correctly formatted for insertion in a JSON text. 703 */ 704 public static String quote(String string) 705 { 706 if (string == null || string.length() == 0) 707 { 708 return "\"\""; 709 } 710 711 char b; 712 char c = 0; 713 int i; 714 int len = string.length(); 715 StringBuilder buffer = new StringBuilder(len + 4); 716 String t; 717 718 buffer.append('"'); 719 for (i = 0; i < len; i += 1) 720 { 721 b = c; 722 c = string.charAt(i); 723 switch (c) 724 { 725 case '\\': 726 case '"': 727 buffer.append('\\'); 728 buffer.append(c); 729 break; 730 case '/': 731 if (b == '<') 732 { 733 buffer.append('\\'); 734 } 735 buffer.append(c); 736 break; 737 case '\b': 738 buffer.append("\\b"); 739 break; 740 case '\t': 741 buffer.append("\\t"); 742 break; 743 case '\n': 744 buffer.append("\\n"); 745 break; 746 case '\f': 747 buffer.append("\\f"); 748 break; 749 case '\r': 750 buffer.append("\\r"); 751 break; 752 default: 753 if (c < ' ' || (c >= '\u0080' && c < '\u00a0') || (c >= '\u2000' && c < '\u2100')) 754 { 755 t = "000" + Integer.toHexString(c); 756 buffer.append("\\u").append(t.substring(t.length() - 4)); 757 } else 758 { 759 buffer.append(c); 760 } 761 } 762 } 763 buffer.append('"'); 764 return buffer.toString(); 765 } 766 767 /** 768 * Remove a name and its value, if present. 769 * 770 * @param key 771 * The name to be removed. 772 * @return The value that was associated with the name, or null if there was no value. 773 */ 774 public Object remove(String key) 775 { 776 return properties.remove(key); 777 } 778 779 private static final Class[] ALLOWED = new Class[] 780 {String.class, Boolean.class, Number.class, JSONObject.class, JSONArray.class, JSONString.class, 781 JSONLiteral.class, Null.class}; 782 783 /** 784 * Throw an exception if the object is an NaN or infinite number, or not a type which may be stored. 785 * 786 * @param value 787 * The object to test. @ If o is a non-finite number. 788 */ 789 @SuppressWarnings("unchecked") 790 static void testValidity(Object value) 791 { 792 if (value == null) 793 throw new IllegalArgumentException("null isn't valid in JSONObject and JSONArray. Use JSONObject.NULL instead."); 794 795 boolean found = false; 796 Class actual = value.getClass(); 797 798 for (Class allowed : ALLOWED) 799 { 800 if (allowed.isAssignableFrom(actual)) 801 { 802 found = true; 803 break; 804 } 805 } 806 807 if (!found) 808 { 809 List<String> typeNames = new ArrayList<String>(); 810 811 for (Class c : ALLOWED) 812 { 813 String name = c.getName(); 814 815 if (name.startsWith("java.lang.")) 816 name = name.substring(10); 817 818 typeNames.add(name); 819 } 820 821 Collections.sort(typeNames); 822 823 StringBuilder joined = new StringBuilder(); 824 String sep = ""; 825 826 for (String name : typeNames) 827 { 828 joined.append(sep); 829 joined.append(name); 830 831 sep = ", "; 832 } 833 834 String message = String.format("JSONObject properties may be one of %s. Type %s is not allowed.", 835 joined.toString(), actual.getName()); 836 837 throw new RuntimeException(message); 838 } 839 840 if (value instanceof Double) 841 { 842 Double asDouble = (Double) value; 843 844 if (asDouble.isInfinite() || asDouble.isNaN()) 845 { 846 throw new RuntimeException( 847 "JSON does not allow non-finite numbers."); 848 } 849 850 return; 851 } 852 853 if (value instanceof Float) 854 { 855 Float asFloat = (Float) value; 856 857 if (asFloat.isInfinite() || asFloat.isNaN()) 858 { 859 throw new RuntimeException( 860 "JSON does not allow non-finite numbers."); 861 } 862 863 } 864 865 } 866 867 /** 868 * Prints the JSONObject using the session. 869 * 870 * @since 5.2.0 871 */ 872 @Override 873 void print(JSONPrintSession session) 874 { 875 session.printSymbol('{'); 876 877 session.indent(); 878 879 boolean comma = false; 880 881 for (String key : keys()) 882 { 883 if (comma) 884 session.printSymbol(','); 885 886 session.newline(); 887 888 session.printQuoted(key); 889 890 session.printSymbol(':'); 891 892 printValue(session, properties.get(key)); 893 894 comma = true; 895 } 896 897 session.outdent(); 898 899 if (comma) 900 session.newline(); 901 902 session.printSymbol('}'); 903 } 904 905 /** 906 * Prints a value (a JSONArray or JSONObject, or a value stored in an array or object) using 907 * the session. 908 * 909 * @since 5.2.0 910 */ 911 static void printValue(JSONPrintSession session, Object value) 912 { 913 914 if (value instanceof JSONObject) 915 { 916 ((JSONObject) value).print(session); 917 return; 918 } 919 920 if (value instanceof JSONArray) 921 { 922 ((JSONArray) value).print(session); 923 return; 924 } 925 926 if (value instanceof JSONString) 927 { 928 String printValue = ((JSONString) value).toJSONString(); 929 930 session.print(printValue); 931 932 return; 933 } 934 935 if (value instanceof Number) 936 { 937 String printValue = numberToString((Number) value); 938 session.print(printValue); 939 return; 940 } 941 942 if (value instanceof Boolean) 943 { 944 session.print(value.toString()); 945 946 return; 947 } 948 949 // Otherwise it really should just be a string. Nothing else can go in. 950 session.printQuoted(value.toString()); 951 } 952 953 /** 954 * Returns true if the other object is a JSONObject and its set of properties matches this object's properties. 955 */ 956 @Override 957 public boolean equals(Object obj) 958 { 959 if (obj == null) 960 return false; 961 962 if (!(obj instanceof JSONObject)) 963 return false; 964 965 JSONObject other = (JSONObject) obj; 966 967 return properties.equals(other.properties); 968 } 969 970 /** 971 * Returns a Map of the keys and values of the JSONObject. The returned map is unmodifiable. 972 * Note that changes to the JSONObject will be reflected in the map. In addition, null values in the JSONObject 973 * are represented as {@link JSONObject#NULL} in the map. 974 * 975 * @return unmodifiable map of properties and values 976 * @since 5.4 977 */ 978 public Map<String, Object> toMap() 979 { 980 return Collections.unmodifiableMap(properties); 981 } 982 983 /** 984 * Invokes {@link #put(String, Object)} for each value from the map. 985 * 986 * @param newProperties 987 * to add to this JSONObject 988 * @return this JSONObject 989 * @since 5.4 990 */ 991 public JSONObject putAll(Map<String, ?> newProperties) 992 { 993 assert newProperties != null; 994 995 for (Map.Entry<String, ?> e : newProperties.entrySet()) 996 { 997 put(e.getKey(), e.getValue()); 998 } 999 1000 return this; 1001 } 1002 1003 /** 1004 * Navigates into a nested JSONObject, creating the JSONObject if necessary. They key must not exist, 1005 * or must be a JSONObject. 1006 * 1007 * @param key 1008 * @return the nested JSONObject 1009 * @throws IllegalStateException 1010 * if the current value for the key is not null and not JSONObject 1011 */ 1012 public JSONObject in(String key) 1013 { 1014 assert key != null; 1015 1016 Object nested = properties.get(key); 1017 1018 if (nested != null && !(nested instanceof JSONObject)) 1019 { 1020 throw new IllegalStateException(String.format("JSONObject[%s] is not a JSONObject.", quote(key))); 1021 } 1022 1023 if (nested == null) 1024 { 1025 nested = new JSONObject(); 1026 properties.put(key, nested); 1027 } 1028 1029 return (JSONObject) nested; 1030 } 1031}