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