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