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 }