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