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