com.globalmentor.javascript.JSON Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of globalmentor-javascript Show documentation
Show all versions of globalmentor-javascript Show documentation
GlobalMentor Java JavaScript library.
/*
* Copyright © 1996-2011 GlobalMentor, Inc.
*
* 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.
*/
package com.globalmentor.javascript;
import static java.lang.reflect.Array.*;
import java.io.IOException;
import java.util.*;
import java.util.regex.Pattern;
import static java.util.Objects.*;
import static com.globalmentor.java.Arrays.*;
import static com.globalmentor.java.CharSequences.*;
import static com.globalmentor.java.Conditions.*;
import static com.globalmentor.java.StringBuilders.*;
import com.globalmentor.model.ObjectHolder;
import com.globalmentor.net.ContentType;
import com.globalmentor.text.ArgumentSyntaxException;
import com.globalmentor.text.W3CDateFormat;
/**
* Utilities for encoding and decoding JavaScript Object Notation (JSON). In addition to standard JSON, any {@link Date} object will be formatted as a string
* value according to the W3C Note, "Date and Time Formats", http://www.w3.org/TR/NOTE-datetime, a profile of
* ISO 8601.
* @author Garret Wilson
* @see RFC 4627: The application/json Media Type for JavaScript Object Notation (JSON)
* @see Introducing JSON
* @see Date and Time Formats
* @see W3CDateFormat.Style#DATE_TIME
*/
public class JSON {
/** The content type for JSON: application/json
. */
public static final ContentType CONTENT_TYPE = ContentType.create(ContentType.APPLICATION_PRIMARY_TYPE, "json");
public static final char BEGIN_ARRAY = 0x5B; //[ left square bracket
public static final char BEGIN_OBJECT = 0x7B; //[ left square bracket
public static final char END_ARRAY = 0x5D; //] right square bracket
public static final char END_OBJECT = 0x7D; //} right curly bracket
public static final char NAME_SEPARATOR = 0x3A; //: colon
public static final char VALUE_SEPARATOR = 0x2C; //, comma
public static final char ESCAPE = 0x5C; //\
public static final char QUOTATION_MARK = 0x22; //"
public static final char MINUS = 0x2D; //-
public static final char PLUS = 0x2B; //+
public static final char DECIMAL_POINT = 0x2E; //.
//whitespace characters
public static final char SPACE = 0x20; //Space
public static final char HORIZONTAL_TAB = 0x09; //Horizontal tab
public static final char LINE_FEED = 0x0A; //Line feed or New line
public static final char CARRIAGE_RETURN = 0x0D; //Carriage return
//escaped forms of characters
public static final char ESCAPED_QUOTATION_MARK = 0x22; //" quotation mark U+0022
public static final char ESCAPED_REVERSE_SOLIDUS = 0x5C; //\ reverse solidus U+005C
public static final char REVERSE_SOLIDUS = 0x5C; //\ reverse solidus U+005C
public static final char ESCAPED_SOLIDUS = 0x2F; /// solidus U+002F
public static final char SOLIDUS = 0x2F; /// solidus U+002F
public static final char ESCAPED_BACKSPACE = 0x62; //b backspace U+0008
public static final char BACKSPACE = 0x08; //b backspace U+0008
public static final char ESCAPED_FORM_FEED = 0x66; //f form feed U+000C
public static final char FORM_FEED = 0x0C; //f form feed U+000C
public static final char ESCAPED_LINE_FEED = 0x6E; //n line feed U+000A
public static final char ESCAPED_CARRIAGE_RETURN = 0x72; //r carriage return U+000D
public static final char ESCAPED_TAB = 0x74; //t tab U+0009
/** The character introducing an escaped Unicode code point. */
public static final char ESCAPED_UNICODE = 'u';
/** Sign characters. */
public static final char[] SIGN = new char[] { MINUS, PLUS };
/** Digit characters. */
public static final char[] DIGITS = new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' };
/** Whitespace characters. */
public static final char[] WHITESPACE = new char[] { SPACE, HORIZONTAL_TAB, LINE_FEED, CARRIAGE_RETURN };
/** The null identifier. */
public static final String NULL = "null";
/** The boolean true identifier. */
public static final String TRUE = "true";
/** The boolean false identifier. */
public static final String FALSE = "false";
//TODO fix (?
* {@link CharSequence} (string)
* {@link Boolean} (boolean)
* {@link Number} (number)
* {@link List} (array)
* {@link Map} (associative array)
* [] (array)
* {@link Date} (W3C date/time)
* {@link Object} (string)
* null
*
* @param The type of the appendable.
* @param appendable The appendable object to accept the string.
* @param value The object value to be appended, or null
.
* @throws IOException if there is an error appending the information.
* @return The appendable object.
*/
public static A appendValue(final A appendable, final Object value) throws IOException {
if(value != null) { //if the value is not null
if(value instanceof CharSequence) { //string
appendStringValue(appendable, (CharSequence)value);
} else if(value instanceof Boolean) { //boolean
appendBooleanValue(appendable, (Boolean)value);
} else if(value instanceof Number) { //number
appendNumberValue(appendable, (Number)value);
} else if(value instanceof List) { //if the value is a list
appendArrayValue(appendable, ((List>)value).toArray()); //append the list as an array
} else if(value instanceof Map) { //if the value is a map
appendAssociativeArrayValue(appendable, (Map, ?>)value); //append the map as an associative array
} else if(value.getClass().isArray()) { //if the value is an array (we can't use instanceof Object[], because this may be an array of something besides Object)
appendArrayValue(appendable, value); //append the array
} else if(value instanceof Date) { //date
appendStringValue(appendable, W3CDateFormat.format((Date)value, W3CDateFormat.Style.DATE_HOURS_MINUTES_SECONDS));
} else { //if we can't determine the type of object
appendStringValue(appendable, value.toString()); //append a string form of the value
}
} else { //if the value is null
appendable.append(NULL); //append null
}
return appendable; //return the appendable object
}
/**
* Appends a string value in the form "string"
. The character sequence will first be encoded as necessary.
* @param The type of the appendable.
* @param appendable The appendable object to accept the string.
* @param charSequence The string characters to be appended.
* @return The appendable object
* @throws NullPointerException if the given character sequence is null
.
* @throws IOException if there is an error appending the information.
* @see #encodeStringValue(CharSequence)
*/
@SuppressWarnings("unchecked")
public static A appendStringValue(final A appendable, final CharSequence charSequence) throws IOException {
return (A)appendable.append(QUOTATION_MARK).append(encodeStringValue(charSequence)).append(QUOTATION_MARK); //append and return "encodedString"
}
/**
* Appends a boolean value.
* @param The type of the appendable.
* @param appendable The appendable object to accept the string.
* @param bool The boolean value to append.
* @return The appendable object.
* @throws NullPointerException if the given boolean value is null
.
* @throws IOException if there is an error appending the information.
*/
@SuppressWarnings("unchecked")
public static A appendBooleanValue(final A appendable, final Boolean bool) throws IOException {
return (A)appendable.append(requireNonNull(bool, "Boolean value cannot be null.").toString()); //append and return boolean
}
/**
* Appends a number value.
* @param The type of the appendable.
* @param appendable The appendable object to accept the string.
* @param number The number to append.
* @return The appendable object.
* @throws NullPointerException if the given number is null
.
* @throws IOException if there is an error appending the information.
*/
@SuppressWarnings("unchecked")
public static A appendNumberValue(final A appendable, final Number number) throws IOException {
return (A)appendable.append(requireNonNull(number, "Number value cannot be null.").toString()); //append and return number
}
/**
* Appends an array value.
* @param The type of the appendable.
* @param appendable The appendable object to accept the string.
* @param array The array to append.
* @return The appendable object.
* @throws NullPointerException if the given array is null
.
* @throws IllegalArgumentException if the given object is not an array.
* @throws IOException if there is an error appending the information.
*/
public static A appendArrayValue(final A appendable, final Object array) throws IOException {
appendable.append(BEGIN_ARRAY); //[
final int arrayLength = getLength(array); //see how long the array is
for(int i = 0; i < arrayLength; ++i) { //for each array element
appendValue(appendable, get(array, i)); //append this element value
if(i < arrayLength - 1) { //if we aren't at the end
appendable.append(VALUE_SEPARATOR); //,
} else {
appendable.append(END_ARRAY); //]
}
}
return appendable; //return the appendable object
}
/**
* Appends an object (associative array) value in the form {"key":value,...}
. The provided map must not have any
* null
keys.
* @param The type of the appendable.
* @param The type of keys stored in the map.
* @param The type of values stored in the map.
* @param appendable The appendable object to accept the string.
* @param map The map containing the associative array values.
* @return The appendable object.
* @throws NullPointerException if one of the keys of the given map is is null
.
* @throws IOException if there is an error appending the information.
* @see #appendValue(Appendable, Object)
*/
public static A appendAssociativeArrayValue(final A appendable, final Map map) throws IOException {
appendable.append(BEGIN_OBJECT); //{
final Set> mapEntrySet = map.entrySet(); //get the set of map entries
boolean hasMoreElements = !mapEntrySet.isEmpty();
if(!hasMoreElements) { //if the map is empty
appendable.append(END_OBJECT); //}
} else { //if the map isn't empty
final Iterator> mapEntryIterator = mapEntrySet.iterator();
do {
final Map.Entry mapEntry = mapEntryIterator.next();
appendStringValue(appendable, mapEntry.getKey().toString()); //key
appendable.append(NAME_SEPARATOR); //:
appendValue(appendable, mapEntry.getValue()); //value
if(hasMoreElements = mapEntryIterator.hasNext()) { //see if there are more elements
appendable.append(VALUE_SEPARATOR); //,
} else {
appendable.append(END_OBJECT); //}
}
} while(hasMoreElements);
}
return appendable; //return the appendable object
}
/**
* Encodes a string value. The characters {@link JavaScript#STRING_ENCODE_CHARS} will be replaced with {@link JavaScript#STRING_ENCODE_REPLACEMENT_STRINGS},
* respectively.
* @param charSequence The characters to encode.
* @return A string containing encoded characters.
* @throws NullPointerException if the given character sequence is null
.
*/
public static String encodeStringValue(final CharSequence charSequence) {
final StringBuilder stringBuilder = new StringBuilder(requireNonNull(charSequence, "Character sequence cannot be null.")); //create a new string builder with the contents of the character sequence
replace(stringBuilder, JavaScript.STRING_ENCODE_CHARS, JavaScript.STRING_ENCODE_REPLACEMENT_STRINGS); //replace the encode characters with their encoded replacements
return stringBuilder.toString(); //return the encoded string
}
/**
* Decodes a string value. Every instance of {@value JavaScript#ESCAPE_CHAR} will be removed if followed by another character and the subsequent character will be
* ignored.
* @param charSequence The characters to encode.
* @return A string containing encoded characters.
* @throws NullPointerException if the given character sequence is null
.
* @throws IllegalArgumentException if the character sequence ends with the given escape character.
*/
public static String decodeStringValue(final CharSequence charSequence) {
return unescape(new StringBuilder(requireNonNull(charSequence, "Character sequence cannot be null.")), ESCAPE).toString(); //unescape the string
}
/**
* Serializes the given object in JSON.
* @param object The object to serialize.
* @return A string serialization of the given object.
*/
public static String serialize(final Object object) {
try {
return appendValue(new StringBuilder(), object).toString(); //serialize the given object and return the resulting string
} catch(final IOException ioException) {
throw impossible(ioException); //string builders never throw I/O exceptions
}
}
/**
* Checks that the current character matches a specific character and advances to the next character.
* @param charSequence The character sequence to be parsed.
* @param index The current parse index in the character sequence.
* @param c The character against which the current character should be checked.
* @return The new index at which to continue parsing.
* @throws NullPointerException if the given character sequence is null
.
* @throws ArrayIndexOutOfBoundsException if the character sequence has insufficient characters at the given index.
* @throws ArgumentSyntaxException if the current character in the sequence does not match the specified character.
*/
protected static int check(final CharSequence charSequence, final int index, final char c) throws ArgumentSyntaxException {
if(charSequence.charAt(index) != c) { //if this character does not match what we expected
throw new ArgumentSyntaxException("Expected " + (char)c + ".", charSequence.toString(), index);
}
return index + 1; //return the subsequent index
}
/**
* Checks that the current character matches a character in a range and advances to the next character.
* @param charSequence The character sequence to be parsed.
* @param index The current parse index in the character sequence.
* @param lowerBound The lowest character in the range.
* @param upperBound The highest character in the range.
* @return The new index at which to continue parsing.
* @throws NullPointerException if the given character sequence is null
.
* @throws ArrayIndexOutOfBoundsException if the character sequence has insufficient characters at the given index.
* @throws ArgumentSyntaxException if the current character in the sequence does not fall within the given range.
*/
protected static int check(final CharSequence charSequence, int index, final char lowerBound, final char upperBound) {
final char c = charSequence.charAt(index); //get the current character
if(c < lowerBound || c > upperBound) { //if this character is not in the range
throw new ArgumentSyntaxException("Expected character from " + (char)lowerBound + " to " + (char)upperBound + ".", charSequence.toString(), index);
}
return index + 1; //return the subsequent index
}
/**
* Checks that the current characters matches a given set of characters and advances to the next character.
* @param charSequence The character sequence to be parsed.
* @param index The current parse index in the character sequence.
* @param characters The characters to accept.
* @return The new index at which to continue parsing.
* @throws NullPointerException if the given character sequence and/or the given characters is null
.
* @throws ArrayIndexOutOfBoundsException if the character sequence has insufficient characters at the given index.
* @throws ArgumentSyntaxException if the current character in the sequence does not match one of the specified characters.
*/
protected static int check(final CharSequence charSequence, int index, final char[] characters) {
if(indexOf(characters, charSequence.charAt(index)) < 0) { //if this character does not match one of the expected characters
throw new ArgumentSyntaxException("Expected one of " + java.util.Arrays.toString(characters) + ".", charSequence.toString(), index);
}
return index + 1; //return the subsequent index
}
/**
* Checks that the current and subsequent characters matches a specified character sequence.
* @param charSequence The character sequence to be parsed.
* @param index The current parse index in the character sequence.
* @param match The character sequence with which the current characters should be checked.
* @return The new index at which to continue parsing.
* @throws NullPointerException if the given character sequence and/or match character sequence is null
.
* @throws ArrayIndexOutOfBoundsException if the character sequence has insufficient characters at the given index.
* @throws ArgumentSyntaxException if the current character in the sequence does not match the specified character sequence.
*/
protected static int check(final CharSequence charSequence, int index, final CharSequence match) throws ArgumentSyntaxException {
final int matchLength = match.length(); //get the length to match
for(int i = 0; i < matchLength; ++i) { //for each match index
index = check(charSequence, index, match.charAt(i)); //compare the current character with the match character
}
return index; //return the index, which is already at the subsequent character
}
/**
* Skips over characters in a character sequence that appear within a given array.
* @param charSequence The character sequence to be parsed.
* @param index The current parse index in the character sequence.
* @param characters The characters to skip.
* @return The new index at which to continue parsing; either the first character not in the array, or the length of the character sequence.
* @throws NullPointerException if the given character sequence and/or the given characters is null
.
*/
protected static int skip(final CharSequence charSequence, int index, final char[] characters) {
char lowerBound = Character.MAX_VALUE; //we'll determine the lower bound of the range
char upperBound = 0; //we'll determine the lower bound of the range
for(int i = characters.length - 1; i >= 0; --i) { //look at each characters to skip
final char c = characters[i]; //get this character
if(c < lowerBound) { //if this is a lower character than the one we already have for the lower bound
lowerBound = c; //update the lower bound
}
if(c > upperBound) { //if this is a higher character than the one we already have for the upper bound
upperBound = c; //update the upper bound
}
}
final int length = charSequence.length(); //get the length of the character sequence
for(; index < length; ++index) { //keep looking until we run out of characters
final char c = charSequence.charAt(index); //get the current character
if(c < lowerBound || c > upperBound) { //if this character is not in the range of the characters
break; //stop searching
} else { //if the character is within the range of characters, make sure it's one of the characters
boolean skip = false; //we'll see if there's a match
for(int i = characters.length - 1; i >= 0 && !skip; --i) { //look at each characters to skip
if(c == characters[i]) { //if we found a character to skip
skip = true; //indicate that we should skip this character
}
}
if(!skip) { //if we shouldn't skip this characters
break; //stop advancing
}
}
}
return index; //return the next index
}
/**
* Skips over characters in a character sequence that lie within a given range.
* @param charSequence The character sequence to be parsed.
* @param index The current parse index in the character sequence.
* @param lowerBound The lowest character in the range.
* @param upperBound The highest character in the range.
* @return The new index at which to continue parsing; either the first character not in the range, or the length of the character sequence.
* @throws NullPointerException if the given character sequence is null
.
*/
protected static int skip(final CharSequence charSequence, int index, final char lowerBound, final char upperBound) {
final int length = charSequence.length(); //get the length of the character sequence
for(; index < length; ++index) { //keep looking until we run out of characters
final char c = charSequence.charAt(index); //get the current character
if(c < lowerBound || c > upperBound) { //if this character is not in the range
break; //stop searching
}
}
return index; //return the next index
}
/**
* Skips over JSON whitespace characters in a character sequence.
* @param charSequence The character sequence to be parsed.
* @param index The current parse index in the character sequence.
* @return The new index at which to continue parsing; either the first non-whitespace character, or the length of the character sequence.
* @throws NullPointerException if the given character sequence is null
.
*/
protected static int skipWhitespace(final CharSequence charSequence, int index) {
final int length = charSequence.length(); //get the length of the character sequence
for(; index < length; ++index) { //keep looking until we run out of characters
switch(charSequence.charAt(index)) {
case SPACE: //whitespace
case HORIZONTAL_TAB:
case LINE_FEED:
case CARRIAGE_RETURN:
continue; //skip whitespace
default:
return index; //stop advancing and return the index
}
}
return index; //return the index of the non-whitespace character
}
/**
* Parses a value encoded in a JSON character sequence.
* @param charSequence The character sequence to be parsed.
* @return A new {@link String}, {@link Boolean}, {@link Number}, {@link List}, {@link Map}, or null
representing the value represented by the
* character sequence.
* @throws NullPointerException if the given character sequence is null
.
* @throws ArgumentSyntaxException if the given character sequence does not represent a valid JSON object.
*/
public static Object parseValue(final CharSequence charSequence) throws ArgumentSyntaxException {
final ObjectHolder