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