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