001/* 002 * Copyright 2015 UnboundID Corp. 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright (C) 2015 UnboundID Corp. 007 * 008 * This program is free software; you can redistribute it and/or modify 009 * it under the terms of the GNU General Public License (GPLv2 only) 010 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only) 011 * as published by the Free Software Foundation. 012 * 013 * This program is distributed in the hope that it will be useful, 014 * but WITHOUT ANY WARRANTY; without even the implied warranty of 015 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 016 * GNU General Public License for more details. 017 * 018 * You should have received a copy of the GNU General Public License 019 * along with this program; if not, see <http://www.gnu.org/licenses>. 020 */ 021package com.unboundid.util.json; 022 023 024 025import java.util.ArrayList; 026import java.util.Collections; 027import java.util.HashMap; 028import java.util.Iterator; 029import java.util.LinkedHashMap; 030import java.util.Map; 031import java.util.TreeMap; 032 033import com.unboundid.util.Debug; 034import com.unboundid.util.NotMutable; 035import com.unboundid.util.StaticUtils; 036import com.unboundid.util.ThreadSafety; 037import com.unboundid.util.ThreadSafetyLevel; 038 039import static com.unboundid.util.json.JSONMessages.*; 040 041 042 043/** 044 * This class provides an implementation of a JSON value that represents an 045 * object with zero or more name-value pairs. In each pair, the name is a JSON 046 * string and the value is any type of JSON value ({@code null}, {@code true}, 047 * {@code false}, number, string, array, or object). Although the ECMA-404 048 * specification does not explicitly forbid a JSON object from having multiple 049 * fields with the same name, RFC 7159 section 4 states that field names should 050 * be unique, and this implementation does not support objects in which multiple 051 * fields have the same name. Note that this uniqueness constraint only applies 052 * to the fields directly contained within an object, and does not prevent an 053 * object from having a field value that is an object (or that is an array 054 * containing one or more objects) that use a field name that is also in use 055 * in the outer object. Similarly, if an array contains multiple JSON objects, 056 * then there is no restriction preventing the same field names from being 057 * used in separate objects within that array. 058 * <BR><BR> 059 * The string representation of a JSON object is an open curly brace (U+007B) 060 * followed by a comma-delimited list of the name-value pairs that comprise the 061 * fields in that object and a closing curly brace (U+007D). Each name-value 062 * pair is represented as a JSON string followed by a colon and the appropriate 063 * string representation of the value. There must not be a comma between the 064 * last field and the closing curly brace. There may optionally be any amount 065 * of whitespace (where whitespace characters include the ASCII space, 066 * horizontal tab, line feed, and carriage return characters) after the open 067 * curly brace, on either or both sides of the colon separating a field name 068 * from its value, on either or both sides of commas separating fields, and 069 * before the closing curly brace. The order in which fields appear in the 070 * string representation is not considered significant. 071 * <BR><BR> 072 * The string representation returned by the {@link #toString()} method (or 073 * appended to the buffer provided to the {@link #toString(StringBuilder)} 074 * method) will include one space before each field name and one space before 075 * the closing curly brace. There will not be any space on either side of the 076 * colon separating the field name from its value, and there will not be any 077 * space between a field value and the comma that follows it. The string 078 * representation of each field name will use the same logic as the 079 * {@link JSONString#toString()} method, and the string representation of each 080 * field value will be obtained using that value's {@code toString} method. 081 * <BR><BR> 082 * The normalized string representation will not include any optional spaces, 083 * and the normalized string representation of each field value will be obtained 084 * using that value's {@code toNormalizedString} method. Field names will be 085 * treated in a case-sensitive manner, but all characters outside the LDAP 086 * printable character set will be escaped using the {@code \}{@code u}-style 087 * Unicode encoding. The normalized string representation will have fields 088 * listed in lexicographic order. 089 */ 090@NotMutable() 091@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE) 092public final class JSONObject 093 extends JSONValue 094{ 095 /** 096 * A pre-allocated empty JSON object. 097 */ 098 public static final JSONObject EMPTY_OBJECT = new JSONObject( 099 Collections.<String,JSONValue>emptyMap()); 100 101 102 103 /** 104 * The serial version UID for this serializable class. 105 */ 106 private static final long serialVersionUID = -4209509956709292141L; 107 108 109 110 // A counter to use in decode processing. 111 private int decodePos; 112 113 // The hash code for this JSON object. 114 private Integer hashCode; 115 116 // The set of fields for this JSON object. 117 private final Map<String,JSONValue> fields; 118 119 // The string representation for this JSON object. 120 private String stringRepresentation; 121 122 // A buffer to use in decode processing. 123 private final StringBuilder decodeBuffer; 124 125 126 127 /** 128 * Creates a new JSON object with the provided fields. 129 * 130 * @param fields The fields to include in this JSON object. It may be 131 * {@code null} or empty if this object should not have any 132 * fields. 133 */ 134 public JSONObject(final JSONField... fields) 135 { 136 if ((fields == null) || (fields.length == 0)) 137 { 138 this.fields = Collections.emptyMap(); 139 } 140 else 141 { 142 final LinkedHashMap<String,JSONValue> m = 143 new LinkedHashMap<String,JSONValue>(fields.length); 144 for (final JSONField f : fields) 145 { 146 m.put(f.getName(), f.getValue()); 147 } 148 this.fields = Collections.unmodifiableMap(m); 149 } 150 151 hashCode = null; 152 stringRepresentation = null; 153 154 // We don't need to decode anything. 155 decodePos = -1; 156 decodeBuffer = null; 157 } 158 159 160 161 /** 162 * Creates a new JSON object with the provided fields. 163 * 164 * @param fields The set of fields for this JSON object. It may be 165 * {@code null} or empty if there should not be any fields. 166 */ 167 public JSONObject(final Map<String,JSONValue> fields) 168 { 169 if (fields == null) 170 { 171 this.fields = Collections.emptyMap(); 172 } 173 else 174 { 175 this.fields = Collections.unmodifiableMap( 176 new LinkedHashMap<String,JSONValue>(fields)); 177 } 178 179 hashCode = null; 180 stringRepresentation = null; 181 182 // We don't need to decode anything. 183 decodePos = -1; 184 decodeBuffer = null; 185 } 186 187 188 189 /** 190 * Creates a new JSON object parsed from the provided string. 191 * 192 * @param stringRepresentation The string to parse as a JSON object. It 193 * must represent exactly one JSON object. 194 * 195 * @throws JSONException If the provided string cannot be parsed as a valid 196 * JSON object. 197 */ 198 public JSONObject(final String stringRepresentation) 199 throws JSONException 200 { 201 this.stringRepresentation = stringRepresentation; 202 203 final char[] chars = stringRepresentation.toCharArray(); 204 decodePos = 0; 205 decodeBuffer = new StringBuilder(chars.length); 206 207 // The JSON object must start with an open curly brace. 208 final Object firstToken = readToken(chars); 209 if (! firstToken.equals('{')) 210 { 211 throw new JSONException(ERR_OBJECT_DOESNT_START_WITH_BRACE.get( 212 stringRepresentation)); 213 } 214 215 final LinkedHashMap<String,JSONValue> m = 216 new LinkedHashMap<String,JSONValue>(10); 217 readObject(chars, m); 218 fields = Collections.unmodifiableMap(m); 219 220 skipWhitespace(chars); 221 if (decodePos < chars.length) 222 { 223 throw new JSONException(ERR_OBJECT_DATA_BEYOND_END.get( 224 stringRepresentation, decodePos)); 225 } 226 } 227 228 229 230 /** 231 * Reads a token from the provided character array, skipping over any 232 * insignificant whitespace that may be before the token. The token that is 233 * returned will be one of the following: 234 * <UL> 235 * <LI>A {@code Character} that is an opening curly brace.</LI> 236 * <LI>A {@code Character} that is a closing curly brace.</LI> 237 * <LI>A {@code Character} that is an opening square bracket.</LI> 238 * <LI>A {@code Character} that is a closing square bracket.</LI> 239 * <LI>A {@code Character} that is a colon.</LI> 240 * <LI>A {@code Character} that is a comma.</LI> 241 * <LI>A {@link JSONBoolean}.</LI> 242 * <LI>A {@link JSONNull}.</LI> 243 * <LI>A {@link JSONNumber}.</LI> 244 * <LI>A {@link JSONString}.</LI> 245 * </UL> 246 * 247 * @param chars The characters that comprise the string representation of 248 * the JSON object. 249 * 250 * @return The token that was read. 251 * 252 * @throws JSONException If a problem was encountered while reading the 253 * token. 254 */ 255 private Object readToken(final char[] chars) 256 throws JSONException 257 { 258 skipWhitespace(chars); 259 260 final char c = readCharacter(chars, false); 261 switch (c) 262 { 263 case '{': 264 case '}': 265 case '[': 266 case ']': 267 case ':': 268 case ',': 269 // This is a token character that we will return as-is. 270 decodePos++; 271 return c; 272 273 case '"': 274 // This is the start of a JSON string. 275 return readString(chars); 276 277 case 't': 278 case 'f': 279 // This is the start of a JSON true or false value. 280 return readBoolean(chars); 281 282 case 'n': 283 // This is the start of a JSON null value. 284 return readNull(chars); 285 286 case '-': 287 case '0': 288 case '1': 289 case '2': 290 case '3': 291 case '4': 292 case '5': 293 case '6': 294 case '7': 295 case '8': 296 case '9': 297 // This is the start of a JSON number value. 298 return readNumber(chars); 299 300 default: 301 // This is not a valid JSON token. 302 throw new JSONException(ERR_OBJECT_INVALID_FIRST_TOKEN_CHAR.get( 303 new String(chars), String.valueOf(c), decodePos)); 304 305 } 306 } 307 308 309 310 /** 311 * Skips over any valid JSON whitespace at the current position in the 312 * provided array. 313 * 314 * @param chars The characters that comprise the string representation of 315 * the JSON object. 316 * 317 * @throws JSONException If a problem is encountered while skipping 318 * whitespace. 319 */ 320 private void skipWhitespace(final char[] chars) 321 throws JSONException 322 { 323 while (decodePos < chars.length) 324 { 325 switch (chars[decodePos]) 326 { 327 // The space, tab, newline, and carriage return characters are 328 // considered valid JSON whitespace. 329 case ' ': 330 case '\t': 331 case '\n': 332 case '\r': 333 decodePos++; 334 break; 335 336 // Technically, JSON does not provide support for comments. But this 337 // implementation will accept two types of comments: 338 // - Comments that start with /* and end with */ (potentially spanning 339 // multiple lines). 340 // - Comments that start with // and continue until the end of the line. 341 // All comments will be ignored by the parser. 342 case '/': 343 final int commentStartPos = decodePos; 344 if ((decodePos+1) >= chars.length) 345 { 346 return; 347 } 348 else if (chars[decodePos+1] == '/') 349 { 350 decodePos += 2; 351 352 // Keep reading until we encounter a newline or carriage return, or 353 // until we hit the end of the string. 354 while (decodePos < chars.length) 355 { 356 if ((chars[decodePos] == '\n') || (chars[decodePos] == '\r')) 357 { 358 break; 359 } 360 decodePos++; 361 } 362 break; 363 } 364 else if (chars[decodePos+1] == '*') 365 { 366 decodePos += 2; 367 368 // Keep reading until we encounter "*/". We must encounter "*/" 369 // before hitting the end of the string. 370 boolean closeFound = false; 371 while (decodePos < chars.length) 372 { 373 if (chars[decodePos] == '*') 374 { 375 if (((decodePos+1) < chars.length) && 376 (chars[decodePos+1] == '/')) 377 { 378 closeFound = true; 379 decodePos += 2; 380 break; 381 } 382 } 383 decodePos++; 384 } 385 386 if (! closeFound) 387 { 388 throw new JSONException(ERR_OBJECT_UNCLOSED_COMMENT.get( 389 new String(chars), commentStartPos)); 390 } 391 break; 392 } 393 else 394 { 395 return; 396 } 397 398 default: 399 return; 400 } 401 } 402 } 403 404 405 406 /** 407 * Reads the character at the specified position and optionally advances the 408 * position. 409 * 410 * @param chars The characters that comprise the string 411 * representation of the JSON object. 412 * @param advancePosition Indicates whether to advance the value of the 413 * position indicator after reading the character. 414 * If this is {@code false}, then this method will be 415 * used to "peek" at the next character without 416 * consuming it. 417 * 418 * @return The character that was read. 419 * 420 * @throws JSONException If the end of the value was encountered when a 421 * character was expected. 422 */ 423 private char readCharacter(final char[] chars, final boolean advancePosition) 424 throws JSONException 425 { 426 if (decodePos >= chars.length) 427 { 428 throw new JSONException( 429 ERR_OBJECT_UNEXPECTED_END_OF_STRING.get(new String(chars))); 430 } 431 432 final char c = chars[decodePos]; 433 if (advancePosition) 434 { 435 decodePos++; 436 } 437 return c; 438 } 439 440 441 442 /** 443 * Reads a JSON string staring at the specified position in the provided 444 * character array. 445 * 446 * @param chars The characters that comprise the string representation of 447 * the JSON object. 448 * 449 * @return The JSON string that was read. 450 * 451 * @throws JSONException If a problem was encountered while reading the JSON 452 * string. 453 */ 454 private JSONString readString(final char[] chars) 455 throws JSONException 456 { 457 // Create a buffer to hold the string. Note that if we've gotten here then 458 // we already know that the character at the provided position is a quote, 459 // so we can read past it in the process. 460 final int startPos = decodePos++; 461 decodeBuffer.setLength(0); 462 while (true) 463 { 464 final char c = readCharacter(chars, true); 465 if (c == '\\') 466 { 467 final int escapedCharPos = decodePos; 468 final char escapedChar = readCharacter(chars, true); 469 switch (escapedChar) 470 { 471 case '"': 472 case '\\': 473 case '/': 474 decodeBuffer.append(escapedChar); 475 break; 476 case 'b': 477 decodeBuffer.append('\b'); 478 break; 479 case 'f': 480 decodeBuffer.append('\f'); 481 break; 482 case 'n': 483 decodeBuffer.append('\n'); 484 break; 485 case 'r': 486 decodeBuffer.append('\r'); 487 break; 488 case 't': 489 decodeBuffer.append('\t'); 490 break; 491 492 case 'u': 493 final char[] hexChars = 494 { 495 readCharacter(chars, true), 496 readCharacter(chars, true), 497 readCharacter(chars, true), 498 readCharacter(chars, true) 499 }; 500 try 501 { 502 decodeBuffer.append( 503 (char) Integer.parseInt(new String(hexChars), 16)); 504 } 505 catch (final Exception e) 506 { 507 Debug.debugException(e); 508 throw new JSONException( 509 ERR_OBJECT_INVALID_UNICODE_ESCAPE.get(new String(chars), 510 escapedCharPos), 511 e); 512 } 513 break; 514 515 default: 516 throw new JSONException(ERR_OBJECT_INVALID_ESCAPED_CHAR.get( 517 new String(chars), escapedChar, escapedCharPos)); 518 } 519 } 520 else if (c == '"') 521 { 522 return new JSONString(decodeBuffer.toString(), 523 new String(chars, startPos, (decodePos - startPos))); 524 } 525 else 526 { 527 if (c <= '\u001F') 528 { 529 throw new JSONException(ERR_OBJECT_UNESCAPED_CONTROL_CHAR.get( 530 new String(chars), String.format("%04X", (int) c), 531 (decodePos - 1))); 532 } 533 534 decodeBuffer.append(c); 535 } 536 } 537 } 538 539 540 541 /** 542 * Reads a JSON Boolean staring at the specified position in the provided 543 * character array. 544 * 545 * @param chars The characters that comprise the string representation of 546 * the JSON object. 547 * 548 * @return The JSON Boolean that was read. 549 * 550 * @throws JSONException If a problem was encountered while reading the JSON 551 * Boolean. 552 */ 553 private JSONBoolean readBoolean(final char[] chars) 554 throws JSONException 555 { 556 final int startPos = decodePos; 557 final char firstCharacter = readCharacter(chars, true); 558 if (firstCharacter == 't') 559 { 560 if ((readCharacter(chars, true) == 'r') && 561 (readCharacter(chars, true) == 'u') && 562 (readCharacter(chars, true) == 'e')) 563 { 564 return JSONBoolean.TRUE; 565 } 566 } 567 else if (firstCharacter == 'f') 568 { 569 if ((readCharacter(chars, true) == 'a') && 570 (readCharacter(chars, true) == 'l') && 571 (readCharacter(chars, true) == 's') && 572 (readCharacter(chars, true) == 'e')) 573 { 574 return JSONBoolean.FALSE; 575 } 576 } 577 578 throw new JSONException(ERR_OBJECT_UNABLE_TO_PARSE_BOOLEAN.get( 579 new String(chars), startPos)); 580 } 581 582 583 584 /** 585 * Reads a JSON null staring at the specified position in the provided 586 * character array. 587 * 588 * @param chars The characters that comprise the string representation of 589 * the JSON object. 590 * 591 * @return The JSON null that was read. 592 * 593 * @throws JSONException If a problem was encountered while reading the JSON 594 * null. 595 */ 596 private JSONNull readNull(final char[] chars) 597 throws JSONException 598 { 599 final int startPos = decodePos; 600 if ((readCharacter(chars, true) == 'n') && 601 (readCharacter(chars, true) == 'u') && 602 (readCharacter(chars, true) == 'l') && 603 (readCharacter(chars, true) == 'l')) 604 { 605 return JSONNull.NULL; 606 } 607 608 throw new JSONException(ERR_OBJECT_UNABLE_TO_PARSE_NULL.get( 609 new String(chars), startPos)); 610 } 611 612 613 614 /** 615 * Reads a JSON number staring at the specified position in the provided 616 * character array. 617 * 618 * @param chars The characters that comprise the string representation of 619 * the JSON object. 620 * 621 * @return The JSON number that was read. 622 * 623 * @throws JSONException If a problem was encountered while reading the JSON 624 * number. 625 */ 626 private JSONNumber readNumber(final char[] chars) 627 throws JSONException 628 { 629 // Read until we encounter whitespace, a comma, a closing square bracket, or 630 // a closing curly brace. Then try to parse what we read as a number. 631 final int startPos = decodePos; 632 decodeBuffer.setLength(0); 633 634 while (true) 635 { 636 final char c = readCharacter(chars, true); 637 switch (c) 638 { 639 case ' ': 640 case '\t': 641 case '\n': 642 case '\r': 643 case ',': 644 case ']': 645 case '}': 646 // We need to decrement the position indicator since the last one we 647 // read wasn't part of the number. 648 decodePos--; 649 return new JSONNumber(decodeBuffer.toString()); 650 651 default: 652 decodeBuffer.append(c); 653 } 654 } 655 } 656 657 658 659 /** 660 * Reads a JSON array starting at the specified position in the provided 661 * character array. Note that this method assumes that the opening square 662 * bracket has already been read. 663 * 664 * @param chars The characters that comprise the string representation of 665 * the JSON object. 666 * 667 * @return The JSON array that was read. 668 * 669 * @throws JSONException If a problem was encountered while reading the JSON 670 * array. 671 */ 672 private JSONArray readArray(final char[] chars) 673 throws JSONException 674 { 675 // The opening square bracket will have already been consumed, so read 676 // JSON values until we hit a closing square bracket. 677 final ArrayList<JSONValue> values = new ArrayList<JSONValue>(10); 678 boolean firstToken = true; 679 while (true) 680 { 681 // If this is the first time through, it is acceptable to find a closing 682 // square bracket. Otherwise, we expect to find a JSON value, an opening 683 // square bracket to denote the start of an embedded array, or an opening 684 // curly brace to denote the start of an embedded JSON object. 685 int p = decodePos; 686 Object token = readToken(chars); 687 if (token instanceof JSONValue) 688 { 689 values.add((JSONValue) token); 690 } 691 else if (token.equals('[')) 692 { 693 values.add(readArray(chars)); 694 } 695 else if (token.equals('{')) 696 { 697 final LinkedHashMap<String,JSONValue> fieldMap = 698 new LinkedHashMap<String,JSONValue>(10); 699 values.add(readObject(chars, fieldMap)); 700 } 701 else if (token.equals(']') && firstToken) 702 { 703 // It's an empty array. 704 return JSONArray.EMPTY_ARRAY; 705 } 706 else 707 { 708 throw new JSONException( 709 ERR_OBJECT_INVALID_TOKEN_WHEN_ARRAY_VALUE_EXPECTED.get( 710 new String(chars), String.valueOf(token), p)); 711 } 712 713 firstToken = false; 714 715 716 // If we've gotten here, then we found a JSON value. It must be followed 717 // by either a comma (to indicate that there's at least one more value) or 718 // a closing square bracket (to denote the end of the array). 719 p = decodePos; 720 token = readToken(chars); 721 if (token.equals(']')) 722 { 723 return new JSONArray(values); 724 } 725 else if (! token.equals(',')) 726 { 727 throw new JSONException( 728 ERR_OBJECT_INVALID_TOKEN_WHEN_ARRAY_COMMA_OR_BRACKET_EXPECTED.get( 729 new String(chars), String.valueOf(token), p)); 730 } 731 } 732 } 733 734 735 736 /** 737 * Reads a JSON object starting at the specified position in the provided 738 * character array. Note that this method assumes that the opening curly 739 * brace has already been read. 740 * 741 * @param chars The characters that comprise the string representation of 742 * the JSON object. 743 * @param fields The map into which to place the fields that are read. The 744 * returned object will include an unmodifiable view of this 745 * map, but the caller may use the map directly if desired. 746 * 747 * @return The JSON object that was read. 748 * 749 * @throws JSONException If a problem was encountered while reading the JSON 750 * object. 751 */ 752 private JSONObject readObject(final char[] chars, 753 final Map<String,JSONValue> fields) 754 throws JSONException 755 { 756 boolean firstField = true; 757 while (true) 758 { 759 // Read the next token. It must be a JSONString, unless we haven't read 760 // any fields yet in which case it can be a closing curly brace to 761 // indicate that it's an empty object. 762 int p = decodePos; 763 final String fieldName; 764 Object token = readToken(chars); 765 if (token instanceof JSONString) 766 { 767 fieldName = ((JSONString) token).stringValue(); 768 if (fields.containsKey(fieldName)) 769 { 770 throw new JSONException(ERR_OBJECT_DUPLICATE_FIELD.get( 771 new String(chars), fieldName)); 772 } 773 } 774 else if (firstField && token.equals('}')) 775 { 776 return new JSONObject(fields); 777 } 778 else 779 { 780 throw new JSONException(ERR_OBJECT_EXPECTED_STRING.get( 781 new String(chars), String.valueOf(token), p)); 782 } 783 firstField = false; 784 785 // Read the next token. It must be a colon. 786 p = decodePos; 787 token = readToken(chars); 788 if (! token.equals(':')) 789 { 790 throw new JSONException(ERR_OBJECT_EXPECTED_COLON.get(new String(chars), 791 String.valueOf(token), p)); 792 } 793 794 // Read the next token. It must be one of the following: 795 // - A JSONValue 796 // - An opening square bracket, designating the start of an array. 797 // - An opening curly brace, designating the start of an object. 798 p = decodePos; 799 token = readToken(chars); 800 if (token instanceof JSONValue) 801 { 802 fields.put(fieldName, (JSONValue) token); 803 } 804 else if (token.equals('[')) 805 { 806 final JSONArray a = readArray(chars); 807 fields.put(fieldName, a); 808 } 809 else if (token.equals('{')) 810 { 811 final LinkedHashMap<String,JSONValue> m = 812 new LinkedHashMap<String,JSONValue>(10); 813 final JSONObject o = readObject(chars, m); 814 fields.put(fieldName, o); 815 } 816 else 817 { 818 throw new JSONException(ERR_OBJECT_EXPECTED_VALUE.get(new String(chars), 819 String.valueOf(token), p, fieldName)); 820 } 821 822 // Read the next token. It must be either a comma (to indicate that 823 // there will be another field) or a closing curly brace (to indicate 824 // that the end of the object has been reached). 825 p = decodePos; 826 token = readToken(chars); 827 if (token.equals('}')) 828 { 829 return new JSONObject(fields); 830 } 831 else if (! token.equals(',')) 832 { 833 throw new JSONException(ERR_OBJECT_EXPECTED_COMMA_OR_CLOSE_BRACE.get( 834 new String(chars), String.valueOf(token), p)); 835 } 836 } 837 } 838 839 840 841 /** 842 * Retrieves a map of the fields contained in this JSON object. 843 * 844 * @return A map of the fields contained in this JSON object. 845 */ 846 public Map<String,JSONValue> getFields() 847 { 848 return fields; 849 } 850 851 852 853 /** 854 * Retrieves the value for the specified field. 855 * 856 * @param name The name of the field for which to retrieve the value. It 857 * will be treated in a case-sensitive manner. 858 * 859 * @return The value for the specified field, or {@code null} if the 860 * requested field is not present in the JSON object. 861 */ 862 public JSONValue getField(final String name) 863 { 864 return fields.get(name); 865 } 866 867 868 869 /** 870 * {@inheritDoc} 871 */ 872 @Override() 873 public int hashCode() 874 { 875 if (hashCode == null) 876 { 877 int hc = 0; 878 for (final Map.Entry<String,JSONValue> e : fields.entrySet()) 879 { 880 hc += e.getKey().hashCode() + e.getValue().hashCode(); 881 } 882 883 hashCode = hc; 884 } 885 886 return hashCode; 887 } 888 889 890 891 /** 892 * {@inheritDoc} 893 */ 894 @Override() 895 public boolean equals(final Object o) 896 { 897 if (o == this) 898 { 899 return true; 900 } 901 902 if (o instanceof JSONObject) 903 { 904 final JSONObject obj = (JSONObject) o; 905 return fields.equals(obj.fields); 906 } 907 908 return false; 909 } 910 911 912 913 /** 914 * Indicates whether this JSON object is considered equal to the provided 915 * object, subject to the specified constraints. 916 * 917 * @param o The object to compare against this JSON 918 * object. It must not be {@code null}. 919 * @param ignoreFieldNameCase Indicates whether to ignore differences in 920 * capitalization in field names. 921 * @param ignoreValueCase Indicates whether to ignore differences in 922 * capitalization in values that are JSON 923 * strings. 924 * @param ignoreArrayOrder Indicates whether to ignore differences in the 925 * order of elements within an array. 926 * 927 * @return {@code true} if this JSON object is considered equal to the 928 * provided object (subject to the specified constraints), or 929 * {@code false} if not. 930 */ 931 public boolean equals(final JSONObject o, final boolean ignoreFieldNameCase, 932 final boolean ignoreValueCase, 933 final boolean ignoreArrayOrder) 934 { 935 // See if we can do a straight-up Map.equals. If so, just do that. 936 if ((! ignoreFieldNameCase) && (! ignoreValueCase) && (! ignoreArrayOrder)) 937 { 938 return fields.equals(o.fields); 939 } 940 941 // Make sure they have the same number of fields. 942 if (fields.size() != o.fields.size()) 943 { 944 return false; 945 } 946 947 // Optimize for the case in which we field names are case sensitive. 948 if (! ignoreFieldNameCase) 949 { 950 for (final Map.Entry<String,JSONValue> e : fields.entrySet()) 951 { 952 final JSONValue thisValue = e.getValue(); 953 final JSONValue thatValue = o.fields.get(e.getKey()); 954 if (thatValue == null) 955 { 956 return false; 957 } 958 959 if (! thisValue.equals(thatValue, ignoreFieldNameCase, ignoreValueCase, 960 ignoreArrayOrder)) 961 { 962 return false; 963 } 964 } 965 966 return true; 967 } 968 969 970 // If we've gotten here, then we know that we need to treat field names in 971 // a case-insensitive manner. Create a new map that we can remove fields 972 // from as we find matches. This can help avoid false-positive matches in 973 // which multiple fields in the first map match the same field in the second 974 // map (e.g., because they have field names that differ only in case and 975 // values that are logically equivalent). It also makes iterating through 976 // the values faster as we make more progress. 977 final HashMap<String,JSONValue> thatMap = 978 new HashMap<String,JSONValue>(o.fields); 979 final Iterator<Map.Entry<String,JSONValue>> thisIterator = 980 fields.entrySet().iterator(); 981 while (thisIterator.hasNext()) 982 { 983 final Map.Entry<String,JSONValue> thisEntry = thisIterator.next(); 984 final String thisFieldName = thisEntry.getKey(); 985 final JSONValue thisValue = thisEntry.getValue(); 986 987 final Iterator<Map.Entry<String,JSONValue>> thatIterator = 988 thatMap.entrySet().iterator(); 989 990 boolean found = false; 991 while (thatIterator.hasNext()) 992 { 993 final Map.Entry<String,JSONValue> thatEntry = thatIterator.next(); 994 final String thatFieldName = thatEntry.getKey(); 995 if (! thisFieldName.equalsIgnoreCase(thatFieldName)) 996 { 997 continue; 998 } 999 1000 final JSONValue thatValue = thatEntry.getValue(); 1001 if (thisValue.equals(thatValue, ignoreFieldNameCase, ignoreValueCase, 1002 ignoreArrayOrder)) 1003 { 1004 found = true; 1005 thatIterator.remove(); 1006 break; 1007 } 1008 } 1009 1010 if (! found) 1011 { 1012 return false; 1013 } 1014 } 1015 1016 return true; 1017 } 1018 1019 1020 1021 /** 1022 * {@inheritDoc} 1023 */ 1024 @Override() 1025 public boolean equals(final JSONValue v, final boolean ignoreFieldNameCase, 1026 final boolean ignoreValueCase, 1027 final boolean ignoreArrayOrder) 1028 { 1029 return ((v instanceof JSONObject) && 1030 equals((JSONObject) v, ignoreFieldNameCase, ignoreValueCase, 1031 ignoreArrayOrder)); 1032 } 1033 1034 1035 1036 /** 1037 * {@inheritDoc} 1038 */ 1039 @Override() 1040 public String toString() 1041 { 1042 if (stringRepresentation == null) 1043 { 1044 final StringBuilder buffer = new StringBuilder(); 1045 toString(buffer); 1046 stringRepresentation = buffer.toString(); 1047 } 1048 1049 return stringRepresentation; 1050 } 1051 1052 1053 1054 /** 1055 * {@inheritDoc} 1056 */ 1057 @Override() 1058 public void toString(final StringBuilder buffer) 1059 { 1060 if (stringRepresentation != null) 1061 { 1062 buffer.append(stringRepresentation); 1063 return; 1064 } 1065 1066 buffer.append("{ "); 1067 1068 final Iterator<Map.Entry<String,JSONValue>> iterator = 1069 fields.entrySet().iterator(); 1070 while (iterator.hasNext()) 1071 { 1072 final Map.Entry<String,JSONValue> e = iterator.next(); 1073 JSONString.encodeString(e.getKey(), buffer); 1074 buffer.append(':'); 1075 e.getValue().toString(buffer); 1076 1077 if (iterator.hasNext()) 1078 { 1079 buffer.append(','); 1080 } 1081 buffer.append(' '); 1082 } 1083 1084 buffer.append('}'); 1085 } 1086 1087 1088 1089 /** 1090 * {@inheritDoc} 1091 */ 1092 @Override() 1093 public String toNormalizedString() 1094 { 1095 final StringBuilder buffer = new StringBuilder(); 1096 toNormalizedString(buffer); 1097 return buffer.toString(); 1098 } 1099 1100 1101 1102 /** 1103 * {@inheritDoc} 1104 */ 1105 @Override() 1106 public void toNormalizedString(final StringBuilder buffer) 1107 { 1108 // The normalized representation needs to have the fields in a predictable 1109 // order, which we will accomplish using the lexicographic ordering that a 1110 // TreeMap will provide. Field names will be case sensitive, but we still 1111 // need to construct a normalized way of escaping non-printable characters 1112 // in each field. 1113 final StringBuilder tempBuffer; 1114 if (decodeBuffer == null) 1115 { 1116 tempBuffer = new StringBuilder(20); 1117 } 1118 else 1119 { 1120 tempBuffer = decodeBuffer; 1121 } 1122 1123 final TreeMap<String,String> m = new TreeMap<String,String>(); 1124 for (final Map.Entry<String,JSONValue> e : fields.entrySet()) 1125 { 1126 tempBuffer.setLength(0); 1127 tempBuffer.append('"'); 1128 for (final char c : e.getKey().toCharArray()) 1129 { 1130 if (StaticUtils.isPrintable(c)) 1131 { 1132 tempBuffer.append(c); 1133 } 1134 else 1135 { 1136 tempBuffer.append("\\u"); 1137 tempBuffer.append(String.format("%04X", (int) c)); 1138 } 1139 } 1140 tempBuffer.append('"'); 1141 final String normalizedKey = tempBuffer.toString(); 1142 1143 tempBuffer.setLength(0); 1144 e.getValue().toNormalizedString(tempBuffer); 1145 m.put(normalizedKey, tempBuffer.toString()); 1146 } 1147 1148 buffer.append('{'); 1149 final Iterator<Map.Entry<String,String>> iterator = m.entrySet().iterator(); 1150 while (iterator.hasNext()) 1151 { 1152 final Map.Entry<String,String> e = iterator.next(); 1153 buffer.append(e.getKey()); 1154 buffer.append(':'); 1155 buffer.append(e.getValue()); 1156 1157 if (iterator.hasNext()) 1158 { 1159 buffer.append(','); 1160 } 1161 } 1162 1163 buffer.append('}'); 1164 } 1165}