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