001/* 002 * Copyright 2015-2018 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright (C) 2015-2018 Ping Identity Corporation 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<>(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(new LinkedHashMap<>(fields)); 176 } 177 178 hashCode = null; 179 stringRepresentation = null; 180 181 // We don't need to decode anything. 182 decodePos = -1; 183 decodeBuffer = null; 184 } 185 186 187 188 /** 189 * Creates a new JSON object parsed from the provided string. 190 * 191 * @param stringRepresentation The string to parse as a JSON object. It 192 * must represent exactly one JSON object. 193 * 194 * @throws JSONException If the provided string cannot be parsed as a valid 195 * JSON object. 196 */ 197 public JSONObject(final String stringRepresentation) 198 throws JSONException 199 { 200 this.stringRepresentation = stringRepresentation; 201 202 final char[] chars = stringRepresentation.toCharArray(); 203 decodePos = 0; 204 decodeBuffer = new StringBuilder(chars.length); 205 206 // The JSON object must start with an open curly brace. 207 final Object firstToken = readToken(chars); 208 if (! firstToken.equals('{')) 209 { 210 throw new JSONException(ERR_OBJECT_DOESNT_START_WITH_BRACE.get( 211 stringRepresentation)); 212 } 213 214 final LinkedHashMap<String,JSONValue> m = new LinkedHashMap<>(10); 215 readObject(chars, m); 216 fields = Collections.unmodifiableMap(m); 217 218 skipWhitespace(chars); 219 if (decodePos < chars.length) 220 { 221 throw new JSONException(ERR_OBJECT_DATA_BEYOND_END.get( 222 stringRepresentation, decodePos)); 223 } 224 } 225 226 227 228 /** 229 * Creates a new JSON object with the provided information. 230 * 231 * @param fields The set of fields for this JSON object. 232 * @param stringRepresentation The string representation for the JSON 233 * object. 234 */ 235 JSONObject(final LinkedHashMap<String,JSONValue> fields, 236 final String stringRepresentation) 237 { 238 this.fields = Collections.unmodifiableMap(fields); 239 this.stringRepresentation = stringRepresentation; 240 241 hashCode = null; 242 decodePos = -1; 243 decodeBuffer = null; 244 } 245 246 247 248 /** 249 * Reads a token from the provided character array, skipping over any 250 * insignificant whitespace that may be before the token. The token that is 251 * returned will be one of the following: 252 * <UL> 253 * <LI>A {@code Character} that is an opening curly brace.</LI> 254 * <LI>A {@code Character} that is a closing curly brace.</LI> 255 * <LI>A {@code Character} that is an opening square bracket.</LI> 256 * <LI>A {@code Character} that is a closing square bracket.</LI> 257 * <LI>A {@code Character} that is a colon.</LI> 258 * <LI>A {@code Character} that is a comma.</LI> 259 * <LI>A {@link JSONBoolean}.</LI> 260 * <LI>A {@link JSONNull}.</LI> 261 * <LI>A {@link JSONNumber}.</LI> 262 * <LI>A {@link JSONString}.</LI> 263 * </UL> 264 * 265 * @param chars The characters that comprise the string representation of 266 * the JSON object. 267 * 268 * @return The token that was read. 269 * 270 * @throws JSONException If a problem was encountered while reading the 271 * token. 272 */ 273 private Object readToken(final char[] chars) 274 throws JSONException 275 { 276 skipWhitespace(chars); 277 278 final char c = readCharacter(chars, false); 279 switch (c) 280 { 281 case '{': 282 case '}': 283 case '[': 284 case ']': 285 case ':': 286 case ',': 287 // This is a token character that we will return as-is. 288 decodePos++; 289 return c; 290 291 case '"': 292 // This is the start of a JSON string. 293 return readString(chars); 294 295 case 't': 296 case 'f': 297 // This is the start of a JSON true or false value. 298 return readBoolean(chars); 299 300 case 'n': 301 // This is the start of a JSON null value. 302 return readNull(chars); 303 304 case '-': 305 case '0': 306 case '1': 307 case '2': 308 case '3': 309 case '4': 310 case '5': 311 case '6': 312 case '7': 313 case '8': 314 case '9': 315 // This is the start of a JSON number value. 316 return readNumber(chars); 317 318 default: 319 // This is not a valid JSON token. 320 throw new JSONException(ERR_OBJECT_INVALID_FIRST_TOKEN_CHAR.get( 321 new String(chars), String.valueOf(c), decodePos)); 322 323 } 324 } 325 326 327 328 /** 329 * Skips over any valid JSON whitespace at the current position in the 330 * provided array. 331 * 332 * @param chars The characters that comprise the string representation of 333 * the JSON object. 334 * 335 * @throws JSONException If a problem is encountered while skipping 336 * whitespace. 337 */ 338 private void skipWhitespace(final char[] chars) 339 throws JSONException 340 { 341 while (decodePos < chars.length) 342 { 343 switch (chars[decodePos]) 344 { 345 // The space, tab, newline, and carriage return characters are 346 // considered valid JSON whitespace. 347 case ' ': 348 case '\t': 349 case '\n': 350 case '\r': 351 decodePos++; 352 break; 353 354 // Technically, JSON does not provide support for comments. But this 355 // implementation will accept three types of comments: 356 // - Comments that start with /* and end with */ (potentially spanning 357 // multiple lines). 358 // - Comments that start with // and continue until the end of the line. 359 // - Comments that start with # and continue until the end of the line. 360 // All comments will be ignored by the parser. 361 case '/': 362 final int commentStartPos = decodePos; 363 if ((decodePos+1) >= chars.length) 364 { 365 return; 366 } 367 else if (chars[decodePos+1] == '/') 368 { 369 decodePos += 2; 370 371 // Keep reading until we encounter a newline or carriage return, or 372 // until we hit the end of the string. 373 while (decodePos < chars.length) 374 { 375 if ((chars[decodePos] == '\n') || (chars[decodePos] == '\r')) 376 { 377 break; 378 } 379 decodePos++; 380 } 381 break; 382 } 383 else if (chars[decodePos+1] == '*') 384 { 385 decodePos += 2; 386 387 // Keep reading until we encounter "*/". We must encounter "*/" 388 // before hitting the end of the string. 389 boolean closeFound = false; 390 while (decodePos < chars.length) 391 { 392 if (chars[decodePos] == '*') 393 { 394 if (((decodePos+1) < chars.length) && 395 (chars[decodePos+1] == '/')) 396 { 397 closeFound = true; 398 decodePos += 2; 399 break; 400 } 401 } 402 decodePos++; 403 } 404 405 if (! closeFound) 406 { 407 throw new JSONException(ERR_OBJECT_UNCLOSED_COMMENT.get( 408 new String(chars), commentStartPos)); 409 } 410 break; 411 } 412 else 413 { 414 return; 415 } 416 417 case '#': 418 // Keep reading until we encounter a newline or carriage return, or 419 // until we hit the end of the string. 420 while (decodePos < chars.length) 421 { 422 if ((chars[decodePos] == '\n') || (chars[decodePos] == '\r')) 423 { 424 break; 425 } 426 decodePos++; 427 } 428 break; 429 430 default: 431 return; 432 } 433 } 434 } 435 436 437 438 /** 439 * Reads the character at the specified position and optionally advances the 440 * position. 441 * 442 * @param chars The characters that comprise the string 443 * representation of the JSON object. 444 * @param advancePosition Indicates whether to advance the value of the 445 * position indicator after reading the character. 446 * If this is {@code false}, then this method will be 447 * used to "peek" at the next character without 448 * consuming it. 449 * 450 * @return The character that was read. 451 * 452 * @throws JSONException If the end of the value was encountered when a 453 * character was expected. 454 */ 455 private char readCharacter(final char[] chars, final boolean advancePosition) 456 throws JSONException 457 { 458 if (decodePos >= chars.length) 459 { 460 throw new JSONException( 461 ERR_OBJECT_UNEXPECTED_END_OF_STRING.get(new String(chars))); 462 } 463 464 final char c = chars[decodePos]; 465 if (advancePosition) 466 { 467 decodePos++; 468 } 469 return c; 470 } 471 472 473 474 /** 475 * Reads a JSON string staring at the specified position in the provided 476 * character array. 477 * 478 * @param chars The characters that comprise the string representation of 479 * the JSON object. 480 * 481 * @return The JSON string that was read. 482 * 483 * @throws JSONException If a problem was encountered while reading the JSON 484 * string. 485 */ 486 private JSONString readString(final char[] chars) 487 throws JSONException 488 { 489 // Create a buffer to hold the string. Note that if we've gotten here then 490 // we already know that the character at the provided position is a quote, 491 // so we can read past it in the process. 492 final int startPos = decodePos++; 493 decodeBuffer.setLength(0); 494 while (true) 495 { 496 final char c = readCharacter(chars, true); 497 if (c == '\\') 498 { 499 final int escapedCharPos = decodePos; 500 final char escapedChar = readCharacter(chars, true); 501 switch (escapedChar) 502 { 503 case '"': 504 case '\\': 505 case '/': 506 decodeBuffer.append(escapedChar); 507 break; 508 case 'b': 509 decodeBuffer.append('\b'); 510 break; 511 case 'f': 512 decodeBuffer.append('\f'); 513 break; 514 case 'n': 515 decodeBuffer.append('\n'); 516 break; 517 case 'r': 518 decodeBuffer.append('\r'); 519 break; 520 case 't': 521 decodeBuffer.append('\t'); 522 break; 523 524 case 'u': 525 final char[] hexChars = 526 { 527 readCharacter(chars, true), 528 readCharacter(chars, true), 529 readCharacter(chars, true), 530 readCharacter(chars, true) 531 }; 532 try 533 { 534 decodeBuffer.append( 535 (char) Integer.parseInt(new String(hexChars), 16)); 536 } 537 catch (final Exception e) 538 { 539 Debug.debugException(e); 540 throw new JSONException( 541 ERR_OBJECT_INVALID_UNICODE_ESCAPE.get(new String(chars), 542 escapedCharPos), 543 e); 544 } 545 break; 546 547 default: 548 throw new JSONException(ERR_OBJECT_INVALID_ESCAPED_CHAR.get( 549 new String(chars), escapedChar, escapedCharPos)); 550 } 551 } 552 else if (c == '"') 553 { 554 return new JSONString(decodeBuffer.toString(), 555 new String(chars, startPos, (decodePos - startPos))); 556 } 557 else 558 { 559 if (c <= '\u001F') 560 { 561 throw new JSONException(ERR_OBJECT_UNESCAPED_CONTROL_CHAR.get( 562 new String(chars), String.format("%04X", (int) c), 563 (decodePos - 1))); 564 } 565 566 decodeBuffer.append(c); 567 } 568 } 569 } 570 571 572 573 /** 574 * Reads a JSON Boolean staring at the specified position in the provided 575 * character array. 576 * 577 * @param chars The characters that comprise the string representation of 578 * the JSON object. 579 * 580 * @return The JSON Boolean that was read. 581 * 582 * @throws JSONException If a problem was encountered while reading the JSON 583 * Boolean. 584 */ 585 private JSONBoolean readBoolean(final char[] chars) 586 throws JSONException 587 { 588 final int startPos = decodePos; 589 final char firstCharacter = readCharacter(chars, true); 590 if (firstCharacter == 't') 591 { 592 if ((readCharacter(chars, true) == 'r') && 593 (readCharacter(chars, true) == 'u') && 594 (readCharacter(chars, true) == 'e')) 595 { 596 return JSONBoolean.TRUE; 597 } 598 } 599 else if (firstCharacter == 'f') 600 { 601 if ((readCharacter(chars, true) == 'a') && 602 (readCharacter(chars, true) == 'l') && 603 (readCharacter(chars, true) == 's') && 604 (readCharacter(chars, true) == 'e')) 605 { 606 return JSONBoolean.FALSE; 607 } 608 } 609 610 throw new JSONException(ERR_OBJECT_UNABLE_TO_PARSE_BOOLEAN.get( 611 new String(chars), startPos)); 612 } 613 614 615 616 /** 617 * Reads a JSON null staring at the specified position in the provided 618 * character array. 619 * 620 * @param chars The characters that comprise the string representation of 621 * the JSON object. 622 * 623 * @return The JSON null that was read. 624 * 625 * @throws JSONException If a problem was encountered while reading the JSON 626 * null. 627 */ 628 private JSONNull readNull(final char[] chars) 629 throws JSONException 630 { 631 final int startPos = decodePos; 632 if ((readCharacter(chars, true) == 'n') && 633 (readCharacter(chars, true) == 'u') && 634 (readCharacter(chars, true) == 'l') && 635 (readCharacter(chars, true) == 'l')) 636 { 637 return JSONNull.NULL; 638 } 639 640 throw new JSONException(ERR_OBJECT_UNABLE_TO_PARSE_NULL.get( 641 new String(chars), startPos)); 642 } 643 644 645 646 /** 647 * Reads a JSON number staring at the specified position in the provided 648 * character array. 649 * 650 * @param chars The characters that comprise the string representation of 651 * the JSON object. 652 * 653 * @return The JSON number that was read. 654 * 655 * @throws JSONException If a problem was encountered while reading the JSON 656 * number. 657 */ 658 private JSONNumber readNumber(final char[] chars) 659 throws JSONException 660 { 661 // Read until we encounter whitespace, a comma, a closing square bracket, or 662 // a closing curly brace. Then try to parse what we read as a number. 663 final int startPos = decodePos; 664 decodeBuffer.setLength(0); 665 666 while (true) 667 { 668 final char c = readCharacter(chars, true); 669 switch (c) 670 { 671 case ' ': 672 case '\t': 673 case '\n': 674 case '\r': 675 case ',': 676 case ']': 677 case '}': 678 // We need to decrement the position indicator since the last one we 679 // read wasn't part of the number. 680 decodePos--; 681 return new JSONNumber(decodeBuffer.toString()); 682 683 default: 684 decodeBuffer.append(c); 685 } 686 } 687 } 688 689 690 691 /** 692 * Reads a JSON array starting at the specified position in the provided 693 * character array. Note that this method assumes that the opening square 694 * bracket has already been read. 695 * 696 * @param chars The characters that comprise the string representation of 697 * the JSON object. 698 * 699 * @return The JSON array that was read. 700 * 701 * @throws JSONException If a problem was encountered while reading the JSON 702 * array. 703 */ 704 private JSONArray readArray(final char[] chars) 705 throws JSONException 706 { 707 // The opening square bracket will have already been consumed, so read 708 // JSON values until we hit a closing square bracket. 709 final ArrayList<JSONValue> values = new ArrayList<>(10); 710 boolean firstToken = true; 711 while (true) 712 { 713 // If this is the first time through, it is acceptable to find a closing 714 // square bracket. Otherwise, we expect to find a JSON value, an opening 715 // square bracket to denote the start of an embedded array, or an opening 716 // curly brace to denote the start of an embedded JSON object. 717 int p = decodePos; 718 Object token = readToken(chars); 719 if (token instanceof JSONValue) 720 { 721 values.add((JSONValue) token); 722 } 723 else if (token.equals('[')) 724 { 725 values.add(readArray(chars)); 726 } 727 else if (token.equals('{')) 728 { 729 final LinkedHashMap<String,JSONValue> fieldMap = 730 new LinkedHashMap<>(10); 731 values.add(readObject(chars, fieldMap)); 732 } 733 else if (token.equals(']') && firstToken) 734 { 735 // It's an empty array. 736 return JSONArray.EMPTY_ARRAY; 737 } 738 else 739 { 740 throw new JSONException( 741 ERR_OBJECT_INVALID_TOKEN_WHEN_ARRAY_VALUE_EXPECTED.get( 742 new String(chars), String.valueOf(token), p)); 743 } 744 745 firstToken = false; 746 747 748 // If we've gotten here, then we found a JSON value. It must be followed 749 // by either a comma (to indicate that there's at least one more value) or 750 // a closing square bracket (to denote the end of the array). 751 p = decodePos; 752 token = readToken(chars); 753 if (token.equals(']')) 754 { 755 return new JSONArray(values); 756 } 757 else if (! token.equals(',')) 758 { 759 throw new JSONException( 760 ERR_OBJECT_INVALID_TOKEN_WHEN_ARRAY_COMMA_OR_BRACKET_EXPECTED.get( 761 new String(chars), String.valueOf(token), p)); 762 } 763 } 764 } 765 766 767 768 /** 769 * Reads a JSON object starting at the specified position in the provided 770 * character array. Note that this method assumes that the opening curly 771 * brace has already been read. 772 * 773 * @param chars The characters that comprise the string representation of 774 * the JSON object. 775 * @param fields The map into which to place the fields that are read. The 776 * returned object will include an unmodifiable view of this 777 * map, but the caller may use the map directly if desired. 778 * 779 * @return The JSON object that was read. 780 * 781 * @throws JSONException If a problem was encountered while reading the JSON 782 * object. 783 */ 784 private JSONObject readObject(final char[] chars, 785 final Map<String,JSONValue> fields) 786 throws JSONException 787 { 788 boolean firstField = true; 789 while (true) 790 { 791 // Read the next token. It must be a JSONString, unless we haven't read 792 // any fields yet in which case it can be a closing curly brace to 793 // indicate that it's an empty object. 794 int p = decodePos; 795 final String fieldName; 796 Object token = readToken(chars); 797 if (token instanceof JSONString) 798 { 799 fieldName = ((JSONString) token).stringValue(); 800 if (fields.containsKey(fieldName)) 801 { 802 throw new JSONException(ERR_OBJECT_DUPLICATE_FIELD.get( 803 new String(chars), fieldName)); 804 } 805 } 806 else if (firstField && token.equals('}')) 807 { 808 return new JSONObject(fields); 809 } 810 else 811 { 812 throw new JSONException(ERR_OBJECT_EXPECTED_STRING.get( 813 new String(chars), String.valueOf(token), p)); 814 } 815 firstField = false; 816 817 // Read the next token. It must be a colon. 818 p = decodePos; 819 token = readToken(chars); 820 if (! token.equals(':')) 821 { 822 throw new JSONException(ERR_OBJECT_EXPECTED_COLON.get(new String(chars), 823 String.valueOf(token), p)); 824 } 825 826 // Read the next token. It must be one of the following: 827 // - A JSONValue 828 // - An opening square bracket, designating the start of an array. 829 // - An opening curly brace, designating the start of an object. 830 p = decodePos; 831 token = readToken(chars); 832 if (token instanceof JSONValue) 833 { 834 fields.put(fieldName, (JSONValue) token); 835 } 836 else if (token.equals('[')) 837 { 838 final JSONArray a = readArray(chars); 839 fields.put(fieldName, a); 840 } 841 else if (token.equals('{')) 842 { 843 final LinkedHashMap<String,JSONValue> m = new LinkedHashMap<>(10); 844 final JSONObject o = readObject(chars, m); 845 fields.put(fieldName, o); 846 } 847 else 848 { 849 throw new JSONException(ERR_OBJECT_EXPECTED_VALUE.get(new String(chars), 850 String.valueOf(token), p, fieldName)); 851 } 852 853 // Read the next token. It must be either a comma (to indicate that 854 // there will be another field) or a closing curly brace (to indicate 855 // that the end of the object has been reached). 856 p = decodePos; 857 token = readToken(chars); 858 if (token.equals('}')) 859 { 860 return new JSONObject(fields); 861 } 862 else if (! token.equals(',')) 863 { 864 throw new JSONException(ERR_OBJECT_EXPECTED_COMMA_OR_CLOSE_BRACE.get( 865 new String(chars), String.valueOf(token), p)); 866 } 867 } 868 } 869 870 871 872 /** 873 * Retrieves a map of the fields contained in this JSON object. 874 * 875 * @return A map of the fields contained in this JSON object. 876 */ 877 public Map<String,JSONValue> getFields() 878 { 879 return fields; 880 } 881 882 883 884 /** 885 * Retrieves the value for the specified field. 886 * 887 * @param name The name of the field for which to retrieve the value. It 888 * will be treated in a case-sensitive manner. 889 * 890 * @return The value for the specified field, or {@code null} if the 891 * requested field is not present in the JSON object. 892 */ 893 public JSONValue getField(final String name) 894 { 895 return fields.get(name); 896 } 897 898 899 900 /** 901 * {@inheritDoc} 902 */ 903 @Override() 904 public int hashCode() 905 { 906 if (hashCode == null) 907 { 908 int hc = 0; 909 for (final Map.Entry<String,JSONValue> e : fields.entrySet()) 910 { 911 hc += e.getKey().hashCode() + e.getValue().hashCode(); 912 } 913 914 hashCode = hc; 915 } 916 917 return hashCode; 918 } 919 920 921 922 /** 923 * {@inheritDoc} 924 */ 925 @Override() 926 public boolean equals(final Object o) 927 { 928 if (o == this) 929 { 930 return true; 931 } 932 933 if (o instanceof JSONObject) 934 { 935 final JSONObject obj = (JSONObject) o; 936 return fields.equals(obj.fields); 937 } 938 939 return false; 940 } 941 942 943 944 /** 945 * Indicates whether this JSON object is considered equal to the provided 946 * object, subject to the specified constraints. 947 * 948 * @param o The object to compare against this JSON 949 * object. It must not be {@code null}. 950 * @param ignoreFieldNameCase Indicates whether to ignore differences in 951 * capitalization in field names. 952 * @param ignoreValueCase Indicates whether to ignore differences in 953 * capitalization in values that are JSON 954 * strings. 955 * @param ignoreArrayOrder Indicates whether to ignore differences in the 956 * order of elements within an array. 957 * 958 * @return {@code true} if this JSON object is considered equal to the 959 * provided object (subject to the specified constraints), or 960 * {@code false} if not. 961 */ 962 public boolean equals(final JSONObject o, final boolean ignoreFieldNameCase, 963 final boolean ignoreValueCase, 964 final boolean ignoreArrayOrder) 965 { 966 // See if we can do a straight-up Map.equals. If so, just do that. 967 if ((! ignoreFieldNameCase) && (! ignoreValueCase) && (! ignoreArrayOrder)) 968 { 969 return fields.equals(o.fields); 970 } 971 972 // Make sure they have the same number of fields. 973 if (fields.size() != o.fields.size()) 974 { 975 return false; 976 } 977 978 // Optimize for the case in which we field names are case sensitive. 979 if (! ignoreFieldNameCase) 980 { 981 for (final Map.Entry<String,JSONValue> e : fields.entrySet()) 982 { 983 final JSONValue thisValue = e.getValue(); 984 final JSONValue thatValue = o.fields.get(e.getKey()); 985 if (thatValue == null) 986 { 987 return false; 988 } 989 990 if (! thisValue.equals(thatValue, ignoreFieldNameCase, ignoreValueCase, 991 ignoreArrayOrder)) 992 { 993 return false; 994 } 995 } 996 997 return true; 998 } 999 1000 1001 // If we've gotten here, then we know that we need to treat field names in 1002 // a case-insensitive manner. Create a new map that we can remove fields 1003 // from as we find matches. This can help avoid false-positive matches in 1004 // which multiple fields in the first map match the same field in the second 1005 // map (e.g., because they have field names that differ only in case and 1006 // values that are logically equivalent). It also makes iterating through 1007 // the values faster as we make more progress. 1008 final HashMap<String,JSONValue> thatMap = new HashMap<>(o.fields); 1009 final Iterator<Map.Entry<String,JSONValue>> thisIterator = 1010 fields.entrySet().iterator(); 1011 while (thisIterator.hasNext()) 1012 { 1013 final Map.Entry<String,JSONValue> thisEntry = thisIterator.next(); 1014 final String thisFieldName = thisEntry.getKey(); 1015 final JSONValue thisValue = thisEntry.getValue(); 1016 1017 final Iterator<Map.Entry<String,JSONValue>> thatIterator = 1018 thatMap.entrySet().iterator(); 1019 1020 boolean found = false; 1021 while (thatIterator.hasNext()) 1022 { 1023 final Map.Entry<String,JSONValue> thatEntry = thatIterator.next(); 1024 final String thatFieldName = thatEntry.getKey(); 1025 if (! thisFieldName.equalsIgnoreCase(thatFieldName)) 1026 { 1027 continue; 1028 } 1029 1030 final JSONValue thatValue = thatEntry.getValue(); 1031 if (thisValue.equals(thatValue, ignoreFieldNameCase, ignoreValueCase, 1032 ignoreArrayOrder)) 1033 { 1034 found = true; 1035 thatIterator.remove(); 1036 break; 1037 } 1038 } 1039 1040 if (! found) 1041 { 1042 return false; 1043 } 1044 } 1045 1046 return true; 1047 } 1048 1049 1050 1051 /** 1052 * {@inheritDoc} 1053 */ 1054 @Override() 1055 public boolean equals(final JSONValue v, final boolean ignoreFieldNameCase, 1056 final boolean ignoreValueCase, 1057 final boolean ignoreArrayOrder) 1058 { 1059 return ((v instanceof JSONObject) && 1060 equals((JSONObject) v, ignoreFieldNameCase, ignoreValueCase, 1061 ignoreArrayOrder)); 1062 } 1063 1064 1065 1066 /** 1067 * Retrieves a string representation of this JSON object. If this object was 1068 * decoded from a string, then the original string representation will be 1069 * used. Otherwise, a single-line string representation will be constructed. 1070 * 1071 * @return A string representation of this JSON object. 1072 */ 1073 @Override() 1074 public String toString() 1075 { 1076 if (stringRepresentation == null) 1077 { 1078 final StringBuilder buffer = new StringBuilder(); 1079 toString(buffer); 1080 stringRepresentation = buffer.toString(); 1081 } 1082 1083 return stringRepresentation; 1084 } 1085 1086 1087 1088 /** 1089 * Appends a string representation of this JSON object to the provided buffer. 1090 * If this object was decoded from a string, then the original string 1091 * representation will be used. Otherwise, a single-line string 1092 * representation will be constructed. 1093 * 1094 * @param buffer The buffer to which the information should be appended. 1095 */ 1096 @Override() 1097 public void toString(final StringBuilder buffer) 1098 { 1099 if (stringRepresentation != null) 1100 { 1101 buffer.append(stringRepresentation); 1102 return; 1103 } 1104 1105 buffer.append("{ "); 1106 1107 final Iterator<Map.Entry<String,JSONValue>> iterator = 1108 fields.entrySet().iterator(); 1109 while (iterator.hasNext()) 1110 { 1111 final Map.Entry<String,JSONValue> e = iterator.next(); 1112 JSONString.encodeString(e.getKey(), buffer); 1113 buffer.append(':'); 1114 e.getValue().toString(buffer); 1115 1116 if (iterator.hasNext()) 1117 { 1118 buffer.append(','); 1119 } 1120 buffer.append(' '); 1121 } 1122 1123 buffer.append('}'); 1124 } 1125 1126 1127 1128 /** 1129 * Retrieves a user-friendly string representation of this JSON object that 1130 * may be formatted across multiple lines for better readability. The last 1131 * line will not include a trailing line break. 1132 * 1133 * @return A user-friendly string representation of this JSON object that may 1134 * be formatted across multiple lines for better readability. 1135 */ 1136 public String toMultiLineString() 1137 { 1138 final JSONBuffer jsonBuffer = new JSONBuffer(null, 0, true); 1139 appendToJSONBuffer(jsonBuffer); 1140 return jsonBuffer.toString(); 1141 } 1142 1143 1144 1145 /** 1146 * Retrieves a single-line string representation of this JSON object. 1147 * 1148 * @return A single-line string representation of this JSON object. 1149 */ 1150 @Override() 1151 public String toSingleLineString() 1152 { 1153 final StringBuilder buffer = new StringBuilder(); 1154 toSingleLineString(buffer); 1155 return buffer.toString(); 1156 } 1157 1158 1159 1160 /** 1161 * Appends a single-line string representation of this JSON object to the 1162 * provided buffer. 1163 * 1164 * @param buffer The buffer to which the information should be appended. 1165 */ 1166 @Override() 1167 public void toSingleLineString(final StringBuilder buffer) 1168 { 1169 buffer.append("{ "); 1170 1171 final Iterator<Map.Entry<String,JSONValue>> iterator = 1172 fields.entrySet().iterator(); 1173 while (iterator.hasNext()) 1174 { 1175 final Map.Entry<String,JSONValue> e = iterator.next(); 1176 JSONString.encodeString(e.getKey(), buffer); 1177 buffer.append(':'); 1178 e.getValue().toSingleLineString(buffer); 1179 1180 if (iterator.hasNext()) 1181 { 1182 buffer.append(','); 1183 } 1184 buffer.append(' '); 1185 } 1186 1187 buffer.append('}'); 1188 } 1189 1190 1191 1192 /** 1193 * Retrieves a normalized string representation of this JSON object. The 1194 * normalized representation of the JSON object will have the following 1195 * characteristics: 1196 * <UL> 1197 * <LI>It will not include any line breaks.</LI> 1198 * <LI>It will not include any spaces around the enclosing braces.</LI> 1199 * <LI>It will not include any spaces around the commas used to separate 1200 * fields.</LI> 1201 * <LI>Field names will be treated in a case-sensitive manner and will not 1202 * be altered.</LI> 1203 * <LI>Field values will be normalized.</LI> 1204 * <LI>Fields will be listed in lexicographic order by field name.</LI> 1205 * </UL> 1206 * 1207 * @return A normalized string representation of this JSON object. 1208 */ 1209 @Override() 1210 public String toNormalizedString() 1211 { 1212 final StringBuilder buffer = new StringBuilder(); 1213 toNormalizedString(buffer); 1214 return buffer.toString(); 1215 } 1216 1217 1218 1219 /** 1220 * Appends a normalized string representation of this JSON object to the 1221 * provided buffer. The normalized representation of the JSON object will 1222 * have the following characteristics: 1223 * <UL> 1224 * <LI>It will not include any line breaks.</LI> 1225 * <LI>It will not include any spaces around the enclosing braces.</LI> 1226 * <LI>It will not include any spaces around the commas used to separate 1227 * fields.</LI> 1228 * <LI>Field names will be treated in a case-sensitive manner and will not 1229 * be altered.</LI> 1230 * <LI>Field values will be normalized.</LI> 1231 * <LI>Fields will be listed in lexicographic order by field name.</LI> 1232 * </UL> 1233 * 1234 * @param buffer The buffer to which the information should be appended. 1235 */ 1236 @Override() 1237 public void toNormalizedString(final StringBuilder buffer) 1238 { 1239 // The normalized representation needs to have the fields in a predictable 1240 // order, which we will accomplish using the lexicographic ordering that a 1241 // TreeMap will provide. Field names will be case sensitive, but we still 1242 // need to construct a normalized way of escaping non-printable characters 1243 // in each field. 1244 final StringBuilder tempBuffer; 1245 if (decodeBuffer == null) 1246 { 1247 tempBuffer = new StringBuilder(20); 1248 } 1249 else 1250 { 1251 tempBuffer = decodeBuffer; 1252 } 1253 1254 final TreeMap<String,String> m = new TreeMap<>(); 1255 for (final Map.Entry<String,JSONValue> e : fields.entrySet()) 1256 { 1257 tempBuffer.setLength(0); 1258 tempBuffer.append('"'); 1259 for (final char c : e.getKey().toCharArray()) 1260 { 1261 if (StaticUtils.isPrintable(c)) 1262 { 1263 tempBuffer.append(c); 1264 } 1265 else 1266 { 1267 tempBuffer.append("\\u"); 1268 tempBuffer.append(String.format("%04X", (int) c)); 1269 } 1270 } 1271 tempBuffer.append('"'); 1272 final String normalizedKey = tempBuffer.toString(); 1273 1274 tempBuffer.setLength(0); 1275 e.getValue().toNormalizedString(tempBuffer); 1276 m.put(normalizedKey, tempBuffer.toString()); 1277 } 1278 1279 buffer.append('{'); 1280 final Iterator<Map.Entry<String,String>> iterator = m.entrySet().iterator(); 1281 while (iterator.hasNext()) 1282 { 1283 final Map.Entry<String,String> e = iterator.next(); 1284 buffer.append(e.getKey()); 1285 buffer.append(':'); 1286 buffer.append(e.getValue()); 1287 1288 if (iterator.hasNext()) 1289 { 1290 buffer.append(','); 1291 } 1292 } 1293 1294 buffer.append('}'); 1295 } 1296 1297 1298 1299 /** 1300 * {@inheritDoc} 1301 */ 1302 @Override() 1303 public void appendToJSONBuffer(final JSONBuffer buffer) 1304 { 1305 buffer.beginObject(); 1306 1307 for (final Map.Entry<String,JSONValue> field : fields.entrySet()) 1308 { 1309 final String name = field.getKey(); 1310 final JSONValue value = field.getValue(); 1311 value.appendToJSONBuffer(name, buffer); 1312 } 1313 1314 buffer.endObject(); 1315 } 1316 1317 1318 1319 /** 1320 * {@inheritDoc} 1321 */ 1322 @Override() 1323 public void appendToJSONBuffer(final String fieldName, 1324 final JSONBuffer buffer) 1325 { 1326 buffer.beginObject(fieldName); 1327 1328 for (final Map.Entry<String,JSONValue> field : fields.entrySet()) 1329 { 1330 final String name = field.getKey(); 1331 final JSONValue value = field.getValue(); 1332 value.appendToJSONBuffer(name, buffer); 1333 } 1334 1335 buffer.endObject(); 1336 } 1337}