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