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