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