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