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}