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