001/* 002 * Copyright 2016-2024 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright 2016-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) 2016-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.io.BufferedInputStream; 041import java.io.Closeable; 042import java.io.InputStream; 043import java.io.IOException; 044import java.util.ArrayList; 045import java.util.LinkedHashMap; 046import java.util.Map; 047 048import com.unboundid.util.ByteStringBuffer; 049import com.unboundid.util.Debug; 050import com.unboundid.util.NotNull; 051import com.unboundid.util.Nullable; 052import com.unboundid.util.StaticUtils; 053import com.unboundid.util.ThreadSafety; 054import com.unboundid.util.ThreadSafetyLevel; 055 056import static com.unboundid.util.json.JSONMessages.*; 057 058 059 060/** 061 * This class provides a mechanism for reading JSON objects from an input 062 * stream. It assumes that any non-ASCII data that may be read from the input 063 * stream is encoded as UTF-8. 064 */ 065@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE) 066public final class JSONObjectReader 067 implements Closeable 068{ 069 // The buffer used to hold the bytes of the object currently being read. 070 @NotNull private final ByteStringBuffer currentObjectBytes; 071 072 // A buffer to use to hold strings being decoded. 073 @NotNull private final ByteStringBuffer stringBuffer; 074 075 // The input stream from which JSON objects will be read. 076 @NotNull private final InputStream inputStream; 077 078 079 080 /** 081 * Creates a new JSON object reader that will read objects from the provided 082 * input stream. 083 * 084 * @param inputStream The input stream from which the data should be read. 085 */ 086 public JSONObjectReader(@NotNull final InputStream inputStream) 087 { 088 this(inputStream, true); 089 } 090 091 092 093 /** 094 * Creates a new JSON object reader that will read objects from the provided 095 * input stream. 096 * 097 * @param inputStream The input stream from which the data should be 098 * read. 099 * @param bufferInputStream Indicates whether to buffer the input stream. 100 * This should be {@code false} if the input stream 101 * could be used for any purpose other than reading 102 * JSON objects after one or more objects are read. 103 */ 104 public JSONObjectReader(@NotNull final InputStream inputStream, 105 final boolean bufferInputStream) 106 { 107 if (bufferInputStream && (! (inputStream instanceof BufferedInputStream))) 108 { 109 this.inputStream = new BufferedInputStream(inputStream); 110 } 111 else 112 { 113 this.inputStream = inputStream; 114 } 115 116 currentObjectBytes = new ByteStringBuffer(); 117 stringBuffer = new ByteStringBuffer(); 118 } 119 120 121 122 /** 123 * Reads the next JSON object from the input stream. 124 * 125 * @return The JSON object that was read, or {@code null} if the end of the 126 * end of the stream has been reached. 127 * 128 * @throws IOException If a problem is encountered while reading from the 129 * input stream. 130 * 131 * @throws JSONException If the data read 132 */ 133 @Nullable() 134 public JSONObject readObject() 135 throws IOException, JSONException 136 { 137 // Skip over any whitespace before the beginning of the next object. 138 skipWhitespace(); 139 currentObjectBytes.clear(); 140 141 142 // The JSON object must start with an open curly brace. 143 final Object firstToken = readToken(true); 144 if (firstToken == null) 145 { 146 return null; 147 } 148 149 if (! firstToken.equals('{')) 150 { 151 throw new JSONException(ERR_OBJECT_READER_ILLEGAL_START_OF_OBJECT.get( 152 String.valueOf(firstToken))); 153 } 154 155 final LinkedHashMap<String,JSONValue> m = 156 new LinkedHashMap<>(StaticUtils.computeMapCapacity(10)); 157 readObject(m); 158 159 return new JSONObject(m, currentObjectBytes.toString()); 160 } 161 162 163 164 /** 165 * Closes this JSON object reader and the underlying input stream. 166 * 167 * @throws IOException If a problem is encountered while closing the 168 * underlying input stream. 169 */ 170 @Override() 171 public void close() 172 throws IOException 173 { 174 inputStream.close(); 175 } 176 177 178 179 /** 180 * Reads a token from the input stream, skipping over any insignificant 181 * whitespace that may be before the token. The token that is returned will 182 * be one of the following: 183 * <UL> 184 * <LI>A {@code Character} that is an opening curly brace.</LI> 185 * <LI>A {@code Character} that is a closing curly brace.</LI> 186 * <LI>A {@code Character} that is an opening square bracket.</LI> 187 * <LI>A {@code Character} that is a closing square bracket.</LI> 188 * <LI>A {@code Character} that is a colon.</LI> 189 * <LI>A {@code Character} that is a comma.</LI> 190 * <LI>A {@link JSONBoolean}.</LI> 191 * <LI>A {@link JSONNull}.</LI> 192 * <LI>A {@link JSONNumber}.</LI> 193 * <LI>A {@link JSONString}.</LI> 194 * </UL> 195 * 196 * @param allowEndOfStream Indicates whether it is acceptable to encounter 197 * the end of the input stream. This should only 198 * be {@code true} when the token is expected to be 199 * the open parenthesis of the outermost JSON 200 * object. 201 * 202 * @return The token that was read, or {@code null} if the end of the input 203 * stream was reached. 204 * 205 * @throws IOException If a problem is encountered while reading from the 206 * input stream. 207 * 208 * @throws JSONException If a problem was encountered while reading the 209 * token. 210 */ 211 @Nullable() 212 private Object readToken(final boolean allowEndOfStream) 213 throws IOException, JSONException 214 { 215 skipWhitespace(); 216 217 final Byte byteRead = readByte(allowEndOfStream); 218 if (byteRead == null) 219 { 220 return null; 221 } 222 223 switch (byteRead) 224 { 225 case '{': 226 return '{'; 227 case '}': 228 return '}'; 229 case '[': 230 return '['; 231 case ']': 232 return ']'; 233 case ':': 234 return ':'; 235 case ',': 236 return ','; 237 238 case '"': 239 // This is the start of a JSON string. 240 return readString(); 241 242 case 't': 243 case 'f': 244 // This is the start of a JSON true or false value. 245 return readBoolean(); 246 247 case 'n': 248 // This is the start of a JSON null value. 249 return readNull(); 250 251 case '-': 252 case '0': 253 case '1': 254 case '2': 255 case '3': 256 case '4': 257 case '5': 258 case '6': 259 case '7': 260 case '8': 261 case '9': 262 // This is the start of a JSON number value. 263 return readNumber(); 264 265 default: 266 throw new JSONException( 267 ERR_OBJECT_READER_ILLEGAL_FIRST_CHAR_FOR_JSON_TOKEN.get( 268 currentObjectBytes.length(), byteToCharString(byteRead))); 269 } 270 } 271 272 273 274 /** 275 * Skips over any valid JSON whitespace at the current position in the input 276 * stream. 277 * 278 * @throws IOException If a problem is encountered while reading from the 279 * input stream. 280 * 281 * @throws JSONException If a problem is encountered while skipping 282 * whitespace. 283 */ 284 private void skipWhitespace() 285 throws IOException, JSONException 286 { 287 while (true) 288 { 289 inputStream.mark(1); 290 final Byte byteRead = readByte(true); 291 if (byteRead == null) 292 { 293 // We've reached the end of the input stream. 294 return; 295 } 296 297 switch (byteRead) 298 { 299 case ' ': 300 case '\t': 301 case '\n': 302 case '\r': 303 // Spaces, tabs, newlines, and carriage returns are valid JSON 304 // whitespace. 305 break; 306 307 // Technically, JSON does not provide support for comments. But this 308 // implementation will accept three types of comments: 309 // - Comments that start with /* and end with */ (potentially spanning 310 // multiple lines). 311 // - Comments that start with // and continue until the end of the line. 312 // - Comments that start with # and continue until the end of the line. 313 // All comments will be ignored by the parser. 314 case '/': 315 // This probably starts a comment. If so, then the next byte must be 316 // either another forward slash or an asterisk. 317 final byte nextByte = readByte(false); 318 if (nextByte == '/') 319 { 320 // Keep reading until we encounter a newline, a carriage return, or 321 // the end of the input stream. 322 while (true) 323 { 324 final Byte commentByte = readByte(true); 325 if (commentByte == null) 326 { 327 return; 328 } 329 330 if ((commentByte == '\n') || (commentByte == '\r')) 331 { 332 break; 333 } 334 } 335 } 336 else if (nextByte == '*') 337 { 338 // Keep reading until we encounter an asterisk followed by a slash. 339 // If we hit the end of the input stream before that, then that's an 340 // error. 341 while (true) 342 { 343 final Byte commentByte = readByte(false); 344 if (commentByte == '*') 345 { 346 final Byte possibleSlashByte = readByte(false); 347 if (possibleSlashByte == '/') 348 { 349 break; 350 } 351 } 352 } 353 } 354 else 355 { 356 throw new JSONException( 357 ERR_OBJECT_READER_ILLEGAL_SLASH_SKIPPING_WHITESPACE.get( 358 currentObjectBytes.length())); 359 } 360 break; 361 362 case '#': 363 // Keep reading until we encounter a newline, a carriage return, or 364 // the end of the input stream. 365 while (true) 366 { 367 final Byte commentByte = readByte(true); 368 if (commentByte == null) 369 { 370 return; 371 } 372 373 if ((commentByte == '\n') || (commentByte == '\r')) 374 { 375 break; 376 } 377 } 378 break; 379 380 default: 381 // We read a byte that isn't whitespace, so we'll need to reset the 382 // stream so it will be read again, and we'll also need to remove the 383 // that byte from the currentObjectBytes buffer. 384 inputStream.reset(); 385 currentObjectBytes.setLength(currentObjectBytes.length() - 1); 386 return; 387 } 388 } 389 } 390 391 392 393 /** 394 * Reads the next byte from the input stream. 395 * 396 * @param allowEndOfStream Indicates whether it is acceptable to encounter 397 * the end of the input stream. This should only 398 * be {@code true} when the token is expected to be 399 * the open parenthesis of the outermost JSON 400 * object. 401 * 402 * @return The next byte read from the input stream, or {@code null} if the 403 * end of the input stream has been reached and that is acceptable. 404 * 405 * @throws IOException If a problem is encountered while reading from the 406 * input stream. 407 * 408 * @throws JSONException If the end of the input stream is reached when that 409 * is not acceptable. 410 */ 411 @Nullable() 412 private Byte readByte(final boolean allowEndOfStream) 413 throws IOException, JSONException 414 { 415 final int byteRead = inputStream.read(); 416 if (byteRead < 0) 417 { 418 if (allowEndOfStream) 419 { 420 return null; 421 } 422 else 423 { 424 throw new JSONException(ERR_OBJECT_READER_UNEXPECTED_END_OF_STREAM.get( 425 currentObjectBytes.length())); 426 } 427 } 428 429 final byte b = (byte) (byteRead & 0xFF); 430 currentObjectBytes.append(b); 431 return b; 432 } 433 434 435 436 /** 437 * Reads a string from the input stream. The open quotation must have already 438 * been read. 439 * 440 * @return The JSON string that was read. 441 * 442 * @throws IOException If a problem is encountered while reading from the 443 * input stream. 444 * 445 * @throws JSONException If a problem was encountered while reading the JSON 446 * string. 447 */ 448 @NotNull() 449 private JSONString readString() 450 throws IOException, JSONException 451 { 452 // Use a buffer to hold the string being decoded. Also mark the current 453 // position in the bytes that comprise the string representation so that 454 // the JSON string representation (including the opening quote) will be 455 // exactly as it was provided. 456 stringBuffer.clear(); 457 final int jsonStringStartPos = currentObjectBytes.length() - 1; 458 while (true) 459 { 460 final Byte byteRead = readByte(false); 461 462 // See if it's a non-ASCII byte. If so, then assume that it's UTF-8 and 463 // read the appropriate number of remaining bytes. We need to handle this 464 // specially to avoid incorrectly detecting the end of the string because 465 // a subsequent byte in a multi-byte character happens to be the same as 466 // the ASCII quotation mark byte. 467 if ((byteRead & 0x80) == 0x80) 468 { 469 final byte[] charBytes; 470 if ((byteRead & 0xE0) == 0xC0) 471 { 472 // It's a two-byte character. 473 charBytes = new byte[] 474 { 475 byteRead, 476 readByte(false) 477 }; 478 } 479 else if ((byteRead & 0xF0) == 0xE0) 480 { 481 // It's a three-byte character. 482 charBytes = new byte[] 483 { 484 byteRead, 485 readByte(false), 486 readByte(false) 487 }; 488 } 489 else if ((byteRead & 0xF8) == 0xF0) 490 { 491 // It's a four-byte character. 492 charBytes = new byte[] 493 { 494 byteRead, 495 readByte(false), 496 readByte(false), 497 readByte(false) 498 }; 499 } 500 else 501 { 502 // This isn't a valid UTF-8 sequence. 503 throw new JSONException( 504 ERR_OBJECT_READER_INVALID_UTF_8_BYTE_IN_STREAM.get( 505 currentObjectBytes.length(), 506 "0x" + StaticUtils.toHex(byteRead))); 507 } 508 509 stringBuffer.append(StaticUtils.toUTF8String(charBytes)); 510 continue; 511 } 512 513 514 // If the byte that we read was an escape, then we know that whatever 515 // immediately follows it shouldn't be allowed to signal the end of the 516 // string. 517 if (byteRead == '\\') 518 { 519 final byte nextByte = readByte(false); 520 switch (nextByte) 521 { 522 case '"': 523 case '\\': 524 case '/': 525 stringBuffer.append(nextByte); 526 break; 527 case 'b': 528 stringBuffer.append('\b'); 529 break; 530 case 'f': 531 stringBuffer.append('\f'); 532 break; 533 case 'n': 534 stringBuffer.append('\n'); 535 break; 536 case 'r': 537 stringBuffer.append('\r'); 538 break; 539 case 't': 540 stringBuffer.append('\t'); 541 break; 542 case 'u': 543 final char[] hexChars = 544 { 545 (char) (readByte(false) & 0xFF), 546 (char) (readByte(false) & 0xFF), 547 (char) (readByte(false) & 0xFF), 548 (char) (readByte(false) & 0xFF) 549 }; 550 551 try 552 { 553 stringBuffer.append( 554 (char) Integer.parseInt(new String(hexChars), 16)); 555 } 556 catch (final Exception e) 557 { 558 Debug.debugException(e); 559 throw new JSONException( 560 ERR_OBJECT_READER_INVALID_UNICODE_ESCAPE.get( 561 currentObjectBytes.length()), 562 e); 563 } 564 break; 565 default: 566 throw new JSONException( 567 ERR_OBJECT_READER_INVALID_ESCAPED_CHAR.get( 568 currentObjectBytes.length(), byteToCharString(nextByte))); 569 } 570 continue; 571 } 572 573 if (byteRead == '"') 574 { 575 // It's an unescaped quote, so it marks the end of the string. 576 return new JSONString(stringBuffer.toString(), 577 StaticUtils.toUTF8String(currentObjectBytes.getBackingArray(), 578 jsonStringStartPos, 579 (currentObjectBytes.length() - jsonStringStartPos))); 580 } 581 582 final int byteReadInt = (byteRead & 0xFF); 583 if ((byteRead & 0xFF) <= 0x1F) 584 { 585 throw new JSONException(ERR_OBJECT_READER_UNESCAPED_CONTROL_CHAR.get( 586 currentObjectBytes.length(), byteToCharString(byteRead))); 587 } 588 else 589 { 590 stringBuffer.append((char) byteReadInt); 591 } 592 } 593 } 594 595 596 597 /** 598 * Reads a JSON Boolean from the input stream. The first byte of either 't' 599 * or 'f' will have already been read. 600 * 601 * @return The JSON Boolean that was read. 602 * 603 * @throws IOException If a problem is encountered while reading from the 604 * input stream. 605 * 606 * @throws JSONException If a problem was encountered while reading the JSON 607 * Boolean. 608 */ 609 @NotNull() 610 private JSONBoolean readBoolean() 611 throws IOException, JSONException 612 { 613 final byte firstByte = 614 currentObjectBytes.getBackingArray()[currentObjectBytes.length() - 1]; 615 if (firstByte == 't') 616 { 617 if ((readByte(false) == 'r') && 618 (readByte(false) == 'u') && 619 (readByte(false) == 'e')) 620 { 621 return JSONBoolean.TRUE; 622 } 623 624 throw new JSONException(ERR_OBJECT_READER_INVALID_BOOLEAN_TRUE.get( 625 currentObjectBytes.length())); 626 } 627 else 628 { 629 if ((readByte(false) == 'a') && 630 (readByte(false) == 'l') && 631 (readByte(false) == 's') && 632 (readByte(false) == 'e')) 633 { 634 return JSONBoolean.FALSE; 635 } 636 637 throw new JSONException(ERR_OBJECT_READER_INVALID_BOOLEAN_FALSE.get( 638 currentObjectBytes.length())); 639 } 640 } 641 642 643 644 /** 645 * Reads a JSON Boolean from the input stream. The first byte of 'n' will 646 * have already been read. 647 * 648 * @return The JSON null that was read. 649 * 650 * @throws IOException If a problem is encountered while reading from the 651 * input stream. 652 * 653 * @throws JSONException If a problem was encountered while reading the JSON 654 * null. 655 */ 656 @NotNull() 657 private JSONNull readNull() 658 throws IOException, JSONException 659 { 660 if ((readByte(false) == 'u') && 661 (readByte(false) == 'l') && 662 (readByte(false) == 'l')) 663 { 664 return JSONNull.NULL; 665 } 666 667 throw new JSONException(ERR_OBJECT_READER_INVALID_NULL.get( 668 currentObjectBytes.length())); 669 } 670 671 672 673 /** 674 * Reads a JSON number from the input stream. The first byte of the number 675 * will have already been read. 676 * 677 * @throws IOException If a problem is encountered while reading from the 678 * input stream. 679 * 680 * @return The JSON number that was read. 681 * 682 * @throws IOException If a problem is encountered while reading from the 683 * input stream. 684 * 685 * @throws JSONException If a problem was encountered while reading the JSON 686 * number. 687 */ 688 @NotNull() 689 private JSONNumber readNumber() 690 throws IOException, JSONException 691 { 692 // Use a buffer to hold the string representation of the number being 693 // decoded. Since the first byte of the number has already been read, we'll 694 // need to add it into the buffer. 695 stringBuffer.clear(); 696 stringBuffer.append( 697 currentObjectBytes.getBackingArray()[currentObjectBytes.length() - 1]); 698 699 700 // Read until we encounter whitespace, a comma, a closing square bracket, or 701 // a closing curly brace. Then try to parse what we read as a number. 702 while (true) 703 { 704 // Mark the stream so that if we read a byte that isn't part of the 705 // number, we'll be able to rewind the stream so that byte will be read 706 // again by something else. 707 inputStream.mark(1); 708 709 final Byte b = readByte(false); 710 switch (b) 711 { 712 case ' ': 713 case '\t': 714 case '\n': 715 case '\r': 716 case ',': 717 case ']': 718 case '}': 719 // This tell us we're at the end of the number. Rewind the stream so 720 // that we can read this last byte again whatever tries to get the 721 // next token. Also remove it from the end of currentObjectBytes 722 // since it will be re-added when it's read again. 723 inputStream.reset(); 724 currentObjectBytes.setLength(currentObjectBytes.length() - 1); 725 return new JSONNumber(stringBuffer.toString()); 726 727 default: 728 stringBuffer.append(b); 729 } 730 } 731 } 732 733 734 735 /** 736 * Reads a JSON array from the input stream. The opening square bracket will 737 * have already been read. 738 * 739 * @return The JSON array that was read. 740 * 741 * @throws IOException If a problem is encountered while reading from the 742 * input stream. 743 * 744 * @throws JSONException If a problem was encountered while reading the JSON 745 * array. 746 */ 747 @NotNull() 748 private JSONArray readArray() 749 throws IOException, JSONException 750 { 751 // The opening square bracket will have already been consumed, so read 752 // JSON values until we hit a closing square bracket. 753 final ArrayList<JSONValue> values = new ArrayList<>(10); 754 boolean firstToken = true; 755 while (true) 756 { 757 // If this is the first time through, it is acceptable to find a closing 758 // square bracket. Otherwise, we expect to find a JSON value, an opening 759 // square bracket to denote the start of an embedded array, or an opening 760 // curly brace to denote the start of an embedded JSON object. 761 final Object token = readToken(false); 762 if (token instanceof JSONValue) 763 { 764 values.add((JSONValue) token); 765 } 766 else if (token.equals('[')) 767 { 768 values.add(readArray()); 769 } 770 else if (token.equals('{')) 771 { 772 final LinkedHashMap<String,JSONValue> fieldMap = 773 new LinkedHashMap<>(StaticUtils.computeMapCapacity(10)); 774 values.add(readObject(fieldMap)); 775 } 776 else if (token.equals(']') && firstToken) 777 { 778 // It's an empty array. 779 return JSONArray.EMPTY_ARRAY; 780 } 781 else 782 { 783 throw new JSONException(ERR_OBJECT_READER_INVALID_TOKEN_IN_ARRAY.get( 784 currentObjectBytes.length(), String.valueOf(token))); 785 } 786 787 firstToken = false; 788 789 790 // If we've gotten here, then we found a JSON value. It must be followed 791 // by either a comma (to indicate that there's at least one more value) or 792 // a closing square bracket (to denote the end of the array). 793 final Object nextToken = readToken(false); 794 if (nextToken.equals(']')) 795 { 796 return new JSONArray(values); 797 } 798 else if (! nextToken.equals(',')) 799 { 800 throw new JSONException( 801 ERR_OBJECT_READER_INVALID_TOKEN_AFTER_ARRAY_VALUE.get( 802 currentObjectBytes.length(), String.valueOf(nextToken))); 803 } 804 } 805 } 806 807 808 809 /** 810 * Reads a JSON object from the input stream. The opening curly brace will 811 * have already been read. 812 * 813 * @param fields The map into which to place the fields that are read. The 814 * returned object will include an unmodifiable view of this 815 * map, but the caller may use the map directly if desired. 816 * 817 * @return The JSON object that was read. 818 * 819 * @throws IOException If a problem is encountered while reading from the 820 * input stream. 821 * 822 * @throws JSONException If a problem was encountered while reading the JSON 823 * object. 824 */ 825 @NotNull() 826 private JSONObject readObject(@NotNull final Map<String,JSONValue> fields) 827 throws IOException, JSONException 828 { 829 boolean firstField = true; 830 while (true) 831 { 832 // Read the next token. It must be a JSONString, unless we haven't read 833 // any fields yet in which case it can be a closing curly brace to 834 // indicate that it's an empty object. 835 final String fieldName; 836 final Object fieldNameToken = readToken(false); 837 if (fieldNameToken instanceof JSONString) 838 { 839 fieldName = ((JSONString) fieldNameToken).stringValue(); 840 if (fields.containsKey(fieldName)) 841 { 842 throw new JSONException(ERR_OBJECT_READER_DUPLICATE_FIELD.get( 843 currentObjectBytes.length(), fieldName)); 844 } 845 } 846 else if (firstField && fieldNameToken.equals('}')) 847 { 848 return new JSONObject(fields); 849 } 850 else 851 { 852 throw new JSONException(ERR_OBJECT_READER_INVALID_TOKEN_IN_OBJECT.get( 853 currentObjectBytes.length(), String.valueOf(fieldNameToken))); 854 } 855 firstField = false; 856 857 // Read the next token. It must be a colon. 858 final Object colonToken = readToken(false); 859 if (! colonToken.equals(':')) 860 { 861 throw new JSONException(ERR_OBJECT_READER_TOKEN_NOT_COLON.get( 862 currentObjectBytes.length(), String.valueOf(colonToken), 863 String.valueOf(fieldNameToken))); 864 } 865 866 // Read the next token. It must be one of the following: 867 // - A JSONValue 868 // - An opening square bracket, designating the start of an array. 869 // - An opening curly brace, designating the start of an object. 870 final Object valueToken = readToken(false); 871 if (valueToken instanceof JSONValue) 872 { 873 fields.put(fieldName, (JSONValue) valueToken); 874 } 875 else if (valueToken.equals('[')) 876 { 877 final JSONArray a = readArray(); 878 fields.put(fieldName, a); 879 } 880 else if (valueToken.equals('{')) 881 { 882 final LinkedHashMap<String,JSONValue> m = 883 new LinkedHashMap<>(StaticUtils.computeMapCapacity(10)); 884 final JSONObject o = readObject(m); 885 fields.put(fieldName, o); 886 } 887 else 888 { 889 throw new JSONException(ERR_OBJECT_READER_TOKEN_NOT_VALUE.get( 890 currentObjectBytes.length(), String.valueOf(valueToken), 891 String.valueOf(fieldNameToken))); 892 } 893 894 // Read the next token. It must be either a comma (to indicate that 895 // there will be another field) or a closing curly brace (to indicate 896 // that the end of the object has been reached). 897 final Object separatorToken = readToken(false); 898 if (separatorToken.equals('}')) 899 { 900 return new JSONObject(fields); 901 } 902 else if (! separatorToken.equals(',')) 903 { 904 throw new JSONException( 905 ERR_OBJECT_READER_INVALID_TOKEN_AFTER_OBJECT_VALUE.get( 906 currentObjectBytes.length(), String.valueOf(separatorToken), 907 String.valueOf(fieldNameToken))); 908 } 909 } 910 } 911 912 913 914 /** 915 * Retrieves a string representation of the provided byte that is intended to 916 * represent a character. If the provided byte is a printable ASCII 917 * character, then that character will be used. Otherwise, the string 918 * representation will be "0x" followed by the hexadecimal representation of 919 * the byte. 920 * 921 * @param b The byte for which to obtain the string representation. 922 * 923 * @return A string representation of the provided byte. 924 */ 925 @NotNull() 926 private static String byteToCharString(final byte b) 927 { 928 if ((b >= ' ') && (b <= '~')) 929 { 930 return String.valueOf((char) (b & 0xFF)); 931 } 932 else 933 { 934 return "0x" + StaticUtils.toHex(b); 935 } 936 } 937}