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