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.math.BigDecimal; 026import java.util.ArrayList; 027import java.util.Collections; 028import java.util.HashMap; 029import java.util.Iterator; 030import java.util.LinkedHashMap; 031import java.util.List; 032import java.util.Map; 033import java.util.TreeMap; 034 035import com.unboundid.util.Debug; 036import com.unboundid.util.NotMutable; 037import com.unboundid.util.StaticUtils; 038import com.unboundid.util.ThreadSafety; 039import com.unboundid.util.ThreadSafetyLevel; 040 041import static com.unboundid.util.json.JSONMessages.*; 042 043 044 045/** 046 * This class provides an implementation of a JSON value that represents an 047 * object with zero or more name-value pairs. In each pair, the name is a JSON 048 * string and the value is any type of JSON value ({@code null}, {@code true}, 049 * {@code false}, number, string, array, or object). Although the ECMA-404 050 * specification does not explicitly forbid a JSON object from having multiple 051 * fields with the same name, RFC 7159 section 4 states that field names should 052 * be unique, and this implementation does not support objects in which multiple 053 * fields have the same name. Note that this uniqueness constraint only applies 054 * to the fields directly contained within an object, and does not prevent an 055 * object from having a field value that is an object (or that is an array 056 * containing one or more objects) that use a field name that is also in use 057 * in the outer object. Similarly, if an array contains multiple JSON objects, 058 * then there is no restriction preventing the same field names from being 059 * used in separate objects within that array. 060 * <BR><BR> 061 * The string representation of a JSON object is an open curly brace (U+007B) 062 * followed by a comma-delimited list of the name-value pairs that comprise the 063 * fields in that object and a closing curly brace (U+007D). Each name-value 064 * pair is represented as a JSON string followed by a colon and the appropriate 065 * string representation of the value. There must not be a comma between the 066 * last field and the closing curly brace. There may optionally be any amount 067 * of whitespace (where whitespace characters include the ASCII space, 068 * horizontal tab, line feed, and carriage return characters) after the open 069 * curly brace, on either or both sides of the colon separating a field name 070 * from its value, on either or both sides of commas separating fields, and 071 * before the closing curly brace. The order in which fields appear in the 072 * string representation is not considered significant. 073 * <BR><BR> 074 * The string representation returned by the {@link #toString()} method (or 075 * appended to the buffer provided to the {@link #toString(StringBuilder)} 076 * method) will include one space before each field name and one space before 077 * the closing curly brace. There will not be any space on either side of the 078 * colon separating the field name from its value, and there will not be any 079 * space between a field value and the comma that follows it. The string 080 * representation of each field name will use the same logic as the 081 * {@link JSONString#toString()} method, and the string representation of each 082 * field value will be obtained using that value's {@code toString} method. 083 * <BR><BR> 084 * The normalized string representation will not include any optional spaces, 085 * and the normalized string representation of each field value will be obtained 086 * using that value's {@code toNormalizedString} method. Field names will be 087 * treated in a case-sensitive manner, but all characters outside the LDAP 088 * printable character set will be escaped using the {@code \}{@code u}-style 089 * Unicode encoding. The normalized string representation will have fields 090 * listed in lexicographic order. 091 */ 092@NotMutable() 093@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE) 094public final class JSONObject 095 extends JSONValue 096{ 097 /** 098 * A pre-allocated empty JSON object. 099 */ 100 public static final JSONObject EMPTY_OBJECT = new JSONObject( 101 Collections.<String,JSONValue>emptyMap()); 102 103 104 105 /** 106 * The serial version UID for this serializable class. 107 */ 108 private static final long serialVersionUID = -4209509956709292141L; 109 110 111 112 // A counter to use in decode processing. 113 private int decodePos; 114 115 // The hash code for this JSON object. 116 private Integer hashCode; 117 118 // The set of fields for this JSON object. 119 private final Map<String,JSONValue> fields; 120 121 // The string representation for this JSON object. 122 private String stringRepresentation; 123 124 // A buffer to use in decode processing. 125 private final StringBuilder decodeBuffer; 126 127 128 129 /** 130 * Creates a new JSON object with the provided fields. 131 * 132 * @param fields The fields to include in this JSON object. It may be 133 * {@code null} or empty if this object should not have any 134 * fields. 135 */ 136 public JSONObject(final JSONField... fields) 137 { 138 if ((fields == null) || (fields.length == 0)) 139 { 140 this.fields = Collections.emptyMap(); 141 } 142 else 143 { 144 final LinkedHashMap<String,JSONValue> m = 145 new LinkedHashMap<>(fields.length); 146 for (final JSONField f : fields) 147 { 148 m.put(f.getName(), f.getValue()); 149 } 150 this.fields = Collections.unmodifiableMap(m); 151 } 152 153 hashCode = null; 154 stringRepresentation = null; 155 156 // We don't need to decode anything. 157 decodePos = -1; 158 decodeBuffer = null; 159 } 160 161 162 163 /** 164 * Creates a new JSON object with the provided fields. 165 * 166 * @param fields The set of fields for this JSON object. It may be 167 * {@code null} or empty if there should not be any fields. 168 */ 169 public JSONObject(final Map<String,JSONValue> fields) 170 { 171 if (fields == null) 172 { 173 this.fields = Collections.emptyMap(); 174 } 175 else 176 { 177 this.fields = Collections.unmodifiableMap(new LinkedHashMap<>(fields)); 178 } 179 180 hashCode = null; 181 stringRepresentation = null; 182 183 // We don't need to decode anything. 184 decodePos = -1; 185 decodeBuffer = null; 186 } 187 188 189 190 /** 191 * Creates a new JSON object parsed from the provided string. 192 * 193 * @param stringRepresentation The string to parse as a JSON object. It 194 * must represent exactly one JSON object. 195 * 196 * @throws JSONException If the provided string cannot be parsed as a valid 197 * JSON object. 198 */ 199 public JSONObject(final String stringRepresentation) 200 throws JSONException 201 { 202 this.stringRepresentation = stringRepresentation; 203 204 final char[] chars = stringRepresentation.toCharArray(); 205 decodePos = 0; 206 decodeBuffer = new StringBuilder(chars.length); 207 208 // The JSON object must start with an open curly brace. 209 final Object firstToken = readToken(chars); 210 if (! firstToken.equals('{')) 211 { 212 throw new JSONException(ERR_OBJECT_DOESNT_START_WITH_BRACE.get( 213 stringRepresentation)); 214 } 215 216 final LinkedHashMap<String,JSONValue> m = new LinkedHashMap<>(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 * Creates a new JSON object with the provided information. 232 * 233 * @param fields The set of fields for this JSON object. 234 * @param stringRepresentation The string representation for the JSON 235 * object. 236 */ 237 JSONObject(final LinkedHashMap<String,JSONValue> fields, 238 final String stringRepresentation) 239 { 240 this.fields = Collections.unmodifiableMap(fields); 241 this.stringRepresentation = stringRepresentation; 242 243 hashCode = null; 244 decodePos = -1; 245 decodeBuffer = null; 246 } 247 248 249 250 /** 251 * Reads a token from the provided character array, skipping over any 252 * insignificant whitespace that may be before the token. The token that is 253 * returned will be one of the following: 254 * <UL> 255 * <LI>A {@code Character} that is an opening curly brace.</LI> 256 * <LI>A {@code Character} that is a closing curly brace.</LI> 257 * <LI>A {@code Character} that is an opening square bracket.</LI> 258 * <LI>A {@code Character} that is a closing square bracket.</LI> 259 * <LI>A {@code Character} that is a colon.</LI> 260 * <LI>A {@code Character} that is a comma.</LI> 261 * <LI>A {@link JSONBoolean}.</LI> 262 * <LI>A {@link JSONNull}.</LI> 263 * <LI>A {@link JSONNumber}.</LI> 264 * <LI>A {@link JSONString}.</LI> 265 * </UL> 266 * 267 * @param chars The characters that comprise the string representation of 268 * the JSON object. 269 * 270 * @return The token that was read. 271 * 272 * @throws JSONException If a problem was encountered while reading the 273 * token. 274 */ 275 private Object readToken(final char[] chars) 276 throws JSONException 277 { 278 skipWhitespace(chars); 279 280 final char c = readCharacter(chars, false); 281 switch (c) 282 { 283 case '{': 284 case '}': 285 case '[': 286 case ']': 287 case ':': 288 case ',': 289 // This is a token character that we will return as-is. 290 decodePos++; 291 return c; 292 293 case '"': 294 // This is the start of a JSON string. 295 return readString(chars); 296 297 case 't': 298 case 'f': 299 // This is the start of a JSON true or false value. 300 return readBoolean(chars); 301 302 case 'n': 303 // This is the start of a JSON null value. 304 return readNull(chars); 305 306 case '-': 307 case '0': 308 case '1': 309 case '2': 310 case '3': 311 case '4': 312 case '5': 313 case '6': 314 case '7': 315 case '8': 316 case '9': 317 // This is the start of a JSON number value. 318 return readNumber(chars); 319 320 default: 321 // This is not a valid JSON token. 322 throw new JSONException(ERR_OBJECT_INVALID_FIRST_TOKEN_CHAR.get( 323 new String(chars), String.valueOf(c), decodePos)); 324 325 } 326 } 327 328 329 330 /** 331 * Skips over any valid JSON whitespace at the current position in the 332 * provided array. 333 * 334 * @param chars The characters that comprise the string representation of 335 * the JSON object. 336 * 337 * @throws JSONException If a problem is encountered while skipping 338 * whitespace. 339 */ 340 private void skipWhitespace(final char[] chars) 341 throws JSONException 342 { 343 while (decodePos < chars.length) 344 { 345 switch (chars[decodePos]) 346 { 347 // The space, tab, newline, and carriage return characters are 348 // considered valid JSON whitespace. 349 case ' ': 350 case '\t': 351 case '\n': 352 case '\r': 353 decodePos++; 354 break; 355 356 // Technically, JSON does not provide support for comments. But this 357 // implementation will accept three types of comments: 358 // - Comments that start with /* and end with */ (potentially spanning 359 // multiple lines). 360 // - Comments that start with // and continue until the end of the line. 361 // - Comments that start with # and continue until the end of the line. 362 // All comments will be ignored by the parser. 363 case '/': 364 final int commentStartPos = decodePos; 365 if ((decodePos+1) >= chars.length) 366 { 367 return; 368 } 369 else if (chars[decodePos+1] == '/') 370 { 371 decodePos += 2; 372 373 // Keep reading until we encounter a newline or carriage return, or 374 // until we hit the end of the string. 375 while (decodePos < chars.length) 376 { 377 if ((chars[decodePos] == '\n') || (chars[decodePos] == '\r')) 378 { 379 break; 380 } 381 decodePos++; 382 } 383 break; 384 } 385 else if (chars[decodePos+1] == '*') 386 { 387 decodePos += 2; 388 389 // Keep reading until we encounter "*/". We must encounter "*/" 390 // before hitting the end of the string. 391 boolean closeFound = false; 392 while (decodePos < chars.length) 393 { 394 if (chars[decodePos] == '*') 395 { 396 if (((decodePos+1) < chars.length) && 397 (chars[decodePos+1] == '/')) 398 { 399 closeFound = true; 400 decodePos += 2; 401 break; 402 } 403 } 404 decodePos++; 405 } 406 407 if (! closeFound) 408 { 409 throw new JSONException(ERR_OBJECT_UNCLOSED_COMMENT.get( 410 new String(chars), commentStartPos)); 411 } 412 break; 413 } 414 else 415 { 416 return; 417 } 418 419 case '#': 420 // Keep reading until we encounter a newline or carriage return, or 421 // until we hit the end of the string. 422 while (decodePos < chars.length) 423 { 424 if ((chars[decodePos] == '\n') || (chars[decodePos] == '\r')) 425 { 426 break; 427 } 428 decodePos++; 429 } 430 break; 431 432 default: 433 return; 434 } 435 } 436 } 437 438 439 440 /** 441 * Reads the character at the specified position and optionally advances the 442 * position. 443 * 444 * @param chars The characters that comprise the string 445 * representation of the JSON object. 446 * @param advancePosition Indicates whether to advance the value of the 447 * position indicator after reading the character. 448 * If this is {@code false}, then this method will be 449 * used to "peek" at the next character without 450 * consuming it. 451 * 452 * @return The character that was read. 453 * 454 * @throws JSONException If the end of the value was encountered when a 455 * character was expected. 456 */ 457 private char readCharacter(final char[] chars, final boolean advancePosition) 458 throws JSONException 459 { 460 if (decodePos >= chars.length) 461 { 462 throw new JSONException( 463 ERR_OBJECT_UNEXPECTED_END_OF_STRING.get(new String(chars))); 464 } 465 466 final char c = chars[decodePos]; 467 if (advancePosition) 468 { 469 decodePos++; 470 } 471 return c; 472 } 473 474 475 476 /** 477 * Reads a JSON string staring at the specified position in the provided 478 * character array. 479 * 480 * @param chars The characters that comprise the string representation of 481 * the JSON object. 482 * 483 * @return The JSON string that was read. 484 * 485 * @throws JSONException If a problem was encountered while reading the JSON 486 * string. 487 */ 488 private JSONString readString(final char[] chars) 489 throws JSONException 490 { 491 // Create a buffer to hold the string. Note that if we've gotten here then 492 // we already know that the character at the provided position is a quote, 493 // so we can read past it in the process. 494 final int startPos = decodePos++; 495 decodeBuffer.setLength(0); 496 while (true) 497 { 498 final char c = readCharacter(chars, true); 499 if (c == '\\') 500 { 501 final int escapedCharPos = decodePos; 502 final char escapedChar = readCharacter(chars, true); 503 switch (escapedChar) 504 { 505 case '"': 506 case '\\': 507 case '/': 508 decodeBuffer.append(escapedChar); 509 break; 510 case 'b': 511 decodeBuffer.append('\b'); 512 break; 513 case 'f': 514 decodeBuffer.append('\f'); 515 break; 516 case 'n': 517 decodeBuffer.append('\n'); 518 break; 519 case 'r': 520 decodeBuffer.append('\r'); 521 break; 522 case 't': 523 decodeBuffer.append('\t'); 524 break; 525 526 case 'u': 527 final char[] hexChars = 528 { 529 readCharacter(chars, true), 530 readCharacter(chars, true), 531 readCharacter(chars, true), 532 readCharacter(chars, true) 533 }; 534 try 535 { 536 decodeBuffer.append( 537 (char) Integer.parseInt(new String(hexChars), 16)); 538 } 539 catch (final Exception e) 540 { 541 Debug.debugException(e); 542 throw new JSONException( 543 ERR_OBJECT_INVALID_UNICODE_ESCAPE.get(new String(chars), 544 escapedCharPos), 545 e); 546 } 547 break; 548 549 default: 550 throw new JSONException(ERR_OBJECT_INVALID_ESCAPED_CHAR.get( 551 new String(chars), escapedChar, escapedCharPos)); 552 } 553 } 554 else if (c == '"') 555 { 556 return new JSONString(decodeBuffer.toString(), 557 new String(chars, startPos, (decodePos - startPos))); 558 } 559 else 560 { 561 if (c <= '\u001F') 562 { 563 throw new JSONException(ERR_OBJECT_UNESCAPED_CONTROL_CHAR.get( 564 new String(chars), String.format("%04X", (int) c), 565 (decodePos - 1))); 566 } 567 568 decodeBuffer.append(c); 569 } 570 } 571 } 572 573 574 575 /** 576 * Reads a JSON Boolean staring at the specified position in the provided 577 * character array. 578 * 579 * @param chars The characters that comprise the string representation of 580 * the JSON object. 581 * 582 * @return The JSON Boolean that was read. 583 * 584 * @throws JSONException If a problem was encountered while reading the JSON 585 * Boolean. 586 */ 587 private JSONBoolean readBoolean(final char[] chars) 588 throws JSONException 589 { 590 final int startPos = decodePos; 591 final char firstCharacter = readCharacter(chars, true); 592 if (firstCharacter == 't') 593 { 594 if ((readCharacter(chars, true) == 'r') && 595 (readCharacter(chars, true) == 'u') && 596 (readCharacter(chars, true) == 'e')) 597 { 598 return JSONBoolean.TRUE; 599 } 600 } 601 else if (firstCharacter == 'f') 602 { 603 if ((readCharacter(chars, true) == 'a') && 604 (readCharacter(chars, true) == 'l') && 605 (readCharacter(chars, true) == 's') && 606 (readCharacter(chars, true) == 'e')) 607 { 608 return JSONBoolean.FALSE; 609 } 610 } 611 612 throw new JSONException(ERR_OBJECT_UNABLE_TO_PARSE_BOOLEAN.get( 613 new String(chars), startPos)); 614 } 615 616 617 618 /** 619 * Reads a JSON null staring at the specified position in the provided 620 * character array. 621 * 622 * @param chars The characters that comprise the string representation of 623 * the JSON object. 624 * 625 * @return The JSON null that was read. 626 * 627 * @throws JSONException If a problem was encountered while reading the JSON 628 * null. 629 */ 630 private JSONNull readNull(final char[] chars) 631 throws JSONException 632 { 633 final int startPos = decodePos; 634 if ((readCharacter(chars, true) == 'n') && 635 (readCharacter(chars, true) == 'u') && 636 (readCharacter(chars, true) == 'l') && 637 (readCharacter(chars, true) == 'l')) 638 { 639 return JSONNull.NULL; 640 } 641 642 throw new JSONException(ERR_OBJECT_UNABLE_TO_PARSE_NULL.get( 643 new String(chars), startPos)); 644 } 645 646 647 648 /** 649 * Reads a JSON number staring at the specified position in the provided 650 * character array. 651 * 652 * @param chars The characters that comprise the string representation of 653 * the JSON object. 654 * 655 * @return The JSON number that was read. 656 * 657 * @throws JSONException If a problem was encountered while reading the JSON 658 * number. 659 */ 660 private JSONNumber readNumber(final char[] chars) 661 throws JSONException 662 { 663 // Read until we encounter whitespace, a comma, a closing square bracket, or 664 // a closing curly brace. Then try to parse what we read as a number. 665 final int startPos = decodePos; 666 decodeBuffer.setLength(0); 667 668 while (true) 669 { 670 final char c = readCharacter(chars, true); 671 switch (c) 672 { 673 case ' ': 674 case '\t': 675 case '\n': 676 case '\r': 677 case ',': 678 case ']': 679 case '}': 680 // We need to decrement the position indicator since the last one we 681 // read wasn't part of the number. 682 decodePos--; 683 return new JSONNumber(decodeBuffer.toString()); 684 685 default: 686 decodeBuffer.append(c); 687 } 688 } 689 } 690 691 692 693 /** 694 * Reads a JSON array starting at the specified position in the provided 695 * character array. Note that this method assumes that the opening square 696 * bracket has already been read. 697 * 698 * @param chars The characters that comprise the string representation of 699 * the JSON object. 700 * 701 * @return The JSON array that was read. 702 * 703 * @throws JSONException If a problem was encountered while reading the JSON 704 * array. 705 */ 706 private JSONArray readArray(final char[] chars) 707 throws JSONException 708 { 709 // The opening square bracket will have already been consumed, so read 710 // JSON values until we hit a closing square bracket. 711 final ArrayList<JSONValue> values = new ArrayList<>(10); 712 boolean firstToken = true; 713 while (true) 714 { 715 // If this is the first time through, it is acceptable to find a closing 716 // square bracket. Otherwise, we expect to find a JSON value, an opening 717 // square bracket to denote the start of an embedded array, or an opening 718 // curly brace to denote the start of an embedded JSON object. 719 int p = decodePos; 720 Object token = readToken(chars); 721 if (token instanceof JSONValue) 722 { 723 values.add((JSONValue) token); 724 } 725 else if (token.equals('[')) 726 { 727 values.add(readArray(chars)); 728 } 729 else if (token.equals('{')) 730 { 731 final LinkedHashMap<String,JSONValue> fieldMap = 732 new LinkedHashMap<>(10); 733 values.add(readObject(chars, fieldMap)); 734 } 735 else if (token.equals(']') && firstToken) 736 { 737 // It's an empty array. 738 return JSONArray.EMPTY_ARRAY; 739 } 740 else 741 { 742 throw new JSONException( 743 ERR_OBJECT_INVALID_TOKEN_WHEN_ARRAY_VALUE_EXPECTED.get( 744 new String(chars), String.valueOf(token), p)); 745 } 746 747 firstToken = false; 748 749 750 // If we've gotten here, then we found a JSON value. It must be followed 751 // by either a comma (to indicate that there's at least one more value) or 752 // a closing square bracket (to denote the end of the array). 753 p = decodePos; 754 token = readToken(chars); 755 if (token.equals(']')) 756 { 757 return new JSONArray(values); 758 } 759 else if (! token.equals(',')) 760 { 761 throw new JSONException( 762 ERR_OBJECT_INVALID_TOKEN_WHEN_ARRAY_COMMA_OR_BRACKET_EXPECTED.get( 763 new String(chars), String.valueOf(token), p)); 764 } 765 } 766 } 767 768 769 770 /** 771 * Reads a JSON object starting at the specified position in the provided 772 * character array. Note that this method assumes that the opening curly 773 * brace has already been read. 774 * 775 * @param chars The characters that comprise the string representation of 776 * the JSON object. 777 * @param fields The map into which to place the fields that are read. The 778 * returned object will include an unmodifiable view of this 779 * map, but the caller may use the map directly if desired. 780 * 781 * @return The JSON object that was read. 782 * 783 * @throws JSONException If a problem was encountered while reading the JSON 784 * object. 785 */ 786 private JSONObject readObject(final char[] chars, 787 final Map<String,JSONValue> fields) 788 throws JSONException 789 { 790 boolean firstField = true; 791 while (true) 792 { 793 // Read the next token. It must be a JSONString, unless we haven't read 794 // any fields yet in which case it can be a closing curly brace to 795 // indicate that it's an empty object. 796 int p = decodePos; 797 final String fieldName; 798 Object token = readToken(chars); 799 if (token instanceof JSONString) 800 { 801 fieldName = ((JSONString) token).stringValue(); 802 if (fields.containsKey(fieldName)) 803 { 804 throw new JSONException(ERR_OBJECT_DUPLICATE_FIELD.get( 805 new String(chars), fieldName)); 806 } 807 } 808 else if (firstField && token.equals('}')) 809 { 810 return new JSONObject(fields); 811 } 812 else 813 { 814 throw new JSONException(ERR_OBJECT_EXPECTED_STRING.get( 815 new String(chars), String.valueOf(token), p)); 816 } 817 firstField = false; 818 819 // Read the next token. It must be a colon. 820 p = decodePos; 821 token = readToken(chars); 822 if (! token.equals(':')) 823 { 824 throw new JSONException(ERR_OBJECT_EXPECTED_COLON.get(new String(chars), 825 String.valueOf(token), p)); 826 } 827 828 // Read the next token. It must be one of the following: 829 // - A JSONValue 830 // - An opening square bracket, designating the start of an array. 831 // - An opening curly brace, designating the start of an object. 832 p = decodePos; 833 token = readToken(chars); 834 if (token instanceof JSONValue) 835 { 836 fields.put(fieldName, (JSONValue) token); 837 } 838 else if (token.equals('[')) 839 { 840 final JSONArray a = readArray(chars); 841 fields.put(fieldName, a); 842 } 843 else if (token.equals('{')) 844 { 845 final LinkedHashMap<String,JSONValue> m = new LinkedHashMap<>(10); 846 final JSONObject o = readObject(chars, m); 847 fields.put(fieldName, o); 848 } 849 else 850 { 851 throw new JSONException(ERR_OBJECT_EXPECTED_VALUE.get(new String(chars), 852 String.valueOf(token), p, fieldName)); 853 } 854 855 // Read the next token. It must be either a comma (to indicate that 856 // there will be another field) or a closing curly brace (to indicate 857 // that the end of the object has been reached). 858 p = decodePos; 859 token = readToken(chars); 860 if (token.equals('}')) 861 { 862 return new JSONObject(fields); 863 } 864 else if (! token.equals(',')) 865 { 866 throw new JSONException(ERR_OBJECT_EXPECTED_COMMA_OR_CLOSE_BRACE.get( 867 new String(chars), String.valueOf(token), p)); 868 } 869 } 870 } 871 872 873 874 /** 875 * Retrieves a map of the fields contained in this JSON object. 876 * 877 * @return A map of the fields contained in this JSON object. 878 */ 879 public Map<String,JSONValue> getFields() 880 { 881 return fields; 882 } 883 884 885 886 /** 887 * Retrieves the value for the specified field. 888 * 889 * @param name The name of the field for which to retrieve the value. It 890 * will be treated in a case-sensitive manner. 891 * 892 * @return The value for the specified field, or {@code null} if the 893 * requested field is not present in the JSON object. 894 */ 895 public JSONValue getField(final String name) 896 { 897 return fields.get(name); 898 } 899 900 901 902 /** 903 * Retrieves the value of the specified field as a string. 904 * 905 * @param name The name of the field for which to retrieve the string value. 906 * It will be treated in a case-sensitive manner. 907 * 908 * @return The value of the specified field as a string, or {@code null} if 909 * this JSON object does not have a field with the specified name, or 910 * if the value of that field is not a string. 911 */ 912 public String getFieldAsString(final String name) 913 { 914 final JSONValue value = fields.get(name); 915 if ((value == null) || (! (value instanceof JSONString))) 916 { 917 return null; 918 } 919 920 return ((JSONString) value).stringValue(); 921 } 922 923 924 925 /** 926 * Retrieves the value of the specified field as a Boolean. 927 * 928 * @param name The name of the field for which to retrieve the Boolean 929 * value. It will be treated in a case-sensitive manner. 930 * 931 * @return The value of the specified field as a Boolean, or {@code null} if 932 * this JSON object does not have a field with the specified name, or 933 * if the value of that field is not a Boolean. 934 */ 935 public Boolean getFieldAsBoolean(final String name) 936 { 937 final JSONValue value = fields.get(name); 938 if ((value == null) || (! (value instanceof JSONBoolean))) 939 { 940 return null; 941 } 942 943 return ((JSONBoolean) value).booleanValue(); 944 } 945 946 947 948 /** 949 * Retrieves the value of the specified field as an integer. 950 * 951 * @param name The name of the field for which to retrieve the integer 952 * value. It will be treated in a case-sensitive manner. 953 * 954 * @return The value of the specified field as an integer, or {@code null} if 955 * this JSON object does not have a field with the specified name, or 956 * if the value of that field is not a number that can be exactly 957 * represented as an integer. 958 */ 959 public Integer getFieldAsInteger(final String name) 960 { 961 final JSONValue value = fields.get(name); 962 if ((value == null) || (! (value instanceof JSONNumber))) 963 { 964 return null; 965 } 966 967 try 968 { 969 final JSONNumber number = (JSONNumber) value; 970 return number.getValue().intValueExact(); 971 } 972 catch (final Exception e) 973 { 974 Debug.debugException(e); 975 return null; 976 } 977 } 978 979 980 981 /** 982 * Retrieves the value of the specified field as a long. 983 * 984 * @param name The name of the field for which to retrieve the long value. 985 * It will be treated in a case-sensitive manner. 986 * 987 * @return The value of the specified field as a long, or {@code null} if 988 * this JSON object does not have a field with the specified name, or 989 * if the value of that field is not a number that can be exactly 990 * represented as a long. 991 */ 992 public Long getFieldAsLong(final String name) 993 { 994 final JSONValue value = fields.get(name); 995 if ((value == null) || (! (value instanceof JSONNumber))) 996 { 997 return null; 998 } 999 1000 try 1001 { 1002 final JSONNumber number = (JSONNumber) value; 1003 return number.getValue().longValueExact(); 1004 } 1005 catch (final Exception e) 1006 { 1007 Debug.debugException(e); 1008 return null; 1009 } 1010 } 1011 1012 1013 1014 /** 1015 * Retrieves the value of the specified field as a BigDecimal. 1016 * 1017 * @param name The name of the field for which to retrieve the BigDecimal 1018 * value. It will be treated in a case-sensitive manner. 1019 * 1020 * @return The value of the specified field as a BigDecimal, or {@code null} 1021 * if this JSON object does not have a field with the specified name, 1022 * or if the value of that field is not a number. 1023 */ 1024 public BigDecimal getFieldAsBigDecimal(final String name) 1025 { 1026 final JSONValue value = fields.get(name); 1027 if ((value == null) || (! (value instanceof JSONNumber))) 1028 { 1029 return null; 1030 } 1031 1032 return ((JSONNumber) value).getValue(); 1033 } 1034 1035 1036 1037 /** 1038 * Retrieves the value of the specified field as a JSON object. 1039 * 1040 * @param name The name of the field for which to retrieve the value. It 1041 * will be treated in a case-sensitive manner. 1042 * 1043 * @return The value of the specified field as a JSON object, or {@code null} 1044 * if this JSON object does not have a field with the specified name, 1045 * or if the value of that field is not an object. 1046 */ 1047 public JSONObject getFieldAsObject(final String name) 1048 { 1049 final JSONValue value = fields.get(name); 1050 if ((value == null) || (! (value instanceof JSONObject))) 1051 { 1052 return null; 1053 } 1054 1055 return (JSONObject) value; 1056 } 1057 1058 1059 1060 /** 1061 * Retrieves a list of the elements in the specified array field. 1062 * 1063 * @param name The name of the field for which to retrieve the array values. 1064 * It will be treated in a case-sensitive manner. 1065 * 1066 * @return A list of the elements in the specified array field, or 1067 * {@code null} if this JSON object does not have a field with the 1068 * specified name, or if the value of that field is not an array. 1069 */ 1070 public List<JSONValue> getFieldAsArray(final String name) 1071 { 1072 final JSONValue value = fields.get(name); 1073 if ((value == null) || (! (value instanceof JSONArray))) 1074 { 1075 return null; 1076 } 1077 1078 return ((JSONArray) value).getValues(); 1079 } 1080 1081 1082 1083 /** 1084 * Indicates whether this JSON object has a null field with the specified 1085 * name. 1086 * 1087 * @param name The name of the field for which to make the determination. 1088 * It will be treated in a case-sensitive manner. 1089 * 1090 * @return {@code true} if this JSON object has a null field with the 1091 * specified name, or {@code false} if this JSON object does not have 1092 * a field with the specified name, or if the value of that field is 1093 * not a null. 1094 */ 1095 public boolean hasNullField(final String name) 1096 { 1097 final JSONValue value = fields.get(name); 1098 return ((value != null) && (value instanceof JSONNull)); 1099 } 1100 1101 1102 1103 /** 1104 * {@inheritDoc} 1105 */ 1106 @Override() 1107 public int hashCode() 1108 { 1109 if (hashCode == null) 1110 { 1111 int hc = 0; 1112 for (final Map.Entry<String,JSONValue> e : fields.entrySet()) 1113 { 1114 hc += e.getKey().hashCode() + e.getValue().hashCode(); 1115 } 1116 1117 hashCode = hc; 1118 } 1119 1120 return hashCode; 1121 } 1122 1123 1124 1125 /** 1126 * {@inheritDoc} 1127 */ 1128 @Override() 1129 public boolean equals(final Object o) 1130 { 1131 if (o == this) 1132 { 1133 return true; 1134 } 1135 1136 if (o instanceof JSONObject) 1137 { 1138 final JSONObject obj = (JSONObject) o; 1139 return fields.equals(obj.fields); 1140 } 1141 1142 return false; 1143 } 1144 1145 1146 1147 /** 1148 * Indicates whether this JSON object is considered equal to the provided 1149 * object, subject to the specified constraints. 1150 * 1151 * @param o The object to compare against this JSON 1152 * object. It must not be {@code null}. 1153 * @param ignoreFieldNameCase Indicates whether to ignore differences in 1154 * capitalization in field names. 1155 * @param ignoreValueCase Indicates whether to ignore differences in 1156 * capitalization in values that are JSON 1157 * strings. 1158 * @param ignoreArrayOrder Indicates whether to ignore differences in the 1159 * order of elements within an array. 1160 * 1161 * @return {@code true} if this JSON object is considered equal to the 1162 * provided object (subject to the specified constraints), or 1163 * {@code false} if not. 1164 */ 1165 public boolean equals(final JSONObject o, final boolean ignoreFieldNameCase, 1166 final boolean ignoreValueCase, 1167 final boolean ignoreArrayOrder) 1168 { 1169 // See if we can do a straight-up Map.equals. If so, just do that. 1170 if ((! ignoreFieldNameCase) && (! ignoreValueCase) && (! ignoreArrayOrder)) 1171 { 1172 return fields.equals(o.fields); 1173 } 1174 1175 // Make sure they have the same number of fields. 1176 if (fields.size() != o.fields.size()) 1177 { 1178 return false; 1179 } 1180 1181 // Optimize for the case in which we field names are case sensitive. 1182 if (! ignoreFieldNameCase) 1183 { 1184 for (final Map.Entry<String,JSONValue> e : fields.entrySet()) 1185 { 1186 final JSONValue thisValue = e.getValue(); 1187 final JSONValue thatValue = o.fields.get(e.getKey()); 1188 if (thatValue == null) 1189 { 1190 return false; 1191 } 1192 1193 if (! thisValue.equals(thatValue, ignoreFieldNameCase, ignoreValueCase, 1194 ignoreArrayOrder)) 1195 { 1196 return false; 1197 } 1198 } 1199 1200 return true; 1201 } 1202 1203 1204 // If we've gotten here, then we know that we need to treat field names in 1205 // a case-insensitive manner. Create a new map that we can remove fields 1206 // from as we find matches. This can help avoid false-positive matches in 1207 // which multiple fields in the first map match the same field in the second 1208 // map (e.g., because they have field names that differ only in case and 1209 // values that are logically equivalent). It also makes iterating through 1210 // the values faster as we make more progress. 1211 final HashMap<String,JSONValue> thatMap = new HashMap<>(o.fields); 1212 final Iterator<Map.Entry<String,JSONValue>> thisIterator = 1213 fields.entrySet().iterator(); 1214 while (thisIterator.hasNext()) 1215 { 1216 final Map.Entry<String,JSONValue> thisEntry = thisIterator.next(); 1217 final String thisFieldName = thisEntry.getKey(); 1218 final JSONValue thisValue = thisEntry.getValue(); 1219 1220 final Iterator<Map.Entry<String,JSONValue>> thatIterator = 1221 thatMap.entrySet().iterator(); 1222 1223 boolean found = false; 1224 while (thatIterator.hasNext()) 1225 { 1226 final Map.Entry<String,JSONValue> thatEntry = thatIterator.next(); 1227 final String thatFieldName = thatEntry.getKey(); 1228 if (! thisFieldName.equalsIgnoreCase(thatFieldName)) 1229 { 1230 continue; 1231 } 1232 1233 final JSONValue thatValue = thatEntry.getValue(); 1234 if (thisValue.equals(thatValue, ignoreFieldNameCase, ignoreValueCase, 1235 ignoreArrayOrder)) 1236 { 1237 found = true; 1238 thatIterator.remove(); 1239 break; 1240 } 1241 } 1242 1243 if (! found) 1244 { 1245 return false; 1246 } 1247 } 1248 1249 return true; 1250 } 1251 1252 1253 1254 /** 1255 * {@inheritDoc} 1256 */ 1257 @Override() 1258 public boolean equals(final JSONValue v, final boolean ignoreFieldNameCase, 1259 final boolean ignoreValueCase, 1260 final boolean ignoreArrayOrder) 1261 { 1262 return ((v instanceof JSONObject) && 1263 equals((JSONObject) v, ignoreFieldNameCase, ignoreValueCase, 1264 ignoreArrayOrder)); 1265 } 1266 1267 1268 1269 /** 1270 * Retrieves a string representation of this JSON object. If this object was 1271 * decoded from a string, then the original string representation will be 1272 * used. Otherwise, a single-line string representation will be constructed. 1273 * 1274 * @return A string representation of this JSON object. 1275 */ 1276 @Override() 1277 public String toString() 1278 { 1279 if (stringRepresentation == null) 1280 { 1281 final StringBuilder buffer = new StringBuilder(); 1282 toString(buffer); 1283 stringRepresentation = buffer.toString(); 1284 } 1285 1286 return stringRepresentation; 1287 } 1288 1289 1290 1291 /** 1292 * Appends a string representation of this JSON object to the provided buffer. 1293 * If this object was decoded from a string, then the original string 1294 * representation will be used. Otherwise, a single-line string 1295 * representation will be constructed. 1296 * 1297 * @param buffer The buffer to which the information should be appended. 1298 */ 1299 @Override() 1300 public void toString(final StringBuilder buffer) 1301 { 1302 if (stringRepresentation != null) 1303 { 1304 buffer.append(stringRepresentation); 1305 return; 1306 } 1307 1308 buffer.append("{ "); 1309 1310 final Iterator<Map.Entry<String,JSONValue>> iterator = 1311 fields.entrySet().iterator(); 1312 while (iterator.hasNext()) 1313 { 1314 final Map.Entry<String,JSONValue> e = iterator.next(); 1315 JSONString.encodeString(e.getKey(), buffer); 1316 buffer.append(':'); 1317 e.getValue().toString(buffer); 1318 1319 if (iterator.hasNext()) 1320 { 1321 buffer.append(','); 1322 } 1323 buffer.append(' '); 1324 } 1325 1326 buffer.append('}'); 1327 } 1328 1329 1330 1331 /** 1332 * Retrieves a user-friendly string representation of this JSON object that 1333 * may be formatted across multiple lines for better readability. The last 1334 * line will not include a trailing line break. 1335 * 1336 * @return A user-friendly string representation of this JSON object that may 1337 * be formatted across multiple lines for better readability. 1338 */ 1339 public String toMultiLineString() 1340 { 1341 final JSONBuffer jsonBuffer = new JSONBuffer(null, 0, true); 1342 appendToJSONBuffer(jsonBuffer); 1343 return jsonBuffer.toString(); 1344 } 1345 1346 1347 1348 /** 1349 * Retrieves a single-line string representation of this JSON object. 1350 * 1351 * @return A single-line string representation of this JSON object. 1352 */ 1353 @Override() 1354 public String toSingleLineString() 1355 { 1356 final StringBuilder buffer = new StringBuilder(); 1357 toSingleLineString(buffer); 1358 return buffer.toString(); 1359 } 1360 1361 1362 1363 /** 1364 * Appends a single-line string representation of this JSON object to the 1365 * provided buffer. 1366 * 1367 * @param buffer The buffer to which the information should be appended. 1368 */ 1369 @Override() 1370 public void toSingleLineString(final StringBuilder buffer) 1371 { 1372 buffer.append("{ "); 1373 1374 final Iterator<Map.Entry<String,JSONValue>> iterator = 1375 fields.entrySet().iterator(); 1376 while (iterator.hasNext()) 1377 { 1378 final Map.Entry<String,JSONValue> e = iterator.next(); 1379 JSONString.encodeString(e.getKey(), buffer); 1380 buffer.append(':'); 1381 e.getValue().toSingleLineString(buffer); 1382 1383 if (iterator.hasNext()) 1384 { 1385 buffer.append(','); 1386 } 1387 buffer.append(' '); 1388 } 1389 1390 buffer.append('}'); 1391 } 1392 1393 1394 1395 /** 1396 * Retrieves a normalized string representation of this JSON object. The 1397 * normalized representation of the JSON object will have the following 1398 * characteristics: 1399 * <UL> 1400 * <LI>It will not include any line breaks.</LI> 1401 * <LI>It will not include any spaces around the enclosing braces.</LI> 1402 * <LI>It will not include any spaces around the commas used to separate 1403 * fields.</LI> 1404 * <LI>Field names will be treated in a case-sensitive manner and will not 1405 * be altered.</LI> 1406 * <LI>Field values will be normalized.</LI> 1407 * <LI>Fields will be listed in lexicographic order by field name.</LI> 1408 * </UL> 1409 * 1410 * @return A normalized string representation of this JSON object. 1411 */ 1412 @Override() 1413 public String toNormalizedString() 1414 { 1415 final StringBuilder buffer = new StringBuilder(); 1416 toNormalizedString(buffer); 1417 return buffer.toString(); 1418 } 1419 1420 1421 1422 /** 1423 * Appends a normalized string representation of this JSON object to the 1424 * provided buffer. The normalized representation of the JSON object will 1425 * have the following characteristics: 1426 * <UL> 1427 * <LI>It will not include any line breaks.</LI> 1428 * <LI>It will not include any spaces around the enclosing braces.</LI> 1429 * <LI>It will not include any spaces around the commas used to separate 1430 * fields.</LI> 1431 * <LI>Field names will be treated in a case-sensitive manner and will not 1432 * be altered.</LI> 1433 * <LI>Field values will be normalized.</LI> 1434 * <LI>Fields will be listed in lexicographic order by field name.</LI> 1435 * </UL> 1436 * 1437 * @param buffer The buffer to which the information should be appended. 1438 */ 1439 @Override() 1440 public void toNormalizedString(final StringBuilder buffer) 1441 { 1442 // The normalized representation needs to have the fields in a predictable 1443 // order, which we will accomplish using the lexicographic ordering that a 1444 // TreeMap will provide. Field names will be case sensitive, but we still 1445 // need to construct a normalized way of escaping non-printable characters 1446 // in each field. 1447 final StringBuilder tempBuffer; 1448 if (decodeBuffer == null) 1449 { 1450 tempBuffer = new StringBuilder(20); 1451 } 1452 else 1453 { 1454 tempBuffer = decodeBuffer; 1455 } 1456 1457 final TreeMap<String,String> m = new TreeMap<>(); 1458 for (final Map.Entry<String,JSONValue> e : fields.entrySet()) 1459 { 1460 tempBuffer.setLength(0); 1461 tempBuffer.append('"'); 1462 for (final char c : e.getKey().toCharArray()) 1463 { 1464 if (StaticUtils.isPrintable(c)) 1465 { 1466 tempBuffer.append(c); 1467 } 1468 else 1469 { 1470 tempBuffer.append("\\u"); 1471 tempBuffer.append(String.format("%04X", (int) c)); 1472 } 1473 } 1474 tempBuffer.append('"'); 1475 final String normalizedKey = tempBuffer.toString(); 1476 1477 tempBuffer.setLength(0); 1478 e.getValue().toNormalizedString(tempBuffer); 1479 m.put(normalizedKey, tempBuffer.toString()); 1480 } 1481 1482 buffer.append('{'); 1483 final Iterator<Map.Entry<String,String>> iterator = m.entrySet().iterator(); 1484 while (iterator.hasNext()) 1485 { 1486 final Map.Entry<String,String> e = iterator.next(); 1487 buffer.append(e.getKey()); 1488 buffer.append(':'); 1489 buffer.append(e.getValue()); 1490 1491 if (iterator.hasNext()) 1492 { 1493 buffer.append(','); 1494 } 1495 } 1496 1497 buffer.append('}'); 1498 } 1499 1500 1501 1502 /** 1503 * {@inheritDoc} 1504 */ 1505 @Override() 1506 public void appendToJSONBuffer(final JSONBuffer buffer) 1507 { 1508 buffer.beginObject(); 1509 1510 for (final Map.Entry<String,JSONValue> field : fields.entrySet()) 1511 { 1512 final String name = field.getKey(); 1513 final JSONValue value = field.getValue(); 1514 value.appendToJSONBuffer(name, buffer); 1515 } 1516 1517 buffer.endObject(); 1518 } 1519 1520 1521 1522 /** 1523 * {@inheritDoc} 1524 */ 1525 @Override() 1526 public void appendToJSONBuffer(final String fieldName, 1527 final JSONBuffer buffer) 1528 { 1529 buffer.beginObject(fieldName); 1530 1531 for (final Map.Entry<String,JSONValue> field : fields.entrySet()) 1532 { 1533 final String name = field.getKey(); 1534 final JSONValue value = field.getValue(); 1535 value.appendToJSONBuffer(name, buffer); 1536 } 1537 1538 buffer.endObject(); 1539 } 1540}