001/* 002 * Copyright 2015-2022 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright 2015-2022 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-2022 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 of the specified field as a string. 933 * 934 * @param name The name of the field for which to retrieve the string value. 935 * It will be treated in a case-sensitive manner. 936 * 937 * @return The value of the specified field as a string, or {@code null} if 938 * this JSON object does not have a field with the specified name, or 939 * if the value of that field is not a string. 940 */ 941 @Nullable() 942 public String getFieldAsString(@NotNull final String name) 943 { 944 final JSONValue value = fields.get(name); 945 if ((value == null) || (! (value instanceof JSONString))) 946 { 947 return null; 948 } 949 950 return ((JSONString) value).stringValue(); 951 } 952 953 954 955 /** 956 * Retrieves the value of the specified field as a Boolean. 957 * 958 * @param name The name of the field for which to retrieve the Boolean 959 * value. It will be treated in a case-sensitive manner. 960 * 961 * @return The value of the specified field as a Boolean, or {@code null} if 962 * this JSON object does not have a field with the specified name, or 963 * if the value of that field is not a Boolean. 964 */ 965 @Nullable() 966 public Boolean getFieldAsBoolean(@NotNull final String name) 967 { 968 final JSONValue value = fields.get(name); 969 if ((value == null) || (! (value instanceof JSONBoolean))) 970 { 971 return null; 972 } 973 974 return ((JSONBoolean) value).booleanValue(); 975 } 976 977 978 979 /** 980 * Retrieves the value of the specified field as an integer. 981 * 982 * @param name The name of the field for which to retrieve the integer 983 * value. It will be treated in a case-sensitive manner. 984 * 985 * @return The value of the specified field as an integer, or {@code null} if 986 * this JSON object does not have a field with the specified name, or 987 * if the value of that field is not a number that can be exactly 988 * represented as an integer. 989 */ 990 @Nullable() 991 public Integer getFieldAsInteger(@NotNull final String name) 992 { 993 final JSONValue value = fields.get(name); 994 if ((value == null) || (! (value instanceof JSONNumber))) 995 { 996 return null; 997 } 998 999 try 1000 { 1001 final JSONNumber number = (JSONNumber) value; 1002 return number.getValue().intValueExact(); 1003 } 1004 catch (final Exception e) 1005 { 1006 Debug.debugException(e); 1007 return null; 1008 } 1009 } 1010 1011 1012 1013 /** 1014 * Retrieves the value of the specified field as a long. 1015 * 1016 * @param name The name of the field for which to retrieve the long value. 1017 * It will be treated in a case-sensitive manner. 1018 * 1019 * @return The value of the specified field as a long, or {@code null} if 1020 * this JSON object does not have a field with the specified name, or 1021 * if the value of that field is not a number that can be exactly 1022 * represented as a long. 1023 */ 1024 @Nullable() 1025 public Long getFieldAsLong(@NotNull final String name) 1026 { 1027 final JSONValue value = fields.get(name); 1028 if ((value == null) || (! (value instanceof JSONNumber))) 1029 { 1030 return null; 1031 } 1032 1033 try 1034 { 1035 final JSONNumber number = (JSONNumber) value; 1036 return number.getValue().longValueExact(); 1037 } 1038 catch (final Exception e) 1039 { 1040 Debug.debugException(e); 1041 return null; 1042 } 1043 } 1044 1045 1046 1047 /** 1048 * Retrieves the value of the specified field as a BigDecimal. 1049 * 1050 * @param name The name of the field for which to retrieve the BigDecimal 1051 * value. It will be treated in a case-sensitive manner. 1052 * 1053 * @return The value of the specified field as a BigDecimal, or {@code null} 1054 * if this JSON object does not have a field with the specified name, 1055 * or if the value of that field is not a number. 1056 */ 1057 @Nullable() 1058 public BigDecimal getFieldAsBigDecimal(@NotNull final String name) 1059 { 1060 final JSONValue value = fields.get(name); 1061 if ((value == null) || (! (value instanceof JSONNumber))) 1062 { 1063 return null; 1064 } 1065 1066 return ((JSONNumber) value).getValue(); 1067 } 1068 1069 1070 1071 /** 1072 * Retrieves the value of the specified field as a JSON object. 1073 * 1074 * @param name The name of the field for which to retrieve the value. It 1075 * will be treated in a case-sensitive manner. 1076 * 1077 * @return The value of the specified field as a JSON object, or {@code null} 1078 * if this JSON object does not have a field with the specified name, 1079 * or if the value of that field is not an object. 1080 */ 1081 @Nullable() 1082 public JSONObject getFieldAsObject(@NotNull final String name) 1083 { 1084 final JSONValue value = fields.get(name); 1085 if ((value == null) || (! (value instanceof JSONObject))) 1086 { 1087 return null; 1088 } 1089 1090 return (JSONObject) value; 1091 } 1092 1093 1094 1095 /** 1096 * Retrieves a list of the elements in the specified array field. 1097 * 1098 * @param name The name of the field for which to retrieve the array values. 1099 * It will be treated in a case-sensitive manner. 1100 * 1101 * @return A list of the elements in the specified array field, or 1102 * {@code null} if this JSON object does not have a field with the 1103 * specified name, or if the value of that field is not an array. 1104 */ 1105 @Nullable() 1106 public List<JSONValue> getFieldAsArray(@NotNull final String name) 1107 { 1108 final JSONValue value = fields.get(name); 1109 if ((value == null) || (! (value instanceof JSONArray))) 1110 { 1111 return null; 1112 } 1113 1114 return ((JSONArray) value).getValues(); 1115 } 1116 1117 1118 1119 /** 1120 * Indicates whether this JSON object has a null field with the specified 1121 * name. 1122 * 1123 * @param name The name of the field for which to make the determination. 1124 * It will be treated in a case-sensitive manner. 1125 * 1126 * @return {@code true} if this JSON object has a null field with the 1127 * specified name, or {@code false} if this JSON object does not have 1128 * a field with the specified name, or if the value of that field is 1129 * not a null. 1130 */ 1131 public boolean hasNullField(@NotNull final String name) 1132 { 1133 final JSONValue value = fields.get(name); 1134 return ((value != null) && (value instanceof JSONNull)); 1135 } 1136 1137 1138 1139 /** 1140 * Indicates whether this JSON object has a field with the specified name. 1141 * 1142 * @param fieldName The name of the field for which to make the 1143 * determination. It will be treated in a case-sensitive 1144 * manner. 1145 * 1146 * @return {@code true} if this JSON object has a field with the specified 1147 * name, or {@code false} if not. 1148 */ 1149 public boolean hasField(@NotNull final String fieldName) 1150 { 1151 return fields.containsKey(fieldName); 1152 } 1153 1154 1155 1156 /** 1157 * {@inheritDoc} 1158 */ 1159 @Override() 1160 public int hashCode() 1161 { 1162 if (hashCode == null) 1163 { 1164 int hc = 0; 1165 for (final Map.Entry<String,JSONValue> e : fields.entrySet()) 1166 { 1167 hc += e.getKey().hashCode() + e.getValue().hashCode(); 1168 } 1169 1170 hashCode = hc; 1171 } 1172 1173 return hashCode; 1174 } 1175 1176 1177 1178 /** 1179 * {@inheritDoc} 1180 */ 1181 @Override() 1182 public boolean equals(@Nullable final Object o) 1183 { 1184 if (o == this) 1185 { 1186 return true; 1187 } 1188 1189 if (o instanceof JSONObject) 1190 { 1191 final JSONObject obj = (JSONObject) o; 1192 return fields.equals(obj.fields); 1193 } 1194 1195 return false; 1196 } 1197 1198 1199 1200 /** 1201 * Indicates whether this JSON object is considered equal to the provided 1202 * object, subject to the specified constraints. 1203 * 1204 * @param o The object to compare against this JSON 1205 * object. It must not be {@code null}. 1206 * @param ignoreFieldNameCase Indicates whether to ignore differences in 1207 * capitalization in field names. 1208 * @param ignoreValueCase Indicates whether to ignore differences in 1209 * capitalization in values that are JSON 1210 * strings. 1211 * @param ignoreArrayOrder Indicates whether to ignore differences in the 1212 * order of elements within an array. 1213 * 1214 * @return {@code true} if this JSON object is considered equal to the 1215 * provided object (subject to the specified constraints), or 1216 * {@code false} if not. 1217 */ 1218 public boolean equals(@NotNull final JSONObject o, 1219 final boolean ignoreFieldNameCase, 1220 final boolean ignoreValueCase, 1221 final boolean ignoreArrayOrder) 1222 { 1223 // See if we can do a straight-up Map.equals. If so, just do that. 1224 if ((! ignoreFieldNameCase) && (! ignoreValueCase) && (! ignoreArrayOrder)) 1225 { 1226 return fields.equals(o.fields); 1227 } 1228 1229 // Make sure they have the same number of fields. 1230 if (fields.size() != o.fields.size()) 1231 { 1232 return false; 1233 } 1234 1235 // Optimize for the case in which we field names are case sensitive. 1236 if (! ignoreFieldNameCase) 1237 { 1238 for (final Map.Entry<String,JSONValue> e : fields.entrySet()) 1239 { 1240 final JSONValue thisValue = e.getValue(); 1241 final JSONValue thatValue = o.fields.get(e.getKey()); 1242 if (thatValue == null) 1243 { 1244 return false; 1245 } 1246 1247 if (! thisValue.equals(thatValue, ignoreFieldNameCase, ignoreValueCase, 1248 ignoreArrayOrder)) 1249 { 1250 return false; 1251 } 1252 } 1253 1254 return true; 1255 } 1256 1257 1258 // If we've gotten here, then we know that we need to treat field names in 1259 // a case-insensitive manner. Create a new map that we can remove fields 1260 // from as we find matches. This can help avoid false-positive matches in 1261 // which multiple fields in the first map match the same field in the second 1262 // map (e.g., because they have field names that differ only in case and 1263 // values that are logically equivalent). It also makes iterating through 1264 // the values faster as we make more progress. 1265 final HashMap<String,JSONValue> thatMap = new HashMap<>(o.fields); 1266 final Iterator<Map.Entry<String,JSONValue>> thisIterator = 1267 fields.entrySet().iterator(); 1268 while (thisIterator.hasNext()) 1269 { 1270 final Map.Entry<String,JSONValue> thisEntry = thisIterator.next(); 1271 final String thisFieldName = thisEntry.getKey(); 1272 final JSONValue thisValue = thisEntry.getValue(); 1273 1274 final Iterator<Map.Entry<String,JSONValue>> thatIterator = 1275 thatMap.entrySet().iterator(); 1276 1277 boolean found = false; 1278 while (thatIterator.hasNext()) 1279 { 1280 final Map.Entry<String,JSONValue> thatEntry = thatIterator.next(); 1281 final String thatFieldName = thatEntry.getKey(); 1282 if (! thisFieldName.equalsIgnoreCase(thatFieldName)) 1283 { 1284 continue; 1285 } 1286 1287 final JSONValue thatValue = thatEntry.getValue(); 1288 if (thisValue.equals(thatValue, ignoreFieldNameCase, ignoreValueCase, 1289 ignoreArrayOrder)) 1290 { 1291 found = true; 1292 thatIterator.remove(); 1293 break; 1294 } 1295 } 1296 1297 if (! found) 1298 { 1299 return false; 1300 } 1301 } 1302 1303 return true; 1304 } 1305 1306 1307 1308 /** 1309 * {@inheritDoc} 1310 */ 1311 @Override() 1312 public boolean equals(@NotNull final JSONValue v, 1313 final boolean ignoreFieldNameCase, 1314 final boolean ignoreValueCase, 1315 final boolean ignoreArrayOrder) 1316 { 1317 return ((v instanceof JSONObject) && 1318 equals((JSONObject) v, ignoreFieldNameCase, ignoreValueCase, 1319 ignoreArrayOrder)); 1320 } 1321 1322 1323 1324 /** 1325 * Retrieves a string representation of this JSON object. If this object was 1326 * decoded from a string, then the original string representation will be 1327 * used. Otherwise, a single-line string representation will be constructed. 1328 * 1329 * @return A string representation of this JSON object. 1330 */ 1331 @Override() 1332 @NotNull() 1333 public String toString() 1334 { 1335 if (stringRepresentation == null) 1336 { 1337 final StringBuilder buffer = new StringBuilder(); 1338 toString(buffer); 1339 stringRepresentation = buffer.toString(); 1340 } 1341 1342 return stringRepresentation; 1343 } 1344 1345 1346 1347 /** 1348 * Appends a string representation of this JSON object to the provided buffer. 1349 * If this object was decoded from a string, then the original string 1350 * representation will be used. Otherwise, a single-line string 1351 * representation will be constructed. 1352 * 1353 * @param buffer The buffer to which the information should be appended. 1354 */ 1355 @Override() 1356 public void toString(@NotNull final StringBuilder buffer) 1357 { 1358 if (stringRepresentation != null) 1359 { 1360 buffer.append(stringRepresentation); 1361 return; 1362 } 1363 1364 buffer.append("{ "); 1365 1366 final Iterator<Map.Entry<String,JSONValue>> iterator = 1367 fields.entrySet().iterator(); 1368 while (iterator.hasNext()) 1369 { 1370 final Map.Entry<String,JSONValue> e = iterator.next(); 1371 JSONString.encodeString(e.getKey(), buffer); 1372 buffer.append(':'); 1373 e.getValue().toString(buffer); 1374 1375 if (iterator.hasNext()) 1376 { 1377 buffer.append(','); 1378 } 1379 buffer.append(' '); 1380 } 1381 1382 buffer.append('}'); 1383 } 1384 1385 1386 1387 /** 1388 * Retrieves a user-friendly string representation of this JSON object that 1389 * may be formatted across multiple lines for better readability. The last 1390 * line will not include a trailing line break. 1391 * 1392 * @return A user-friendly string representation of this JSON object that may 1393 * be formatted across multiple lines for better readability. 1394 */ 1395 @NotNull() 1396 public String toMultiLineString() 1397 { 1398 final JSONBuffer jsonBuffer = new JSONBuffer(null, 0, true); 1399 appendToJSONBuffer(jsonBuffer); 1400 return jsonBuffer.toString(); 1401 } 1402 1403 1404 1405 /** 1406 * Retrieves a single-line string representation of this JSON object. 1407 * 1408 * @return A single-line string representation of this JSON object. 1409 */ 1410 @Override() 1411 @NotNull 1412 public String toSingleLineString() 1413 { 1414 final StringBuilder buffer = new StringBuilder(); 1415 toSingleLineString(buffer); 1416 return buffer.toString(); 1417 } 1418 1419 1420 1421 /** 1422 * Appends a single-line string representation of this JSON object to the 1423 * provided buffer. 1424 * 1425 * @param buffer The buffer to which the information should be appended. 1426 */ 1427 @Override() 1428 public void toSingleLineString(@NotNull final StringBuilder buffer) 1429 { 1430 buffer.append("{ "); 1431 1432 final Iterator<Map.Entry<String,JSONValue>> iterator = 1433 fields.entrySet().iterator(); 1434 while (iterator.hasNext()) 1435 { 1436 final Map.Entry<String,JSONValue> e = iterator.next(); 1437 JSONString.encodeString(e.getKey(), buffer); 1438 buffer.append(':'); 1439 e.getValue().toSingleLineString(buffer); 1440 1441 if (iterator.hasNext()) 1442 { 1443 buffer.append(','); 1444 } 1445 buffer.append(' '); 1446 } 1447 1448 buffer.append('}'); 1449 } 1450 1451 1452 1453 /** 1454 * Retrieves a normalized string representation of this JSON object. The 1455 * normalized representation of the JSON object will have the following 1456 * characteristics: 1457 * <UL> 1458 * <LI>It will not include any line breaks.</LI> 1459 * <LI>It will not include any spaces around the enclosing braces.</LI> 1460 * <LI>It will not include any spaces around the commas used to separate 1461 * fields.</LI> 1462 * <LI>Field names will be treated in a case-sensitive manner and will not 1463 * be altered.</LI> 1464 * <LI>Field values will be normalized.</LI> 1465 * <LI>Fields will be listed in lexicographic order by field name.</LI> 1466 * </UL> 1467 * 1468 * @return A normalized string representation of this JSON object. 1469 */ 1470 @Override() 1471 @NotNull() 1472 public String toNormalizedString() 1473 { 1474 final StringBuilder buffer = new StringBuilder(); 1475 toNormalizedString(buffer); 1476 return buffer.toString(); 1477 } 1478 1479 1480 1481 /** 1482 * Appends a normalized string representation of this JSON object to the 1483 * provided buffer. The normalized representation of the JSON object will 1484 * have the following characteristics: 1485 * <UL> 1486 * <LI>It will not include any line breaks.</LI> 1487 * <LI>It will not include any spaces around the enclosing braces.</LI> 1488 * <LI>It will not include any spaces around the commas used to separate 1489 * fields.</LI> 1490 * <LI>Field names will be treated in a case-sensitive manner and will not 1491 * be altered.</LI> 1492 * <LI>Field values will be normalized.</LI> 1493 * <LI>Fields will be listed in lexicographic order by field name.</LI> 1494 * </UL> 1495 * 1496 * @param buffer The buffer to which the information should be appended. 1497 */ 1498 @Override() 1499 public void toNormalizedString(@NotNull final StringBuilder buffer) 1500 { 1501 toNormalizedString(buffer, false, true, false); 1502 } 1503 1504 1505 1506 /** 1507 * Retrieves a normalized string representation of this JSON object. The 1508 * normalized representation of the JSON object will have the following 1509 * characteristics: 1510 * <UL> 1511 * <LI>It will not include any line breaks.</LI> 1512 * <LI>It will not include any spaces around the enclosing braces.</LI> 1513 * <LI>It will not include any spaces around the commas used to separate 1514 * fields.</LI> 1515 * <LI>Case sensitivity of field names and values will be controlled by 1516 * argument values. 1517 * <LI>Fields will be listed in lexicographic order by field name.</LI> 1518 * </UL> 1519 * 1520 * @param ignoreFieldNameCase Indicates whether field names should be 1521 * treated in a case-sensitive (if {@code false}) 1522 * or case-insensitive (if {@code true}) manner. 1523 * @param ignoreValueCase Indicates whether string field values should 1524 * be treated in a case-sensitive (if 1525 * {@code false}) or case-insensitive (if 1526 * {@code true}) manner. 1527 * @param ignoreArrayOrder Indicates whether the order of elements in an 1528 * array should be considered significant (if 1529 * {@code false}) or insignificant (if 1530 * {@code true}). 1531 * 1532 * @return A normalized string representation of this JSON object. 1533 */ 1534 @Override() 1535 @NotNull() 1536 public String toNormalizedString(final boolean ignoreFieldNameCase, 1537 final boolean ignoreValueCase, 1538 final boolean ignoreArrayOrder) 1539 { 1540 final StringBuilder buffer = new StringBuilder(); 1541 toNormalizedString(buffer, ignoreFieldNameCase, ignoreValueCase, 1542 ignoreArrayOrder); 1543 return buffer.toString(); 1544 } 1545 1546 1547 1548 /** 1549 * Appends a normalized string representation of this JSON object to the 1550 * provided buffer. The normalized representation of the JSON object will 1551 * have the following characteristics: 1552 * <UL> 1553 * <LI>It will not include any line breaks.</LI> 1554 * <LI>It will not include any spaces around the enclosing braces.</LI> 1555 * <LI>It will not include any spaces around the commas used to separate 1556 * fields.</LI> 1557 * <LI>Field names will be treated in a case-sensitive manner and will not 1558 * be altered.</LI> 1559 * <LI>Field values will be normalized.</LI> 1560 * <LI>Fields will be listed in lexicographic order by field name.</LI> 1561 * </UL> 1562 * 1563 * @param buffer The buffer to which the information should be 1564 * appended. 1565 * @param ignoreFieldNameCase Indicates whether field names should be 1566 * treated in a case-sensitive (if {@code false}) 1567 * or case-insensitive (if {@code true}) manner. 1568 * @param ignoreValueCase Indicates whether string field values should 1569 * be treated in a case-sensitive (if 1570 * {@code false}) or case-insensitive (if 1571 * {@code true}) manner. 1572 * @param ignoreArrayOrder Indicates whether the order of elements in an 1573 * array should be considered significant (if 1574 * {@code false}) or insignificant (if 1575 * {@code true}). 1576 */ 1577 @Override() 1578 public void toNormalizedString(@NotNull final StringBuilder buffer, 1579 final boolean ignoreFieldNameCase, 1580 final boolean ignoreValueCase, 1581 final boolean ignoreArrayOrder) 1582 { 1583 // The normalized representation needs to have the fields in a predictable 1584 // order, which we will accomplish using the lexicographic ordering that a 1585 // TreeMap will provide. Field names may or may not be treated in a 1586 // case-sensitive manner, but we still need to construct a normalized way of 1587 // escaping non-printable characters in each field. 1588 final TreeMap<String,String> m = new TreeMap<>(); 1589 for (final Map.Entry<String,JSONValue> e : fields.entrySet()) 1590 { 1591 m.put( 1592 new JSONString(e.getKey()).toNormalizedString(false, 1593 ignoreFieldNameCase, false), 1594 e.getValue().toNormalizedString(ignoreFieldNameCase, ignoreValueCase, 1595 ignoreArrayOrder)); 1596 } 1597 1598 buffer.append('{'); 1599 final Iterator<Map.Entry<String,String>> iterator = m.entrySet().iterator(); 1600 while (iterator.hasNext()) 1601 { 1602 final Map.Entry<String,String> e = iterator.next(); 1603 buffer.append(e.getKey()); 1604 buffer.append(':'); 1605 buffer.append(e.getValue()); 1606 1607 if (iterator.hasNext()) 1608 { 1609 buffer.append(','); 1610 } 1611 } 1612 1613 buffer.append('}'); 1614 } 1615 1616 1617 1618 /** 1619 * {@inheritDoc} 1620 */ 1621 @Override() 1622 public void appendToJSONBuffer(@NotNull final JSONBuffer buffer) 1623 { 1624 buffer.beginObject(); 1625 1626 for (final Map.Entry<String,JSONValue> field : fields.entrySet()) 1627 { 1628 final String name = field.getKey(); 1629 final JSONValue value = field.getValue(); 1630 value.appendToJSONBuffer(name, buffer); 1631 } 1632 1633 buffer.endObject(); 1634 } 1635 1636 1637 1638 /** 1639 * {@inheritDoc} 1640 */ 1641 @Override() 1642 public void appendToJSONBuffer(@NotNull final String fieldName, 1643 @NotNull final JSONBuffer buffer) 1644 { 1645 buffer.beginObject(fieldName); 1646 1647 for (final Map.Entry<String,JSONValue> field : fields.entrySet()) 1648 { 1649 final String name = field.getKey(); 1650 final JSONValue value = field.getValue(); 1651 value.appendToJSONBuffer(name, buffer); 1652 } 1653 1654 buffer.endObject(); 1655 } 1656}