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