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