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}