001/*
002 * Copyright 2015-2020 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2015-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) 2015-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.math.BigDecimal;
041import java.util.ArrayList;
042import java.util.Collections;
043import java.util.HashMap;
044import java.util.Iterator;
045import java.util.LinkedHashMap;
046import java.util.List;
047import java.util.Map;
048import java.util.TreeMap;
049
050import com.unboundid.util.Debug;
051import com.unboundid.util.NotMutable;
052import com.unboundid.util.StaticUtils;
053import com.unboundid.util.ThreadSafety;
054import com.unboundid.util.ThreadSafetyLevel;
055
056import static com.unboundid.util.json.JSONMessages.*;
057
058
059
060/**
061 * This class provides an implementation of a JSON value that represents an
062 * object with zero or more name-value pairs.  In each pair, the name is a JSON
063 * string and the value is any type of JSON value ({@code null}, {@code true},
064 * {@code false}, number, string, array, or object).  Although the ECMA-404
065 * specification does not explicitly forbid a JSON object from having multiple
066 * fields with the same name, RFC 7159 section 4 states that field names should
067 * be unique, and this implementation does not support objects in which multiple
068 * fields have the same name.  Note that this uniqueness constraint only applies
069 * to the fields directly contained within an object, and does not prevent an
070 * object from having a field value that is an object (or that is an array
071 * containing one or more objects) that use a field name that is also in use
072 * in the outer object.  Similarly, if an array contains multiple JSON objects,
073 * then there is no restriction preventing the same field names from being
074 * used in separate objects within that array.
075 * <BR><BR>
076 * The string representation of a JSON object is an open curly brace (U+007B)
077 * followed by a comma-delimited list of the name-value pairs that comprise the
078 * fields in that object and a closing curly brace (U+007D).  Each name-value
079 * pair is represented as a JSON string followed by a colon and the appropriate
080 * string representation of the value.  There must not be a comma between the
081 * last field and the closing curly brace.  There may optionally be any amount
082 * of whitespace (where whitespace characters include the ASCII space,
083 * horizontal tab, line feed, and carriage return characters) after the open
084 * curly brace, on either or both sides of the colon separating a field name
085 * from its value, on either or both sides of commas separating fields, and
086 * before the closing curly brace.  The order in which fields appear in the
087 * string representation is not considered significant.
088 * <BR><BR>
089 * The string representation returned by the {@link #toString()} method (or
090 * appended to the buffer provided to the {@link #toString(StringBuilder)}
091 * method) will include one space before each field name and one space before
092 * the closing curly brace.  There will not be any space on either side of the
093 * colon separating the field name from its value, and there will not be any
094 * space between a field value and the comma that follows it.  The string
095 * representation of each field name will use the same logic as the
096 * {@link JSONString#toString()} method, and the string representation of each
097 * field value will be obtained using that value's {@code toString} method.
098 * <BR><BR>
099 * The normalized string representation will not include any optional spaces,
100 * and the normalized string representation of each field value will be obtained
101 * using that value's {@code toNormalizedString} method.  Field names will be
102 * treated in a case-sensitive manner, but all characters outside the LDAP
103 * printable character set will be escaped using the {@code \}{@code u}-style
104 * Unicode encoding.  The normalized string representation will have fields
105 * listed in lexicographic order.
106 */
107@NotMutable()
108@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
109public final class JSONObject
110       extends JSONValue
111{
112  /**
113   * A pre-allocated empty JSON object.
114   */
115  public static final JSONObject EMPTY_OBJECT = new JSONObject(
116       Collections.<String,JSONValue>emptyMap());
117
118
119
120  /**
121   * The serial version UID for this serializable class.
122   */
123  private static final long serialVersionUID = -4209509956709292141L;
124
125
126
127  // A counter to use in decode processing.
128  private int decodePos;
129
130  // The hash code for this JSON object.
131  private Integer hashCode;
132
133  // The set of fields for this JSON object.
134  private final Map<String,JSONValue> fields;
135
136  // The string representation for this JSON object.
137  private String stringRepresentation;
138
139  // A buffer to use in decode processing.
140  private final StringBuilder decodeBuffer;
141
142
143
144  /**
145   * Creates a new JSON object with the provided fields.
146   *
147   * @param  fields  The fields to include in this JSON object.  It may be
148   *                 {@code null} or empty if this object should not have any
149   *                 fields.
150   */
151  public JSONObject(final JSONField... fields)
152  {
153    if ((fields == null) || (fields.length == 0))
154    {
155      this.fields = Collections.emptyMap();
156    }
157    else
158    {
159      final LinkedHashMap<String,JSONValue> m =
160           new LinkedHashMap<>(StaticUtils.computeMapCapacity(fields.length));
161      for (final JSONField f : fields)
162      {
163        m.put(f.getName(), f.getValue());
164      }
165      this.fields = Collections.unmodifiableMap(m);
166    }
167
168    hashCode = null;
169    stringRepresentation = null;
170
171    // We don't need to decode anything.
172    decodePos = -1;
173    decodeBuffer = null;
174  }
175
176
177
178  /**
179   * Creates a new JSON object with the provided fields.
180   *
181   * @param  fields  The set of fields for this JSON object.  It may be
182   *                 {@code null} or empty if there should not be any fields.
183   */
184  public JSONObject(final Map<String,JSONValue> fields)
185  {
186    if (fields == null)
187    {
188      this.fields = Collections.emptyMap();
189    }
190    else
191    {
192      this.fields = Collections.unmodifiableMap(new LinkedHashMap<>(fields));
193    }
194
195    hashCode = null;
196    stringRepresentation = null;
197
198    // We don't need to decode anything.
199    decodePos = -1;
200    decodeBuffer = null;
201  }
202
203
204
205  /**
206   * Creates a new JSON object parsed from the provided string.
207   *
208   * @param  stringRepresentation  The string to parse as a JSON object.  It
209   *                               must represent exactly one JSON object.
210   *
211   * @throws  JSONException  If the provided string cannot be parsed as a valid
212   *                         JSON object.
213   */
214  public JSONObject(final String stringRepresentation)
215         throws JSONException
216  {
217    this.stringRepresentation = stringRepresentation;
218
219    final char[] chars = stringRepresentation.toCharArray();
220    decodePos = 0;
221    decodeBuffer = new StringBuilder(chars.length);
222
223    // The JSON object must start with an open curly brace.
224    final Object firstToken = readToken(chars);
225    if (! firstToken.equals('{'))
226    {
227      throw new JSONException(ERR_OBJECT_DOESNT_START_WITH_BRACE.get(
228           stringRepresentation));
229    }
230
231    final LinkedHashMap<String,JSONValue> m =
232         new LinkedHashMap<>(StaticUtils.computeMapCapacity(10));
233    readObject(chars, m);
234    fields = Collections.unmodifiableMap(m);
235
236    skipWhitespace(chars);
237    if (decodePos < chars.length)
238    {
239      throw new JSONException(ERR_OBJECT_DATA_BEYOND_END.get(
240           stringRepresentation, decodePos));
241    }
242  }
243
244
245
246  /**
247   * Creates a new JSON object with the provided information.
248   *
249   * @param  fields                The set of fields for this JSON object.
250   * @param  stringRepresentation  The string representation for the JSON
251   *                               object.
252   */
253  JSONObject(final LinkedHashMap<String,JSONValue> fields,
254             final String stringRepresentation)
255  {
256    this.fields = Collections.unmodifiableMap(fields);
257    this.stringRepresentation = stringRepresentation;
258
259    hashCode = null;
260    decodePos = -1;
261    decodeBuffer = null;
262  }
263
264
265
266  /**
267   * Reads a token from the provided character array, skipping over any
268   * insignificant whitespace that may be before the token.  The token that is
269   * returned will be one of the following:
270   * <UL>
271   *   <LI>A {@code Character} that is an opening curly brace.</LI>
272   *   <LI>A {@code Character} that is a closing curly brace.</LI>
273   *   <LI>A {@code Character} that is an opening square bracket.</LI>
274   *   <LI>A {@code Character} that is a closing square bracket.</LI>
275   *   <LI>A {@code Character} that is a colon.</LI>
276   *   <LI>A {@code Character} that is a comma.</LI>
277   *   <LI>A {@link JSONBoolean}.</LI>
278   *   <LI>A {@link JSONNull}.</LI>
279   *   <LI>A {@link JSONNumber}.</LI>
280   *   <LI>A {@link JSONString}.</LI>
281   * </UL>
282   *
283   * @param  chars  The characters that comprise the string representation of
284   *                the JSON object.
285   *
286   * @return  The token that was read.
287   *
288   * @throws  JSONException  If a problem was encountered while reading the
289   *                         token.
290   */
291  private Object readToken(final char[] chars)
292          throws JSONException
293  {
294    skipWhitespace(chars);
295
296    final char c = readCharacter(chars, false);
297    switch (c)
298    {
299      case '{':
300      case '}':
301      case '[':
302      case ']':
303      case ':':
304      case ',':
305        // This is a token character that we will return as-is.
306        decodePos++;
307        return c;
308
309      case '"':
310        // This is the start of a JSON string.
311        return readString(chars);
312
313      case 't':
314      case 'f':
315        // This is the start of a JSON true or false value.
316        return readBoolean(chars);
317
318      case 'n':
319        // This is the start of a JSON null value.
320        return readNull(chars);
321
322      case '-':
323      case '0':
324      case '1':
325      case '2':
326      case '3':
327      case '4':
328      case '5':
329      case '6':
330      case '7':
331      case '8':
332      case '9':
333        // This is the start of a JSON number value.
334        return readNumber(chars);
335
336      default:
337        // This is not a valid JSON token.
338        throw new JSONException(ERR_OBJECT_INVALID_FIRST_TOKEN_CHAR.get(
339             new String(chars), String.valueOf(c), decodePos));
340
341    }
342  }
343
344
345
346  /**
347   * Skips over any valid JSON whitespace at the current position in the
348   * provided array.
349   *
350   * @param  chars  The characters that comprise the string representation of
351   *                the JSON object.
352   *
353   * @throws  JSONException  If a problem is encountered while skipping
354   *                         whitespace.
355   */
356  private void skipWhitespace(final char[] chars)
357          throws JSONException
358  {
359    while (decodePos < chars.length)
360    {
361      switch (chars[decodePos])
362      {
363        // The space, tab, newline, and carriage return characters are
364        // considered valid JSON whitespace.
365        case ' ':
366        case '\t':
367        case '\n':
368        case '\r':
369          decodePos++;
370          break;
371
372        // Technically, JSON does not provide support for comments.  But this
373        // implementation will accept three types of comments:
374        // - Comments that start with /* and end with */ (potentially spanning
375        //   multiple lines).
376        // - Comments that start with // and continue until the end of the line.
377        // - Comments that start with # and continue until the end of the line.
378        // All comments will be ignored by the parser.
379        case '/':
380          final int commentStartPos = decodePos;
381          if ((decodePos+1) >= chars.length)
382          {
383            return;
384          }
385          else if (chars[decodePos+1] == '/')
386          {
387            decodePos += 2;
388
389            // Keep reading until we encounter a newline or carriage return, or
390            // until we hit the end of the string.
391            while (decodePos < chars.length)
392            {
393              if ((chars[decodePos] == '\n') || (chars[decodePos] == '\r'))
394              {
395                break;
396              }
397              decodePos++;
398            }
399            break;
400          }
401          else if (chars[decodePos+1] == '*')
402          {
403            decodePos += 2;
404
405            // Keep reading until we encounter "*/".  We must encounter "*/"
406            // before hitting the end of the string.
407            boolean closeFound = false;
408            while (decodePos < chars.length)
409            {
410              if (chars[decodePos] == '*')
411              {
412                if (((decodePos+1) < chars.length) &&
413                    (chars[decodePos+1] == '/'))
414                {
415                  closeFound = true;
416                  decodePos += 2;
417                  break;
418                }
419              }
420              decodePos++;
421            }
422
423            if (! closeFound)
424            {
425              throw new JSONException(ERR_OBJECT_UNCLOSED_COMMENT.get(
426                   new String(chars), commentStartPos));
427            }
428            break;
429          }
430          else
431          {
432            return;
433          }
434
435        case '#':
436          // Keep reading until we encounter a newline or carriage return, or
437          // until we hit the end of the string.
438          while (decodePos < chars.length)
439          {
440            if ((chars[decodePos] == '\n') || (chars[decodePos] == '\r'))
441            {
442              break;
443            }
444            decodePos++;
445          }
446          break;
447
448        default:
449          return;
450      }
451    }
452  }
453
454
455
456  /**
457   * Reads the character at the specified position and optionally advances the
458   * position.
459   *
460   * @param  chars            The characters that comprise the string
461   *                          representation of the JSON object.
462   * @param  advancePosition  Indicates whether to advance the value of the
463   *                          position indicator after reading the character.
464   *                          If this is {@code false}, then this method will be
465   *                          used to "peek" at the next character without
466   *                          consuming it.
467   *
468   * @return  The character that was read.
469   *
470   * @throws  JSONException  If the end of the value was encountered when a
471   *                         character was expected.
472   */
473  private char readCharacter(final char[] chars, final boolean advancePosition)
474          throws JSONException
475  {
476    if (decodePos >= chars.length)
477    {
478      throw new JSONException(
479           ERR_OBJECT_UNEXPECTED_END_OF_STRING.get(new String(chars)));
480    }
481
482    final char c = chars[decodePos];
483    if (advancePosition)
484    {
485      decodePos++;
486    }
487    return c;
488  }
489
490
491
492  /**
493   * Reads a JSON string staring at the specified position in the provided
494   * character array.
495   *
496   * @param  chars  The characters that comprise the string representation of
497   *                the JSON object.
498   *
499   * @return  The JSON string that was read.
500   *
501   * @throws  JSONException  If a problem was encountered while reading the JSON
502   *                         string.
503   */
504  private JSONString readString(final char[] chars)
505          throws JSONException
506  {
507    // Create a buffer to hold the string.  Note that if we've gotten here then
508    // we already know that the character at the provided position is a quote,
509    // so we can read past it in the process.
510    final int startPos = decodePos++;
511    decodeBuffer.setLength(0);
512    while (true)
513    {
514      final char c = readCharacter(chars, true);
515      if (c == '\\')
516      {
517        final int escapedCharPos = decodePos;
518        final char escapedChar = readCharacter(chars, true);
519        switch (escapedChar)
520        {
521          case '"':
522          case '\\':
523          case '/':
524            decodeBuffer.append(escapedChar);
525            break;
526          case 'b':
527            decodeBuffer.append('\b');
528            break;
529          case 'f':
530            decodeBuffer.append('\f');
531            break;
532          case 'n':
533            decodeBuffer.append('\n');
534            break;
535          case 'r':
536            decodeBuffer.append('\r');
537            break;
538          case 't':
539            decodeBuffer.append('\t');
540            break;
541
542          case 'u':
543            final char[] hexChars =
544            {
545              readCharacter(chars, true),
546              readCharacter(chars, true),
547              readCharacter(chars, true),
548              readCharacter(chars, true)
549            };
550            try
551            {
552              decodeBuffer.append(
553                   (char) Integer.parseInt(new String(hexChars), 16));
554            }
555            catch (final Exception e)
556            {
557              Debug.debugException(e);
558              throw new JSONException(
559                   ERR_OBJECT_INVALID_UNICODE_ESCAPE.get(new String(chars),
560                        escapedCharPos),
561                   e);
562            }
563            break;
564
565          default:
566            throw new JSONException(ERR_OBJECT_INVALID_ESCAPED_CHAR.get(
567                 new String(chars), escapedChar, escapedCharPos));
568        }
569      }
570      else if (c == '"')
571      {
572        return new JSONString(decodeBuffer.toString(),
573             new String(chars, startPos, (decodePos - startPos)));
574      }
575      else
576      {
577        if (c <= '\u001F')
578        {
579          throw new JSONException(ERR_OBJECT_UNESCAPED_CONTROL_CHAR.get(
580               new String(chars), String.format("%04X", (int) c),
581               (decodePos - 1)));
582        }
583
584        decodeBuffer.append(c);
585      }
586    }
587  }
588
589
590
591  /**
592   * Reads a JSON Boolean staring at the specified position in the provided
593   * character array.
594   *
595   * @param  chars  The characters that comprise the string representation of
596   *                the JSON object.
597   *
598   * @return  The JSON Boolean that was read.
599   *
600   * @throws  JSONException  If a problem was encountered while reading the JSON
601   *                         Boolean.
602   */
603  private JSONBoolean readBoolean(final char[] chars)
604          throws JSONException
605  {
606    final int startPos = decodePos;
607    final char firstCharacter = readCharacter(chars, true);
608    if (firstCharacter == 't')
609    {
610      if ((readCharacter(chars, true) == 'r') &&
611          (readCharacter(chars, true) == 'u') &&
612          (readCharacter(chars, true) == 'e'))
613      {
614        return JSONBoolean.TRUE;
615      }
616    }
617    else if (firstCharacter == 'f')
618    {
619      if ((readCharacter(chars, true) == 'a') &&
620          (readCharacter(chars, true) == 'l') &&
621          (readCharacter(chars, true) == 's') &&
622          (readCharacter(chars, true) == 'e'))
623      {
624        return JSONBoolean.FALSE;
625      }
626    }
627
628    throw new JSONException(ERR_OBJECT_UNABLE_TO_PARSE_BOOLEAN.get(
629         new String(chars), startPos));
630  }
631
632
633
634  /**
635   * Reads a JSON null staring at the specified position in the provided
636   * character array.
637   *
638   * @param  chars  The characters that comprise the string representation of
639   *                the JSON object.
640   *
641   * @return  The JSON null that was read.
642   *
643   * @throws  JSONException  If a problem was encountered while reading the JSON
644   *                         null.
645   */
646  private JSONNull readNull(final char[] chars)
647          throws JSONException
648  {
649    final int startPos = decodePos;
650    if ((readCharacter(chars, true) == 'n') &&
651        (readCharacter(chars, true) == 'u') &&
652        (readCharacter(chars, true) == 'l') &&
653        (readCharacter(chars, true) == 'l'))
654    {
655      return JSONNull.NULL;
656    }
657
658    throw new JSONException(ERR_OBJECT_UNABLE_TO_PARSE_NULL.get(
659         new String(chars), startPos));
660  }
661
662
663
664  /**
665   * Reads a JSON number staring at the specified position in the provided
666   * character array.
667   *
668   * @param  chars  The characters that comprise the string representation of
669   *                the JSON object.
670   *
671   * @return  The JSON number that was read.
672   *
673   * @throws  JSONException  If a problem was encountered while reading the JSON
674   *                         number.
675   */
676  private JSONNumber readNumber(final char[] chars)
677          throws JSONException
678  {
679    // Read until we encounter whitespace, a comma, a closing square bracket, or
680    // a closing curly brace.  Then try to parse what we read as a number.
681    final int startPos = decodePos;
682    decodeBuffer.setLength(0);
683
684    while (true)
685    {
686      final char c = readCharacter(chars, true);
687      switch (c)
688      {
689        case ' ':
690        case '\t':
691        case '\n':
692        case '\r':
693        case ',':
694        case ']':
695        case '}':
696          // We need to decrement the position indicator since the last one we
697          // read wasn't part of the number.
698          decodePos--;
699          return new JSONNumber(decodeBuffer.toString());
700
701        default:
702          decodeBuffer.append(c);
703      }
704    }
705  }
706
707
708
709  /**
710   * Reads a JSON array starting at the specified position in the provided
711   * character array.  Note that this method assumes that the opening square
712   * bracket has already been read.
713   *
714   * @param  chars  The characters that comprise the string representation of
715   *                the JSON object.
716   *
717   * @return  The JSON array that was read.
718   *
719   * @throws  JSONException  If a problem was encountered while reading the JSON
720   *                         array.
721   */
722  private JSONArray readArray(final char[] chars)
723          throws JSONException
724  {
725    // The opening square bracket will have already been consumed, so read
726    // JSON values until we hit a closing square bracket.
727    final ArrayList<JSONValue> values = new ArrayList<>(10);
728    boolean firstToken = true;
729    while (true)
730    {
731      // If this is the first time through, it is acceptable to find a closing
732      // square bracket.  Otherwise, we expect to find a JSON value, an opening
733      // square bracket to denote the start of an embedded array, or an opening
734      // curly brace to denote the start of an embedded JSON object.
735      int p = decodePos;
736      Object token = readToken(chars);
737      if (token instanceof JSONValue)
738      {
739        values.add((JSONValue) token);
740      }
741      else if (token.equals('['))
742      {
743        values.add(readArray(chars));
744      }
745      else if (token.equals('{'))
746      {
747        final LinkedHashMap<String,JSONValue> fieldMap =
748             new LinkedHashMap<>(StaticUtils.computeMapCapacity(10));
749        values.add(readObject(chars, 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(
759             ERR_OBJECT_INVALID_TOKEN_WHEN_ARRAY_VALUE_EXPECTED.get(
760                  new String(chars), String.valueOf(token), p));
761      }
762
763      firstToken = false;
764
765
766      // If we've gotten here, then we found a JSON value.  It must be followed
767      // by either a comma (to indicate that there's at least one more value) or
768      // a closing square bracket (to denote the end of the array).
769      p = decodePos;
770      token = readToken(chars);
771      if (token.equals(']'))
772      {
773        return new JSONArray(values);
774      }
775      else if (! token.equals(','))
776      {
777        throw new JSONException(
778             ERR_OBJECT_INVALID_TOKEN_WHEN_ARRAY_COMMA_OR_BRACKET_EXPECTED.get(
779                  new String(chars), String.valueOf(token), p));
780      }
781    }
782  }
783
784
785
786  /**
787   * Reads a JSON object starting at the specified position in the provided
788   * character array.  Note that this method assumes that the opening curly
789   * brace has already been read.
790   *
791   * @param  chars   The characters that comprise the string representation of
792   *                 the JSON object.
793   * @param  fields  The map into which to place the fields that are read.  The
794   *                 returned object will include an unmodifiable view of this
795   *                 map, but the caller may use the map directly if desired.
796   *
797   * @return  The JSON object that was read.
798   *
799   * @throws  JSONException  If a problem was encountered while reading the JSON
800   *                         object.
801   */
802  private JSONObject readObject(final char[] chars,
803                                final Map<String,JSONValue> fields)
804          throws JSONException
805  {
806    boolean firstField = true;
807    while (true)
808    {
809      // Read the next token.  It must be a JSONString, unless we haven't read
810      // any fields yet in which case it can be a closing curly brace to
811      // indicate that it's an empty object.
812      int p = decodePos;
813      final String fieldName;
814      Object token = readToken(chars);
815      if (token instanceof JSONString)
816      {
817        fieldName = ((JSONString) token).stringValue();
818        if (fields.containsKey(fieldName))
819        {
820          throw new JSONException(ERR_OBJECT_DUPLICATE_FIELD.get(
821               new String(chars), fieldName));
822        }
823      }
824      else if (firstField && token.equals('}'))
825      {
826        return new JSONObject(fields);
827      }
828      else
829      {
830        throw new JSONException(ERR_OBJECT_EXPECTED_STRING.get(
831             new String(chars), String.valueOf(token), p));
832      }
833      firstField = false;
834
835      // Read the next token.  It must be a colon.
836      p = decodePos;
837      token = readToken(chars);
838      if (! token.equals(':'))
839      {
840        throw new JSONException(ERR_OBJECT_EXPECTED_COLON.get(new String(chars),
841             String.valueOf(token), p));
842      }
843
844      // Read the next token.  It must be one of the following:
845      // - A JSONValue
846      // - An opening square bracket, designating the start of an array.
847      // - An opening curly brace, designating the start of an object.
848      p = decodePos;
849      token = readToken(chars);
850      if (token instanceof JSONValue)
851      {
852        fields.put(fieldName, (JSONValue) token);
853      }
854      else if (token.equals('['))
855      {
856        final JSONArray a = readArray(chars);
857        fields.put(fieldName, a);
858      }
859      else if (token.equals('{'))
860      {
861        final LinkedHashMap<String,JSONValue> m =
862             new LinkedHashMap<>(StaticUtils.computeMapCapacity(10));
863        final JSONObject o = readObject(chars, m);
864        fields.put(fieldName, o);
865      }
866      else
867      {
868        throw new JSONException(ERR_OBJECT_EXPECTED_VALUE.get(new String(chars),
869             String.valueOf(token), p, fieldName));
870      }
871
872      // Read the next token.  It must be either a comma (to indicate that
873      // there will be another field) or a closing curly brace (to indicate
874      // that the end of the object has been reached).
875      p = decodePos;
876      token = readToken(chars);
877      if (token.equals('}'))
878      {
879        return new JSONObject(fields);
880      }
881      else if (! token.equals(','))
882      {
883        throw new JSONException(ERR_OBJECT_EXPECTED_COMMA_OR_CLOSE_BRACE.get(
884             new String(chars), String.valueOf(token), p));
885      }
886    }
887  }
888
889
890
891  /**
892   * Retrieves a map of the fields contained in this JSON object.
893   *
894   * @return  A map of the fields contained in this JSON object.
895   */
896  public Map<String,JSONValue> getFields()
897  {
898    return fields;
899  }
900
901
902
903  /**
904   * Retrieves the value for the specified field.
905   *
906   * @param  name  The name of the field for which to retrieve the value.  It
907   *               will be treated in a case-sensitive manner.
908   *
909   * @return  The value for the specified field, or {@code null} if the
910   *          requested field is not present in the JSON object.
911   */
912  public JSONValue getField(final String name)
913  {
914    return fields.get(name);
915  }
916
917
918
919  /**
920   * Retrieves the value of the specified field as a string.
921   *
922   * @param  name  The name of the field for which to retrieve the string value.
923   *               It will be treated in a case-sensitive manner.
924   *
925   * @return  The value of the specified field as a string, or {@code null} if
926   *          this JSON object does not have a field with the specified name, or
927   *          if the value of that field is not a string.
928   */
929  public String getFieldAsString(final String name)
930  {
931    final JSONValue value = fields.get(name);
932    if ((value == null) || (! (value instanceof JSONString)))
933    {
934      return null;
935    }
936
937    return ((JSONString) value).stringValue();
938  }
939
940
941
942  /**
943   * Retrieves the value of the specified field as a Boolean.
944   *
945   * @param  name  The name of the field for which to retrieve the Boolean
946   *               value.  It will be treated in a case-sensitive manner.
947   *
948   * @return  The value of the specified field as a Boolean, or {@code null} if
949   *          this JSON object does not have a field with the specified name, or
950   *          if the value of that field is not a Boolean.
951   */
952  public Boolean getFieldAsBoolean(final String name)
953  {
954    final JSONValue value = fields.get(name);
955    if ((value == null) || (! (value instanceof JSONBoolean)))
956    {
957      return null;
958    }
959
960    return ((JSONBoolean) value).booleanValue();
961  }
962
963
964
965  /**
966   * Retrieves the value of the specified field as an integer.
967   *
968   * @param  name  The name of the field for which to retrieve the integer
969   *               value.  It will be treated in a case-sensitive manner.
970   *
971   * @return  The value of the specified field as an integer, or {@code null} if
972   *          this JSON object does not have a field with the specified name, or
973   *          if the value of that field is not a number that can be exactly
974   *          represented as an integer.
975   */
976  public Integer getFieldAsInteger(final String name)
977  {
978    final JSONValue value = fields.get(name);
979    if ((value == null) || (! (value instanceof JSONNumber)))
980    {
981      return null;
982    }
983
984    try
985    {
986      final JSONNumber number = (JSONNumber) value;
987      return number.getValue().intValueExact();
988    }
989    catch (final Exception e)
990    {
991      Debug.debugException(e);
992      return null;
993    }
994  }
995
996
997
998  /**
999   * Retrieves the value of the specified field as a long.
1000   *
1001   * @param  name  The name of the field for which to retrieve the long value.
1002   *               It will be treated in a case-sensitive manner.
1003   *
1004   * @return  The value of the specified field as a long, or {@code null} if
1005   *          this JSON object does not have a field with the specified name, or
1006   *          if the value of that field is not a number that can be exactly
1007   *          represented as a long.
1008   */
1009  public Long getFieldAsLong(final String name)
1010  {
1011    final JSONValue value = fields.get(name);
1012    if ((value == null) || (! (value instanceof JSONNumber)))
1013    {
1014      return null;
1015    }
1016
1017    try
1018    {
1019      final JSONNumber number = (JSONNumber) value;
1020      return number.getValue().longValueExact();
1021    }
1022    catch (final Exception e)
1023    {
1024      Debug.debugException(e);
1025      return null;
1026    }
1027  }
1028
1029
1030
1031  /**
1032   * Retrieves the value of the specified field as a BigDecimal.
1033   *
1034   * @param  name  The name of the field for which to retrieve the BigDecimal
1035   *               value.  It will be treated in a case-sensitive manner.
1036   *
1037   * @return  The value of the specified field as a BigDecimal, or {@code null}
1038   *          if this JSON object does not have a field with the specified name,
1039   *          or if the value of that field is not a number.
1040   */
1041  public BigDecimal getFieldAsBigDecimal(final String name)
1042  {
1043    final JSONValue value = fields.get(name);
1044    if ((value == null) || (! (value instanceof JSONNumber)))
1045    {
1046      return null;
1047    }
1048
1049    return ((JSONNumber) value).getValue();
1050  }
1051
1052
1053
1054  /**
1055   * Retrieves the value of the specified field as a JSON object.
1056   *
1057   * @param  name  The name of the field for which to retrieve the value.  It
1058   *               will be treated in a case-sensitive manner.
1059   *
1060   * @return  The value of the specified field as a JSON object, or {@code null}
1061   *          if this JSON object does not have a field with the specified name,
1062   *          or if the value of that field is not an object.
1063   */
1064  public JSONObject getFieldAsObject(final String name)
1065  {
1066    final JSONValue value = fields.get(name);
1067    if ((value == null) || (! (value instanceof JSONObject)))
1068    {
1069      return null;
1070    }
1071
1072    return (JSONObject) value;
1073  }
1074
1075
1076
1077  /**
1078   * Retrieves a list of the elements in the specified array field.
1079   *
1080   * @param  name  The name of the field for which to retrieve the array values.
1081   *               It will be treated in a case-sensitive manner.
1082   *
1083   * @return  A list of the elements in the specified array field, or
1084   *          {@code null} if this JSON object does not have a field with the
1085   *          specified name, or if the value of that field is not an array.
1086   */
1087  public List<JSONValue> getFieldAsArray(final String name)
1088  {
1089    final JSONValue value = fields.get(name);
1090    if ((value == null) || (! (value instanceof JSONArray)))
1091    {
1092      return null;
1093    }
1094
1095    return ((JSONArray) value).getValues();
1096  }
1097
1098
1099
1100  /**
1101   * Indicates whether this JSON object has a null field with the specified
1102   * name.
1103   *
1104   * @param  name  The name of the field for which to make the determination.
1105   *               It will be treated in a case-sensitive manner.
1106   *
1107   * @return  {@code true} if this JSON object has a null field with the
1108   *          specified name, or {@code false} if this JSON object does not have
1109   *          a field with the specified name, or if the value of that field is
1110   *          not a null.
1111   */
1112  public boolean hasNullField(final String name)
1113  {
1114    final JSONValue value = fields.get(name);
1115    return ((value != null) && (value instanceof JSONNull));
1116  }
1117
1118
1119
1120  /**
1121   * Indicates whether this JSON object has a field with the specified name.
1122   *
1123   * @param  fieldName  The name of the field for which to make the
1124   *                    determination.  It will be treated in a case-sensitive
1125   *                    manner.
1126   *
1127   * @return  {@code true} if this JSON object has a field with the specified
1128   *          name, or {@code false} if not.
1129   */
1130  public boolean hasField(final String fieldName)
1131  {
1132    return fields.containsKey(fieldName);
1133  }
1134
1135
1136
1137  /**
1138   * {@inheritDoc}
1139   */
1140  @Override()
1141  public int hashCode()
1142  {
1143    if (hashCode == null)
1144    {
1145      int hc = 0;
1146      for (final Map.Entry<String,JSONValue> e : fields.entrySet())
1147      {
1148        hc += e.getKey().hashCode() + e.getValue().hashCode();
1149      }
1150
1151      hashCode = hc;
1152    }
1153
1154    return hashCode;
1155  }
1156
1157
1158
1159  /**
1160   * {@inheritDoc}
1161   */
1162  @Override()
1163  public boolean equals(final Object o)
1164  {
1165    if (o == this)
1166    {
1167      return true;
1168    }
1169
1170    if (o instanceof JSONObject)
1171    {
1172      final JSONObject obj = (JSONObject) o;
1173      return fields.equals(obj.fields);
1174    }
1175
1176    return false;
1177  }
1178
1179
1180
1181  /**
1182   * Indicates whether this JSON object is considered equal to the provided
1183   * object, subject to the specified constraints.
1184   *
1185   * @param  o                    The object to compare against this JSON
1186   *                              object.  It must not be {@code null}.
1187   * @param  ignoreFieldNameCase  Indicates whether to ignore differences in
1188   *                              capitalization in field names.
1189   * @param  ignoreValueCase      Indicates whether to ignore differences in
1190   *                              capitalization in values that are JSON
1191   *                              strings.
1192   * @param  ignoreArrayOrder     Indicates whether to ignore differences in the
1193   *                              order of elements within an array.
1194   *
1195   * @return  {@code true} if this JSON object is considered equal to the
1196   *          provided object (subject to the specified constraints), or
1197   *          {@code false} if not.
1198   */
1199  public boolean equals(final JSONObject o, final boolean ignoreFieldNameCase,
1200                        final boolean ignoreValueCase,
1201                        final boolean ignoreArrayOrder)
1202  {
1203    // See if we can do a straight-up Map.equals.  If so, just do that.
1204    if ((! ignoreFieldNameCase) && (! ignoreValueCase) && (! ignoreArrayOrder))
1205    {
1206      return fields.equals(o.fields);
1207    }
1208
1209    // Make sure they have the same number of fields.
1210    if (fields.size() != o.fields.size())
1211    {
1212      return false;
1213    }
1214
1215    // Optimize for the case in which we field names are case sensitive.
1216    if (! ignoreFieldNameCase)
1217    {
1218      for (final Map.Entry<String,JSONValue> e : fields.entrySet())
1219      {
1220        final JSONValue thisValue = e.getValue();
1221        final JSONValue thatValue = o.fields.get(e.getKey());
1222        if (thatValue == null)
1223        {
1224          return false;
1225        }
1226
1227        if (! thisValue.equals(thatValue, ignoreFieldNameCase, ignoreValueCase,
1228             ignoreArrayOrder))
1229        {
1230          return false;
1231        }
1232      }
1233
1234      return true;
1235    }
1236
1237
1238    // If we've gotten here, then we know that we need to treat field names in
1239    // a case-insensitive manner.  Create a new map that we can remove fields
1240    // from as we find matches.  This can help avoid false-positive matches in
1241    // which multiple fields in the first map match the same field in the second
1242    // map (e.g., because they have field names that differ only in case and
1243    // values that are logically equivalent).  It also makes iterating through
1244    // the values faster as we make more progress.
1245    final HashMap<String,JSONValue> thatMap = new HashMap<>(o.fields);
1246    final Iterator<Map.Entry<String,JSONValue>> thisIterator =
1247         fields.entrySet().iterator();
1248    while (thisIterator.hasNext())
1249    {
1250      final Map.Entry<String,JSONValue> thisEntry = thisIterator.next();
1251      final String thisFieldName = thisEntry.getKey();
1252      final JSONValue thisValue = thisEntry.getValue();
1253
1254      final Iterator<Map.Entry<String,JSONValue>> thatIterator =
1255           thatMap.entrySet().iterator();
1256
1257      boolean found = false;
1258      while (thatIterator.hasNext())
1259      {
1260        final Map.Entry<String,JSONValue> thatEntry = thatIterator.next();
1261        final String thatFieldName = thatEntry.getKey();
1262        if (! thisFieldName.equalsIgnoreCase(thatFieldName))
1263        {
1264          continue;
1265        }
1266
1267        final JSONValue thatValue = thatEntry.getValue();
1268        if (thisValue.equals(thatValue, ignoreFieldNameCase, ignoreValueCase,
1269             ignoreArrayOrder))
1270        {
1271          found = true;
1272          thatIterator.remove();
1273          break;
1274        }
1275      }
1276
1277      if (! found)
1278      {
1279        return false;
1280      }
1281    }
1282
1283    return true;
1284  }
1285
1286
1287
1288  /**
1289   * {@inheritDoc}
1290   */
1291  @Override()
1292  public boolean equals(final JSONValue v, final boolean ignoreFieldNameCase,
1293                        final boolean ignoreValueCase,
1294                        final boolean ignoreArrayOrder)
1295  {
1296    return ((v instanceof JSONObject) &&
1297         equals((JSONObject) v, ignoreFieldNameCase, ignoreValueCase,
1298              ignoreArrayOrder));
1299  }
1300
1301
1302
1303  /**
1304   * Retrieves a string representation of this JSON object.  If this object was
1305   * decoded from a string, then the original string representation will be
1306   * used.  Otherwise, a single-line string representation will be constructed.
1307   *
1308   * @return  A string representation of this JSON object.
1309   */
1310  @Override()
1311  public String toString()
1312  {
1313    if (stringRepresentation == null)
1314    {
1315      final StringBuilder buffer = new StringBuilder();
1316      toString(buffer);
1317      stringRepresentation = buffer.toString();
1318    }
1319
1320    return stringRepresentation;
1321  }
1322
1323
1324
1325  /**
1326   * Appends a string representation of this JSON object to the provided buffer.
1327   * If this object was decoded from a string, then the original string
1328   * representation will be used.  Otherwise, a single-line string
1329   * representation will be constructed.
1330   *
1331   * @param  buffer  The buffer to which the information should be appended.
1332   */
1333  @Override()
1334  public void toString(final StringBuilder buffer)
1335  {
1336    if (stringRepresentation != null)
1337    {
1338      buffer.append(stringRepresentation);
1339      return;
1340    }
1341
1342    buffer.append("{ ");
1343
1344    final Iterator<Map.Entry<String,JSONValue>> iterator =
1345         fields.entrySet().iterator();
1346    while (iterator.hasNext())
1347    {
1348      final Map.Entry<String,JSONValue> e = iterator.next();
1349      JSONString.encodeString(e.getKey(), buffer);
1350      buffer.append(':');
1351      e.getValue().toString(buffer);
1352
1353      if (iterator.hasNext())
1354      {
1355        buffer.append(',');
1356      }
1357      buffer.append(' ');
1358    }
1359
1360    buffer.append('}');
1361  }
1362
1363
1364
1365  /**
1366   * Retrieves a user-friendly string representation of this JSON object that
1367   * may be formatted across multiple lines for better readability.  The last
1368   * line will not include a trailing line break.
1369   *
1370   * @return  A user-friendly string representation of this JSON object that may
1371   *          be formatted across multiple lines for better readability.
1372   */
1373  public String toMultiLineString()
1374  {
1375    final JSONBuffer jsonBuffer = new JSONBuffer(null, 0, true);
1376    appendToJSONBuffer(jsonBuffer);
1377    return jsonBuffer.toString();
1378  }
1379
1380
1381
1382  /**
1383   * Retrieves a single-line string representation of this JSON object.
1384   *
1385   * @return  A single-line string representation of this JSON object.
1386   */
1387  @Override()
1388  public String toSingleLineString()
1389  {
1390    final StringBuilder buffer = new StringBuilder();
1391    toSingleLineString(buffer);
1392    return buffer.toString();
1393  }
1394
1395
1396
1397  /**
1398   * Appends a single-line string representation of this JSON object to the
1399   * provided buffer.
1400   *
1401   * @param  buffer  The buffer to which the information should be appended.
1402   */
1403  @Override()
1404  public void toSingleLineString(final StringBuilder buffer)
1405  {
1406    buffer.append("{ ");
1407
1408    final Iterator<Map.Entry<String,JSONValue>> iterator =
1409         fields.entrySet().iterator();
1410    while (iterator.hasNext())
1411    {
1412      final Map.Entry<String,JSONValue> e = iterator.next();
1413      JSONString.encodeString(e.getKey(), buffer);
1414      buffer.append(':');
1415      e.getValue().toSingleLineString(buffer);
1416
1417      if (iterator.hasNext())
1418      {
1419        buffer.append(',');
1420      }
1421      buffer.append(' ');
1422    }
1423
1424    buffer.append('}');
1425  }
1426
1427
1428
1429  /**
1430   * Retrieves a normalized string representation of this JSON object.  The
1431   * normalized representation of the JSON object will have the following
1432   * characteristics:
1433   * <UL>
1434   *   <LI>It will not include any line breaks.</LI>
1435   *   <LI>It will not include any spaces around the enclosing braces.</LI>
1436   *   <LI>It will not include any spaces around the commas used to separate
1437   *       fields.</LI>
1438   *   <LI>Field names will be treated in a case-sensitive manner and will not
1439   *       be altered.</LI>
1440   *   <LI>Field values will be normalized.</LI>
1441   *   <LI>Fields will be listed in lexicographic order by field name.</LI>
1442   * </UL>
1443   *
1444   * @return  A normalized string representation of this JSON object.
1445   */
1446  @Override()
1447  public String toNormalizedString()
1448  {
1449    final StringBuilder buffer = new StringBuilder();
1450    toNormalizedString(buffer);
1451    return buffer.toString();
1452  }
1453
1454
1455
1456  /**
1457   * Appends a normalized string representation of this JSON object to the
1458   * provided buffer.  The normalized representation of the JSON object will
1459   * have the following characteristics:
1460   * <UL>
1461   *   <LI>It will not include any line breaks.</LI>
1462   *   <LI>It will not include any spaces around the enclosing braces.</LI>
1463   *   <LI>It will not include any spaces around the commas used to separate
1464   *       fields.</LI>
1465   *   <LI>Field names will be treated in a case-sensitive manner and will not
1466   *       be altered.</LI>
1467   *   <LI>Field values will be normalized.</LI>
1468   *   <LI>Fields will be listed in lexicographic order by field name.</LI>
1469   * </UL>
1470   *
1471   * @param  buffer  The buffer to which the information should be appended.
1472   */
1473  @Override()
1474  public void toNormalizedString(final StringBuilder buffer)
1475  {
1476    toNormalizedString(buffer, false, true, false);
1477  }
1478
1479
1480
1481  /**
1482   * Retrieves a normalized string representation of this JSON object.  The
1483   * normalized representation of the JSON object will have the following
1484   * characteristics:
1485   * <UL>
1486   *   <LI>It will not include any line breaks.</LI>
1487   *   <LI>It will not include any spaces around the enclosing braces.</LI>
1488   *   <LI>It will not include any spaces around the commas used to separate
1489   *       fields.</LI>
1490   *   <LI>Case sensitivity of field names and values will be controlled by
1491   *       argument values.
1492   *   <LI>Fields will be listed in lexicographic order by field name.</LI>
1493   * </UL>
1494   *
1495   * @param  ignoreFieldNameCase  Indicates whether field names should be
1496   *                              treated in a case-sensitive (if {@code false})
1497   *                              or case-insensitive (if {@code true}) manner.
1498   * @param  ignoreValueCase      Indicates whether string field values should
1499   *                              be treated in a case-sensitive (if
1500   *                              {@code false}) or case-insensitive (if
1501   *                              {@code true}) manner.
1502   * @param  ignoreArrayOrder     Indicates whether the order of elements in an
1503   *                              array should be considered significant (if
1504   *                              {@code false}) or insignificant (if
1505   *                              {@code true}).
1506   *
1507   * @return  A normalized string representation of this JSON object.
1508   */
1509  @Override()
1510  public String toNormalizedString(final boolean ignoreFieldNameCase,
1511                                   final boolean ignoreValueCase,
1512                                   final boolean ignoreArrayOrder)
1513  {
1514    final StringBuilder buffer = new StringBuilder();
1515    toNormalizedString(buffer, ignoreFieldNameCase, ignoreValueCase,
1516         ignoreArrayOrder);
1517    return buffer.toString();
1518  }
1519
1520
1521
1522  /**
1523   * Appends a normalized string representation of this JSON object to the
1524   * provided buffer.  The normalized representation of the JSON object will
1525   * have the following characteristics:
1526   * <UL>
1527   *   <LI>It will not include any line breaks.</LI>
1528   *   <LI>It will not include any spaces around the enclosing braces.</LI>
1529   *   <LI>It will not include any spaces around the commas used to separate
1530   *       fields.</LI>
1531   *   <LI>Field names will be treated in a case-sensitive manner and will not
1532   *       be altered.</LI>
1533   *   <LI>Field values will be normalized.</LI>
1534   *   <LI>Fields will be listed in lexicographic order by field name.</LI>
1535   * </UL>
1536   *
1537   * @param  buffer               The buffer to which the information should be
1538   *                              appended.
1539   * @param  ignoreFieldNameCase  Indicates whether field names should be
1540   *                              treated in a case-sensitive (if {@code false})
1541   *                              or case-insensitive (if {@code true}) manner.
1542   * @param  ignoreValueCase      Indicates whether string field values should
1543   *                              be treated in a case-sensitive (if
1544   *                              {@code false}) or case-insensitive (if
1545   *                              {@code true}) manner.
1546   * @param  ignoreArrayOrder     Indicates whether the order of elements in an
1547   *                              array should be considered significant (if
1548   *                              {@code false}) or insignificant (if
1549   *                              {@code true}).
1550   */
1551  @Override()
1552  public void toNormalizedString(final StringBuilder buffer,
1553                                 final boolean ignoreFieldNameCase,
1554                                 final boolean ignoreValueCase,
1555                                 final boolean ignoreArrayOrder)
1556  {
1557    // The normalized representation needs to have the fields in a predictable
1558    // order, which we will accomplish using the lexicographic ordering that a
1559    // TreeMap will provide.  Field names may or may not be treated in a
1560    // case-sensitive manner, but we still need to construct a normalized way of
1561    // escaping non-printable characters in each field.
1562    final TreeMap<String,String> m = new TreeMap<>();
1563    for (final Map.Entry<String,JSONValue> e : fields.entrySet())
1564    {
1565      m.put(
1566           new JSONString(e.getKey()).toNormalizedString(false,
1567                ignoreFieldNameCase, false),
1568           e.getValue().toNormalizedString(ignoreFieldNameCase, ignoreValueCase,
1569                ignoreArrayOrder));
1570    }
1571
1572    buffer.append('{');
1573    final Iterator<Map.Entry<String,String>> iterator = m.entrySet().iterator();
1574    while (iterator.hasNext())
1575    {
1576      final Map.Entry<String,String> e = iterator.next();
1577      buffer.append(e.getKey());
1578      buffer.append(':');
1579      buffer.append(e.getValue());
1580
1581      if (iterator.hasNext())
1582      {
1583        buffer.append(',');
1584      }
1585    }
1586
1587    buffer.append('}');
1588  }
1589
1590
1591
1592  /**
1593   * {@inheritDoc}
1594   */
1595  @Override()
1596  public void appendToJSONBuffer(final JSONBuffer buffer)
1597  {
1598    buffer.beginObject();
1599
1600    for (final Map.Entry<String,JSONValue> field : fields.entrySet())
1601    {
1602      final String name = field.getKey();
1603      final JSONValue value = field.getValue();
1604      value.appendToJSONBuffer(name, buffer);
1605    }
1606
1607    buffer.endObject();
1608  }
1609
1610
1611
1612  /**
1613   * {@inheritDoc}
1614   */
1615  @Override()
1616  public void appendToJSONBuffer(final String fieldName,
1617                                 final JSONBuffer buffer)
1618  {
1619    buffer.beginObject(fieldName);
1620
1621    for (final Map.Entry<String,JSONValue> field : fields.entrySet())
1622    {
1623      final String name = field.getKey();
1624      final JSONValue value = field.getValue();
1625      value.appendToJSONBuffer(name, buffer);
1626    }
1627
1628    buffer.endObject();
1629  }
1630}