001 /*
002 * Copyright 2016 UnboundID Corp.
003 * All Rights Reserved.
004 */
005 /*
006 * Copyright (C) 2016 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 */
021 package com.unboundid.util.json;
022
023
024
025 import java.io.BufferedInputStream;
026 import java.io.Closeable;
027 import java.io.InputStream;
028 import java.io.IOException;
029 import java.util.ArrayList;
030 import java.util.LinkedHashMap;
031 import java.util.Map;
032
033 import com.unboundid.util.ByteStringBuffer;
034 import com.unboundid.util.Debug;
035 import com.unboundid.util.StaticUtils;
036 import com.unboundid.util.ThreadSafety;
037 import com.unboundid.util.ThreadSafetyLevel;
038
039 import 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)
049 public 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 }