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