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