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