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