com.unboundid.util.json.JSONObject Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of unboundid-ldapsdk Show documentation
Show all versions of unboundid-ldapsdk Show documentation
The UnboundID LDAP SDK for Java is a fast, comprehensive, and easy-to-use
Java API for communicating with LDAP directory servers and performing
related tasks like reading and writing LDIF, encoding and decoding data
using base64 and ASN.1 BER, and performing secure communication. This
package contains the Standard Edition of the LDAP SDK, which is a
complete, general-purpose library for communicating with LDAPv3 directory
servers.
/*
* Copyright 2015-2022 Ping Identity Corporation
* All Rights Reserved.
*/
/*
* Copyright 2015-2022 Ping Identity Corporation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* Copyright (C) 2015-2022 Ping Identity Corporation
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License (GPLv2 only)
* or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, see .
*/
package com.unboundid.util.json;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import com.unboundid.util.Debug;
import com.unboundid.util.NotMutable;
import com.unboundid.util.NotNull;
import com.unboundid.util.Nullable;
import com.unboundid.util.StaticUtils;
import com.unboundid.util.ThreadSafety;
import com.unboundid.util.ThreadSafetyLevel;
import static com.unboundid.util.json.JSONMessages.*;
/**
* This class provides an implementation of a JSON value that represents an
* object with zero or more name-value pairs. In each pair, the name is a JSON
* string and the value is any type of JSON value ({@code null}, {@code true},
* {@code false}, number, string, array, or object). Although the ECMA-404
* specification does not explicitly forbid a JSON object from having multiple
* fields with the same name, RFC 7159 section 4 states that field names should
* be unique, and this implementation does not support objects in which multiple
* fields have the same name. Note that this uniqueness constraint only applies
* to the fields directly contained within an object, and does not prevent an
* object from having a field value that is an object (or that is an array
* containing one or more objects) that use a field name that is also in use
* in the outer object. Similarly, if an array contains multiple JSON objects,
* then there is no restriction preventing the same field names from being
* used in separate objects within that array.
*
* The string representation of a JSON object is an open curly brace (U+007B)
* followed by a comma-delimited list of the name-value pairs that comprise the
* fields in that object and a closing curly brace (U+007D). Each name-value
* pair is represented as a JSON string followed by a colon and the appropriate
* string representation of the value. There must not be a comma between the
* last field and the closing curly brace. There may optionally be any amount
* of whitespace (where whitespace characters include the ASCII space,
* horizontal tab, line feed, and carriage return characters) after the open
* curly brace, on either or both sides of the colon separating a field name
* from its value, on either or both sides of commas separating fields, and
* before the closing curly brace. The order in which fields appear in the
* string representation is not considered significant.
*
* The string representation returned by the {@link #toString()} method (or
* appended to the buffer provided to the {@link #toString(StringBuilder)}
* method) will include one space before each field name and one space before
* the closing curly brace. There will not be any space on either side of the
* colon separating the field name from its value, and there will not be any
* space between a field value and the comma that follows it. The string
* representation of each field name will use the same logic as the
* {@link JSONString#toString()} method, and the string representation of each
* field value will be obtained using that value's {@code toString} method.
*
* The normalized string representation will not include any optional spaces,
* and the normalized string representation of each field value will be obtained
* using that value's {@code toNormalizedString} method. Field names will be
* treated in a case-sensitive manner, but all characters outside the LDAP
* printable character set will be escaped using the {@code \}{@code u}-style
* Unicode encoding. The normalized string representation will have fields
* listed in lexicographic order.
*/
@NotMutable()
@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
public final class JSONObject
extends JSONValue
{
/**
* A pre-allocated empty JSON object.
*/
@NotNull public static final JSONObject EMPTY_OBJECT = new JSONObject(
Collections.emptyMap());
/**
* The serial version UID for this serializable class.
*/
private static final long serialVersionUID = -4209509956709292141L;
// A counter to use in decode processing.
private int decodePos;
// The hash code for this JSON object.
@Nullable private Integer hashCode;
// The set of fields for this JSON object.
@NotNull private final Map fields;
// The string representation for this JSON object.
@Nullable private String stringRepresentation;
// A buffer to use in decode processing.
@Nullable private final StringBuilder decodeBuffer;
/**
* Creates a new JSON object with the provided fields.
*
* @param fields The fields to include in this JSON object. It may be
* {@code null} or empty if this object should not have any
* fields.
*/
public JSONObject(@Nullable final JSONField... fields)
{
if ((fields == null) || (fields.length == 0))
{
this.fields = Collections.emptyMap();
}
else
{
final LinkedHashMap m =
new LinkedHashMap<>(StaticUtils.computeMapCapacity(fields.length));
for (final JSONField f : fields)
{
m.put(f.getName(), f.getValue());
}
this.fields = Collections.unmodifiableMap(m);
}
hashCode = null;
stringRepresentation = null;
// We don't need to decode anything.
decodePos = -1;
decodeBuffer = null;
}
/**
* Creates a new JSON object with the provided fields.
*
* @param fields The set of fields for this JSON object. It may be
* {@code null} or empty if there should not be any fields.
*/
public JSONObject(@Nullable final Map fields)
{
if (fields == null)
{
this.fields = Collections.emptyMap();
}
else
{
this.fields = Collections.unmodifiableMap(new LinkedHashMap<>(fields));
}
hashCode = null;
stringRepresentation = null;
// We don't need to decode anything.
decodePos = -1;
decodeBuffer = null;
}
/**
* Creates a new JSON object parsed from the provided string.
*
* @param stringRepresentation The string to parse as a JSON object. It
* must represent exactly one JSON object.
*
* @throws JSONException If the provided string cannot be parsed as a valid
* JSON object.
*/
public JSONObject(@NotNull final String stringRepresentation)
throws JSONException
{
this.stringRepresentation = stringRepresentation;
final char[] chars = stringRepresentation.toCharArray();
decodePos = 0;
decodeBuffer = new StringBuilder(chars.length);
// The JSON object must start with an open curly brace.
final Object firstToken = readToken(chars);
if (! firstToken.equals('{'))
{
throw new JSONException(ERR_OBJECT_DOESNT_START_WITH_BRACE.get(
stringRepresentation));
}
final LinkedHashMap m =
new LinkedHashMap<>(StaticUtils.computeMapCapacity(10));
readObject(chars, m);
fields = Collections.unmodifiableMap(m);
skipWhitespace(chars);
if (decodePos < chars.length)
{
throw new JSONException(ERR_OBJECT_DATA_BEYOND_END.get(
stringRepresentation, decodePos));
}
}
/**
* Creates a new JSON object with the provided information.
*
* @param fields The set of fields for this JSON object.
* @param stringRepresentation The string representation for the JSON
* object.
*/
JSONObject(@NotNull final LinkedHashMap fields,
@NotNull final String stringRepresentation)
{
this.fields = Collections.unmodifiableMap(fields);
this.stringRepresentation = stringRepresentation;
hashCode = null;
decodePos = -1;
decodeBuffer = null;
}
/**
* Reads a token from the provided character array, skipping over any
* insignificant whitespace that may be before the token. The token that is
* returned will be one of the following:
*
* - A {@code Character} that is an opening curly brace.
* - A {@code Character} that is a closing curly brace.
* - A {@code Character} that is an opening square bracket.
* - A {@code Character} that is a closing square bracket.
* - A {@code Character} that is a colon.
* - A {@code Character} that is a comma.
* - A {@link JSONBoolean}.
* - A {@link JSONNull}.
* - A {@link JSONNumber}.
* - A {@link JSONString}.
*
*
* @param chars The characters that comprise the string representation of
* the JSON object.
*
* @return The token that was read.
*
* @throws JSONException If a problem was encountered while reading the
* token.
*/
@NotNull()
private Object readToken(@NotNull final char[] chars)
throws JSONException
{
skipWhitespace(chars);
final char c = readCharacter(chars, false);
switch (c)
{
case '{':
case '}':
case '[':
case ']':
case ':':
case ',':
// This is a token character that we will return as-is.
decodePos++;
return c;
case '"':
// This is the start of a JSON string.
return readString(chars);
case 't':
case 'f':
// This is the start of a JSON true or false value.
return readBoolean(chars);
case 'n':
// This is the start of a JSON null value.
return readNull(chars);
case '-':
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
// This is the start of a JSON number value.
return readNumber(chars);
default:
// This is not a valid JSON token.
throw new JSONException(ERR_OBJECT_INVALID_FIRST_TOKEN_CHAR.get(
new String(chars), String.valueOf(c), decodePos));
}
}
/**
* Skips over any valid JSON whitespace at the current position in the
* provided array.
*
* @param chars The characters that comprise the string representation of
* the JSON object.
*
* @throws JSONException If a problem is encountered while skipping
* whitespace.
*/
private void skipWhitespace(@NotNull final char[] chars)
throws JSONException
{
while (decodePos < chars.length)
{
switch (chars[decodePos])
{
// The space, tab, newline, and carriage return characters are
// considered valid JSON whitespace.
case ' ':
case '\t':
case '\n':
case '\r':
decodePos++;
break;
// Technically, JSON does not provide support for comments. But this
// implementation will accept three types of comments:
// - Comments that start with /* and end with */ (potentially spanning
// multiple lines).
// - Comments that start with // and continue until the end of the line.
// - Comments that start with # and continue until the end of the line.
// All comments will be ignored by the parser.
case '/':
final int commentStartPos = decodePos;
if ((decodePos+1) >= chars.length)
{
return;
}
else if (chars[decodePos+1] == '/')
{
decodePos += 2;
// Keep reading until we encounter a newline or carriage return, or
// until we hit the end of the string.
while (decodePos < chars.length)
{
if ((chars[decodePos] == '\n') || (chars[decodePos] == '\r'))
{
break;
}
decodePos++;
}
break;
}
else if (chars[decodePos+1] == '*')
{
decodePos += 2;
// Keep reading until we encounter "*/". We must encounter "*/"
// before hitting the end of the string.
boolean closeFound = false;
while (decodePos < chars.length)
{
if (chars[decodePos] == '*')
{
if (((decodePos+1) < chars.length) &&
(chars[decodePos+1] == '/'))
{
closeFound = true;
decodePos += 2;
break;
}
}
decodePos++;
}
if (! closeFound)
{
throw new JSONException(ERR_OBJECT_UNCLOSED_COMMENT.get(
new String(chars), commentStartPos));
}
break;
}
else
{
return;
}
case '#':
// Keep reading until we encounter a newline or carriage return, or
// until we hit the end of the string.
while (decodePos < chars.length)
{
if ((chars[decodePos] == '\n') || (chars[decodePos] == '\r'))
{
break;
}
decodePos++;
}
break;
default:
return;
}
}
}
/**
* Reads the character at the specified position and optionally advances the
* position.
*
* @param chars The characters that comprise the string
* representation of the JSON object.
* @param advancePosition Indicates whether to advance the value of the
* position indicator after reading the character.
* If this is {@code false}, then this method will be
* used to "peek" at the next character without
* consuming it.
*
* @return The character that was read.
*
* @throws JSONException If the end of the value was encountered when a
* character was expected.
*/
private char readCharacter(@NotNull final char[] chars,
final boolean advancePosition)
throws JSONException
{
if (decodePos >= chars.length)
{
throw new JSONException(
ERR_OBJECT_UNEXPECTED_END_OF_STRING.get(new String(chars)));
}
final char c = chars[decodePos];
if (advancePosition)
{
decodePos++;
}
return c;
}
/**
* Reads a JSON string staring at the specified position in the provided
* character array.
*
* @param chars The characters that comprise the string representation of
* the JSON object.
*
* @return The JSON string that was read.
*
* @throws JSONException If a problem was encountered while reading the JSON
* string.
*/
@NotNull()
private JSONString readString(@NotNull final char[] chars)
throws JSONException
{
// Create a buffer to hold the string. Note that if we've gotten here then
// we already know that the character at the provided position is a quote,
// so we can read past it in the process.
final int startPos = decodePos++;
decodeBuffer.setLength(0);
while (true)
{
final char c = readCharacter(chars, true);
if (c == '\\')
{
final int escapedCharPos = decodePos;
final char escapedChar = readCharacter(chars, true);
switch (escapedChar)
{
case '"':
case '\\':
case '/':
decodeBuffer.append(escapedChar);
break;
case 'b':
decodeBuffer.append('\b');
break;
case 'f':
decodeBuffer.append('\f');
break;
case 'n':
decodeBuffer.append('\n');
break;
case 'r':
decodeBuffer.append('\r');
break;
case 't':
decodeBuffer.append('\t');
break;
case 'u':
final char[] hexChars =
{
readCharacter(chars, true),
readCharacter(chars, true),
readCharacter(chars, true),
readCharacter(chars, true)
};
try
{
decodeBuffer.append(
(char) Integer.parseInt(new String(hexChars), 16));
}
catch (final Exception e)
{
Debug.debugException(e);
throw new JSONException(
ERR_OBJECT_INVALID_UNICODE_ESCAPE.get(new String(chars),
escapedCharPos),
e);
}
break;
default:
throw new JSONException(ERR_OBJECT_INVALID_ESCAPED_CHAR.get(
new String(chars), escapedChar, escapedCharPos));
}
}
else if (c == '"')
{
return new JSONString(decodeBuffer.toString(),
new String(chars, startPos, (decodePos - startPos)));
}
else
{
if (c <= '\u001F')
{
throw new JSONException(ERR_OBJECT_UNESCAPED_CONTROL_CHAR.get(
new String(chars), String.format("%04X", (int) c),
(decodePos - 1)));
}
decodeBuffer.append(c);
}
}
}
/**
* Reads a JSON Boolean staring at the specified position in the provided
* character array.
*
* @param chars The characters that comprise the string representation of
* the JSON object.
*
* @return The JSON Boolean that was read.
*
* @throws JSONException If a problem was encountered while reading the JSON
* Boolean.
*/
@NotNull()
private JSONBoolean readBoolean(@NotNull final char[] chars)
throws JSONException
{
final int startPos = decodePos;
final char firstCharacter = readCharacter(chars, true);
if (firstCharacter == 't')
{
if ((readCharacter(chars, true) == 'r') &&
(readCharacter(chars, true) == 'u') &&
(readCharacter(chars, true) == 'e'))
{
return JSONBoolean.TRUE;
}
}
else if (firstCharacter == 'f')
{
if ((readCharacter(chars, true) == 'a') &&
(readCharacter(chars, true) == 'l') &&
(readCharacter(chars, true) == 's') &&
(readCharacter(chars, true) == 'e'))
{
return JSONBoolean.FALSE;
}
}
throw new JSONException(ERR_OBJECT_UNABLE_TO_PARSE_BOOLEAN.get(
new String(chars), startPos));
}
/**
* Reads a JSON null staring at the specified position in the provided
* character array.
*
* @param chars The characters that comprise the string representation of
* the JSON object.
*
* @return The JSON null that was read.
*
* @throws JSONException If a problem was encountered while reading the JSON
* null.
*/
@NotNull()
private JSONNull readNull(@NotNull final char[] chars)
throws JSONException
{
final int startPos = decodePos;
if ((readCharacter(chars, true) == 'n') &&
(readCharacter(chars, true) == 'u') &&
(readCharacter(chars, true) == 'l') &&
(readCharacter(chars, true) == 'l'))
{
return JSONNull.NULL;
}
throw new JSONException(ERR_OBJECT_UNABLE_TO_PARSE_NULL.get(
new String(chars), startPos));
}
/**
* Reads a JSON number staring at the specified position in the provided
* character array.
*
* @param chars The characters that comprise the string representation of
* the JSON object.
*
* @return The JSON number that was read.
*
* @throws JSONException If a problem was encountered while reading the JSON
* number.
*/
@NotNull()
private JSONNumber readNumber(@NotNull final char[] chars)
throws JSONException
{
// Read until we encounter whitespace, a comma, a closing square bracket, or
// a closing curly brace. Then try to parse what we read as a number.
final int startPos = decodePos;
decodeBuffer.setLength(0);
while (true)
{
final char c = readCharacter(chars, true);
switch (c)
{
case ' ':
case '\t':
case '\n':
case '\r':
case ',':
case ']':
case '}':
// We need to decrement the position indicator since the last one we
// read wasn't part of the number.
decodePos--;
return new JSONNumber(decodeBuffer.toString());
default:
decodeBuffer.append(c);
}
}
}
/**
* Reads a JSON array starting at the specified position in the provided
* character array. Note that this method assumes that the opening square
* bracket has already been read.
*
* @param chars The characters that comprise the string representation of
* the JSON object.
*
* @return The JSON array that was read.
*
* @throws JSONException If a problem was encountered while reading the JSON
* array.
*/
@NotNull()
private JSONArray readArray(@NotNull final char[] chars)
throws JSONException
{
// The opening square bracket will have already been consumed, so read
// JSON values until we hit a closing square bracket.
final ArrayList values = new ArrayList<>(10);
boolean firstToken = true;
while (true)
{
// If this is the first time through, it is acceptable to find a closing
// square bracket. Otherwise, we expect to find a JSON value, an opening
// square bracket to denote the start of an embedded array, or an opening
// curly brace to denote the start of an embedded JSON object.
int p = decodePos;
Object token = readToken(chars);
if (token instanceof JSONValue)
{
values.add((JSONValue) token);
}
else if (token.equals('['))
{
values.add(readArray(chars));
}
else if (token.equals('{'))
{
final LinkedHashMap fieldMap =
new LinkedHashMap<>(StaticUtils.computeMapCapacity(10));
values.add(readObject(chars, fieldMap));
}
else if (token.equals(']') && firstToken)
{
// It's an empty array.
return JSONArray.EMPTY_ARRAY;
}
else
{
throw new JSONException(
ERR_OBJECT_INVALID_TOKEN_WHEN_ARRAY_VALUE_EXPECTED.get(
new String(chars), String.valueOf(token), p));
}
firstToken = false;
// If we've gotten here, then we found a JSON value. It must be followed
// by either a comma (to indicate that there's at least one more value) or
// a closing square bracket (to denote the end of the array).
p = decodePos;
token = readToken(chars);
if (token.equals(']'))
{
return new JSONArray(values);
}
else if (! token.equals(','))
{
throw new JSONException(
ERR_OBJECT_INVALID_TOKEN_WHEN_ARRAY_COMMA_OR_BRACKET_EXPECTED.get(
new String(chars), String.valueOf(token), p));
}
}
}
/**
* Reads a JSON object starting at the specified position in the provided
* character array. Note that this method assumes that the opening curly
* brace has already been read.
*
* @param chars The characters that comprise the string representation of
* the JSON object.
* @param fields The map into which to place the fields that are read. The
* returned object will include an unmodifiable view of this
* map, but the caller may use the map directly if desired.
*
* @return The JSON object that was read.
*
* @throws JSONException If a problem was encountered while reading the JSON
* object.
*/
@NotNull()
private JSONObject readObject(@NotNull final char[] chars,
@NotNull final Map fields)
throws JSONException
{
boolean firstField = true;
while (true)
{
// Read the next token. It must be a JSONString, unless we haven't read
// any fields yet in which case it can be a closing curly brace to
// indicate that it's an empty object.
int p = decodePos;
final String fieldName;
Object token = readToken(chars);
if (token instanceof JSONString)
{
fieldName = ((JSONString) token).stringValue();
if (fields.containsKey(fieldName))
{
throw new JSONException(ERR_OBJECT_DUPLICATE_FIELD.get(
new String(chars), fieldName));
}
}
else if (firstField && token.equals('}'))
{
return new JSONObject(fields);
}
else
{
throw new JSONException(ERR_OBJECT_EXPECTED_STRING.get(
new String(chars), String.valueOf(token), p));
}
firstField = false;
// Read the next token. It must be a colon.
p = decodePos;
token = readToken(chars);
if (! token.equals(':'))
{
throw new JSONException(ERR_OBJECT_EXPECTED_COLON.get(new String(chars),
String.valueOf(token), p));
}
// Read the next token. It must be one of the following:
// - A JSONValue
// - An opening square bracket, designating the start of an array.
// - An opening curly brace, designating the start of an object.
p = decodePos;
token = readToken(chars);
if (token instanceof JSONValue)
{
fields.put(fieldName, (JSONValue) token);
}
else if (token.equals('['))
{
final JSONArray a = readArray(chars);
fields.put(fieldName, a);
}
else if (token.equals('{'))
{
final LinkedHashMap m =
new LinkedHashMap<>(StaticUtils.computeMapCapacity(10));
final JSONObject o = readObject(chars, m);
fields.put(fieldName, o);
}
else
{
throw new JSONException(ERR_OBJECT_EXPECTED_VALUE.get(new String(chars),
String.valueOf(token), p, fieldName));
}
// Read the next token. It must be either a comma (to indicate that
// there will be another field) or a closing curly brace (to indicate
// that the end of the object has been reached).
p = decodePos;
token = readToken(chars);
if (token.equals('}'))
{
return new JSONObject(fields);
}
else if (! token.equals(','))
{
throw new JSONException(ERR_OBJECT_EXPECTED_COMMA_OR_CLOSE_BRACE.get(
new String(chars), String.valueOf(token), p));
}
}
}
/**
* Retrieves a map of the fields contained in this JSON object.
*
* @return A map of the fields contained in this JSON object.
*/
@NotNull()
public Map getFields()
{
return fields;
}
/**
* Retrieves the value for the specified field.
*
* @param name The name of the field for which to retrieve the value. It
* will be treated in a case-sensitive manner.
*
* @return The value for the specified field, or {@code null} if the
* requested field is not present in the JSON object.
*/
@Nullable()
public JSONValue getField(@NotNull final String name)
{
return fields.get(name);
}
/**
* Retrieves the value of the specified field as a string.
*
* @param name The name of the field for which to retrieve the string value.
* It will be treated in a case-sensitive manner.
*
* @return The value of the specified field as a string, or {@code null} if
* this JSON object does not have a field with the specified name, or
* if the value of that field is not a string.
*/
@Nullable()
public String getFieldAsString(@NotNull final String name)
{
final JSONValue value = fields.get(name);
if ((value == null) || (! (value instanceof JSONString)))
{
return null;
}
return ((JSONString) value).stringValue();
}
/**
* Retrieves the value of the specified field as a Boolean.
*
* @param name The name of the field for which to retrieve the Boolean
* value. It will be treated in a case-sensitive manner.
*
* @return The value of the specified field as a Boolean, or {@code null} if
* this JSON object does not have a field with the specified name, or
* if the value of that field is not a Boolean.
*/
@Nullable()
public Boolean getFieldAsBoolean(@NotNull final String name)
{
final JSONValue value = fields.get(name);
if ((value == null) || (! (value instanceof JSONBoolean)))
{
return null;
}
return ((JSONBoolean) value).booleanValue();
}
/**
* Retrieves the value of the specified field as an integer.
*
* @param name The name of the field for which to retrieve the integer
* value. It will be treated in a case-sensitive manner.
*
* @return The value of the specified field as an integer, or {@code null} if
* this JSON object does not have a field with the specified name, or
* if the value of that field is not a number that can be exactly
* represented as an integer.
*/
@Nullable()
public Integer getFieldAsInteger(@NotNull final String name)
{
final JSONValue value = fields.get(name);
if ((value == null) || (! (value instanceof JSONNumber)))
{
return null;
}
try
{
final JSONNumber number = (JSONNumber) value;
return number.getValue().intValueExact();
}
catch (final Exception e)
{
Debug.debugException(e);
return null;
}
}
/**
* Retrieves the value of the specified field as a long.
*
* @param name The name of the field for which to retrieve the long value.
* It will be treated in a case-sensitive manner.
*
* @return The value of the specified field as a long, or {@code null} if
* this JSON object does not have a field with the specified name, or
* if the value of that field is not a number that can be exactly
* represented as a long.
*/
@Nullable()
public Long getFieldAsLong(@NotNull final String name)
{
final JSONValue value = fields.get(name);
if ((value == null) || (! (value instanceof JSONNumber)))
{
return null;
}
try
{
final JSONNumber number = (JSONNumber) value;
return number.getValue().longValueExact();
}
catch (final Exception e)
{
Debug.debugException(e);
return null;
}
}
/**
* Retrieves the value of the specified field as a BigDecimal.
*
* @param name The name of the field for which to retrieve the BigDecimal
* value. It will be treated in a case-sensitive manner.
*
* @return The value of the specified field as a BigDecimal, or {@code null}
* if this JSON object does not have a field with the specified name,
* or if the value of that field is not a number.
*/
@Nullable()
public BigDecimal getFieldAsBigDecimal(@NotNull final String name)
{
final JSONValue value = fields.get(name);
if ((value == null) || (! (value instanceof JSONNumber)))
{
return null;
}
return ((JSONNumber) value).getValue();
}
/**
* Retrieves the value of the specified field as a JSON object.
*
* @param name The name of the field for which to retrieve the value. It
* will be treated in a case-sensitive manner.
*
* @return The value of the specified field as a JSON object, or {@code null}
* if this JSON object does not have a field with the specified name,
* or if the value of that field is not an object.
*/
@Nullable()
public JSONObject getFieldAsObject(@NotNull final String name)
{
final JSONValue value = fields.get(name);
if ((value == null) || (! (value instanceof JSONObject)))
{
return null;
}
return (JSONObject) value;
}
/**
* Retrieves a list of the elements in the specified array field.
*
* @param name The name of the field for which to retrieve the array values.
* It will be treated in a case-sensitive manner.
*
* @return A list of the elements in the specified array field, or
* {@code null} if this JSON object does not have a field with the
* specified name, or if the value of that field is not an array.
*/
@Nullable()
public List getFieldAsArray(@NotNull final String name)
{
final JSONValue value = fields.get(name);
if ((value == null) || (! (value instanceof JSONArray)))
{
return null;
}
return ((JSONArray) value).getValues();
}
/**
* Indicates whether this JSON object has a null field with the specified
* name.
*
* @param name The name of the field for which to make the determination.
* It will be treated in a case-sensitive manner.
*
* @return {@code true} if this JSON object has a null field with the
* specified name, or {@code false} if this JSON object does not have
* a field with the specified name, or if the value of that field is
* not a null.
*/
public boolean hasNullField(@NotNull final String name)
{
final JSONValue value = fields.get(name);
return ((value != null) && (value instanceof JSONNull));
}
/**
* Indicates whether this JSON object has a field with the specified name.
*
* @param fieldName The name of the field for which to make the
* determination. It will be treated in a case-sensitive
* manner.
*
* @return {@code true} if this JSON object has a field with the specified
* name, or {@code false} if not.
*/
public boolean hasField(@NotNull final String fieldName)
{
return fields.containsKey(fieldName);
}
/**
* {@inheritDoc}
*/
@Override()
public int hashCode()
{
if (hashCode == null)
{
int hc = 0;
for (final Map.Entry e : fields.entrySet())
{
hc += e.getKey().hashCode() + e.getValue().hashCode();
}
hashCode = hc;
}
return hashCode;
}
/**
* {@inheritDoc}
*/
@Override()
public boolean equals(@Nullable final Object o)
{
if (o == this)
{
return true;
}
if (o instanceof JSONObject)
{
final JSONObject obj = (JSONObject) o;
return fields.equals(obj.fields);
}
return false;
}
/**
* Indicates whether this JSON object is considered equal to the provided
* object, subject to the specified constraints.
*
* @param o The object to compare against this JSON
* object. It must not be {@code null}.
* @param ignoreFieldNameCase Indicates whether to ignore differences in
* capitalization in field names.
* @param ignoreValueCase Indicates whether to ignore differences in
* capitalization in values that are JSON
* strings.
* @param ignoreArrayOrder Indicates whether to ignore differences in the
* order of elements within an array.
*
* @return {@code true} if this JSON object is considered equal to the
* provided object (subject to the specified constraints), or
* {@code false} if not.
*/
public boolean equals(@NotNull final JSONObject o,
final boolean ignoreFieldNameCase,
final boolean ignoreValueCase,
final boolean ignoreArrayOrder)
{
// See if we can do a straight-up Map.equals. If so, just do that.
if ((! ignoreFieldNameCase) && (! ignoreValueCase) && (! ignoreArrayOrder))
{
return fields.equals(o.fields);
}
// Make sure they have the same number of fields.
if (fields.size() != o.fields.size())
{
return false;
}
// Optimize for the case in which we field names are case sensitive.
if (! ignoreFieldNameCase)
{
for (final Map.Entry e : fields.entrySet())
{
final JSONValue thisValue = e.getValue();
final JSONValue thatValue = o.fields.get(e.getKey());
if (thatValue == null)
{
return false;
}
if (! thisValue.equals(thatValue, ignoreFieldNameCase, ignoreValueCase,
ignoreArrayOrder))
{
return false;
}
}
return true;
}
// If we've gotten here, then we know that we need to treat field names in
// a case-insensitive manner. Create a new map that we can remove fields
// from as we find matches. This can help avoid false-positive matches in
// which multiple fields in the first map match the same field in the second
// map (e.g., because they have field names that differ only in case and
// values that are logically equivalent). It also makes iterating through
// the values faster as we make more progress.
final HashMap thatMap = new HashMap<>(o.fields);
final Iterator> thisIterator =
fields.entrySet().iterator();
while (thisIterator.hasNext())
{
final Map.Entry thisEntry = thisIterator.next();
final String thisFieldName = thisEntry.getKey();
final JSONValue thisValue = thisEntry.getValue();
final Iterator> thatIterator =
thatMap.entrySet().iterator();
boolean found = false;
while (thatIterator.hasNext())
{
final Map.Entry thatEntry = thatIterator.next();
final String thatFieldName = thatEntry.getKey();
if (! thisFieldName.equalsIgnoreCase(thatFieldName))
{
continue;
}
final JSONValue thatValue = thatEntry.getValue();
if (thisValue.equals(thatValue, ignoreFieldNameCase, ignoreValueCase,
ignoreArrayOrder))
{
found = true;
thatIterator.remove();
break;
}
}
if (! found)
{
return false;
}
}
return true;
}
/**
* {@inheritDoc}
*/
@Override()
public boolean equals(@NotNull final JSONValue v,
final boolean ignoreFieldNameCase,
final boolean ignoreValueCase,
final boolean ignoreArrayOrder)
{
return ((v instanceof JSONObject) &&
equals((JSONObject) v, ignoreFieldNameCase, ignoreValueCase,
ignoreArrayOrder));
}
/**
* Retrieves a string representation of this JSON object. If this object was
* decoded from a string, then the original string representation will be
* used. Otherwise, a single-line string representation will be constructed.
*
* @return A string representation of this JSON object.
*/
@Override()
@NotNull()
public String toString()
{
if (stringRepresentation == null)
{
final StringBuilder buffer = new StringBuilder();
toString(buffer);
stringRepresentation = buffer.toString();
}
return stringRepresentation;
}
/**
* Appends a string representation of this JSON object to the provided buffer.
* If this object was decoded from a string, then the original string
* representation will be used. Otherwise, a single-line string
* representation will be constructed.
*
* @param buffer The buffer to which the information should be appended.
*/
@Override()
public void toString(@NotNull final StringBuilder buffer)
{
if (stringRepresentation != null)
{
buffer.append(stringRepresentation);
return;
}
buffer.append("{ ");
final Iterator> iterator =
fields.entrySet().iterator();
while (iterator.hasNext())
{
final Map.Entry e = iterator.next();
JSONString.encodeString(e.getKey(), buffer);
buffer.append(':');
e.getValue().toString(buffer);
if (iterator.hasNext())
{
buffer.append(',');
}
buffer.append(' ');
}
buffer.append('}');
}
/**
* Retrieves a user-friendly string representation of this JSON object that
* may be formatted across multiple lines for better readability. The last
* line will not include a trailing line break.
*
* @return A user-friendly string representation of this JSON object that may
* be formatted across multiple lines for better readability.
*/
@NotNull()
public String toMultiLineString()
{
final JSONBuffer jsonBuffer = new JSONBuffer(null, 0, true);
appendToJSONBuffer(jsonBuffer);
return jsonBuffer.toString();
}
/**
* Retrieves a single-line string representation of this JSON object.
*
* @return A single-line string representation of this JSON object.
*/
@Override()
@NotNull
public String toSingleLineString()
{
final StringBuilder buffer = new StringBuilder();
toSingleLineString(buffer);
return buffer.toString();
}
/**
* Appends a single-line string representation of this JSON object to the
* provided buffer.
*
* @param buffer The buffer to which the information should be appended.
*/
@Override()
public void toSingleLineString(@NotNull final StringBuilder buffer)
{
buffer.append("{ ");
final Iterator> iterator =
fields.entrySet().iterator();
while (iterator.hasNext())
{
final Map.Entry e = iterator.next();
JSONString.encodeString(e.getKey(), buffer);
buffer.append(':');
e.getValue().toSingleLineString(buffer);
if (iterator.hasNext())
{
buffer.append(',');
}
buffer.append(' ');
}
buffer.append('}');
}
/**
* Retrieves a normalized string representation of this JSON object. The
* normalized representation of the JSON object will have the following
* characteristics:
*
* - It will not include any line breaks.
* - It will not include any spaces around the enclosing braces.
* - It will not include any spaces around the commas used to separate
* fields.
* - Field names will be treated in a case-sensitive manner and will not
* be altered.
* - Field values will be normalized.
* - Fields will be listed in lexicographic order by field name.
*
*
* @return A normalized string representation of this JSON object.
*/
@Override()
@NotNull()
public String toNormalizedString()
{
final StringBuilder buffer = new StringBuilder();
toNormalizedString(buffer);
return buffer.toString();
}
/**
* Appends a normalized string representation of this JSON object to the
* provided buffer. The normalized representation of the JSON object will
* have the following characteristics:
*
* - It will not include any line breaks.
* - It will not include any spaces around the enclosing braces.
* - It will not include any spaces around the commas used to separate
* fields.
* - Field names will be treated in a case-sensitive manner and will not
* be altered.
* - Field values will be normalized.
* - Fields will be listed in lexicographic order by field name.
*
*
* @param buffer The buffer to which the information should be appended.
*/
@Override()
public void toNormalizedString(@NotNull final StringBuilder buffer)
{
toNormalizedString(buffer, false, true, false);
}
/**
* Retrieves a normalized string representation of this JSON object. The
* normalized representation of the JSON object will have the following
* characteristics:
*
* - It will not include any line breaks.
* - It will not include any spaces around the enclosing braces.
* - It will not include any spaces around the commas used to separate
* fields.
* - Case sensitivity of field names and values will be controlled by
* argument values.
*
- Fields will be listed in lexicographic order by field name.
*
*
* @param ignoreFieldNameCase Indicates whether field names should be
* treated in a case-sensitive (if {@code false})
* or case-insensitive (if {@code true}) manner.
* @param ignoreValueCase Indicates whether string field values should
* be treated in a case-sensitive (if
* {@code false}) or case-insensitive (if
* {@code true}) manner.
* @param ignoreArrayOrder Indicates whether the order of elements in an
* array should be considered significant (if
* {@code false}) or insignificant (if
* {@code true}).
*
* @return A normalized string representation of this JSON object.
*/
@Override()
@NotNull()
public String toNormalizedString(final boolean ignoreFieldNameCase,
final boolean ignoreValueCase,
final boolean ignoreArrayOrder)
{
final StringBuilder buffer = new StringBuilder();
toNormalizedString(buffer, ignoreFieldNameCase, ignoreValueCase,
ignoreArrayOrder);
return buffer.toString();
}
/**
* Appends a normalized string representation of this JSON object to the
* provided buffer. The normalized representation of the JSON object will
* have the following characteristics:
*
* - It will not include any line breaks.
* - It will not include any spaces around the enclosing braces.
* - It will not include any spaces around the commas used to separate
* fields.
* - Field names will be treated in a case-sensitive manner and will not
* be altered.
* - Field values will be normalized.
* - Fields will be listed in lexicographic order by field name.
*
*
* @param buffer The buffer to which the information should be
* appended.
* @param ignoreFieldNameCase Indicates whether field names should be
* treated in a case-sensitive (if {@code false})
* or case-insensitive (if {@code true}) manner.
* @param ignoreValueCase Indicates whether string field values should
* be treated in a case-sensitive (if
* {@code false}) or case-insensitive (if
* {@code true}) manner.
* @param ignoreArrayOrder Indicates whether the order of elements in an
* array should be considered significant (if
* {@code false}) or insignificant (if
* {@code true}).
*/
@Override()
public void toNormalizedString(@NotNull final StringBuilder buffer,
final boolean ignoreFieldNameCase,
final boolean ignoreValueCase,
final boolean ignoreArrayOrder)
{
// The normalized representation needs to have the fields in a predictable
// order, which we will accomplish using the lexicographic ordering that a
// TreeMap will provide. Field names may or may not be treated in a
// case-sensitive manner, but we still need to construct a normalized way of
// escaping non-printable characters in each field.
final TreeMap m = new TreeMap<>();
for (final Map.Entry e : fields.entrySet())
{
m.put(
new JSONString(e.getKey()).toNormalizedString(false,
ignoreFieldNameCase, false),
e.getValue().toNormalizedString(ignoreFieldNameCase, ignoreValueCase,
ignoreArrayOrder));
}
buffer.append('{');
final Iterator> iterator = m.entrySet().iterator();
while (iterator.hasNext())
{
final Map.Entry e = iterator.next();
buffer.append(e.getKey());
buffer.append(':');
buffer.append(e.getValue());
if (iterator.hasNext())
{
buffer.append(',');
}
}
buffer.append('}');
}
/**
* {@inheritDoc}
*/
@Override()
public void appendToJSONBuffer(@NotNull final JSONBuffer buffer)
{
buffer.beginObject();
for (final Map.Entry field : fields.entrySet())
{
final String name = field.getKey();
final JSONValue value = field.getValue();
value.appendToJSONBuffer(name, buffer);
}
buffer.endObject();
}
/**
* {@inheritDoc}
*/
@Override()
public void appendToJSONBuffer(@NotNull final String fieldName,
@NotNull final JSONBuffer buffer)
{
buffer.beginObject(fieldName);
for (final Map.Entry field : fields.entrySet())
{
final String name = field.getKey();
final JSONValue value = field.getValue();
value.appendToJSONBuffer(name, buffer);
}
buffer.endObject();
}
}