
org.eclipse.jetty.util.ajax.JSON Maven / Gradle / Ivy
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.util.ajax;
import java.io.Externalizable;
import java.io.IOException;
import java.io.Reader;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.stream.IntStream;
import org.eclipse.jetty.util.Loader;
import org.eclipse.jetty.util.TypeUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* JSON parser and generator.
* This class provides methods to convert POJOs to and from JSON notation.
* The mapping from JSON to Java is:
*
*
* object --> Map<String, Object>
* array --> Object[]
* number --> Double or Long
* string --> String
* null --> null
* bool --> Boolean
*
*
* The Java to JSON mapping is:
*
*
* String --> string
* Number --> number
* Map --> object
* List --> array
* Array --> array
* null --> null
* Boolean--> boolean
* Object --> string (dubious!)
*
*
* The interface {@link JSON.Convertible} may be implemented by classes that
* wish to externalize and initialize specific fields to and from JSON objects.
* Only directed acyclic graphs of objects are supported.
* The interface {@link JSON.Generator} may be implemented by classes that
* know how to render themselves as JSON and the {@link #toJSON(Object)} method
* will use {@link JSON.Generator#addJSON(Appendable)} to generate the JSON.
* The class {@link JSON.Literal} may be used to hold pre-generated JSON object.
* The interface {@link JSON.Convertor} may be implemented to provide
* converters for objects that may be registered with
* {@link #addConvertor(Class, Convertor)}.
* These converters are looked up by class, interface and super class by
* {@link #getConvertor(Class)}.
* If a JSON object has a {@code class} field, then a Java class for that
* name is loaded and the method {@link #convertTo(Class, Map)} is used to find
* a {@link JSON.Convertor} for that class.
* If a JSON object has a {@code x-class} field then a direct lookup for a
* {@link JSON.Convertor} for that class name is done (without loading the class).
*/
public class JSON
{
static final Logger LOG = LoggerFactory.getLogger(JSON.class);
private final Map _convertors = new ConcurrentHashMap<>();
private int _stringBufferSize = 1024;
private Function, Object> _arrayConverter = this::defaultArrayConverter;
/**
* @return the initial stringBuffer size to use when creating JSON strings
* (default 1024)
*/
public int getStringBufferSize()
{
return _stringBufferSize;
}
/**
* @param stringBufferSize the initial stringBuffer size to use when creating JSON
* strings (default 1024)
*/
public void setStringBufferSize(int stringBufferSize)
{
_stringBufferSize = stringBufferSize;
}
private void quotedEscape(Appendable buffer, String input)
{
try
{
buffer.append('"');
if (input != null && !input.isEmpty())
escapeString(buffer, input);
buffer.append('"');
}
catch (IOException e)
{
throw new RuntimeException(e);
}
}
/**
* Escapes the characters of the given {@code input} string into the given buffer.
*
* @param buffer the buffer to escape the string into
* @param input the string to escape
* @throws IOException if appending to the buffer fails
* @see #escapeUnicode(Appendable, char)
*/
public void escapeString(Appendable buffer, String input) throws IOException
{
// Default escaping algorithm.
for (int i = 0; i < input.length(); ++i)
{
char c = input.charAt(i);
// ASCII printable range
if ((c >= 0x20) && (c <= 0x7E))
{
// Special cases for quotation-mark, reverse-solidus, and solidus.
if ((c == '"') || (c == '\\')
/* solidus is optional - per Carsten Bormann (IETF)
|| (c == '/') */)
{
buffer.append('\\').append(c);
}
else
{
// ASCII printable (that isn't escaped above).
buffer.append(c);
}
}
else
{
// All other characters are escaped (in some way).
// First we deal with the special short-form escaping.
if (c == '\b') // backspace
buffer.append("\\b");
else if (c == '\f') // form-feed
buffer.append("\\f");
else if (c == '\n') // line feed
buffer.append("\\n");
else if (c == '\r') // carriage return
buffer.append("\\r");
else if (c == '\t') // tab
buffer.append("\\t");
else if (c < 0x20 || c == 0x7F) // all control characters
{
// Default behavior is to encode.
buffer.append(String.format("\\u%04x", (int)c));
}
else
{
// Optional behavior in JSON spec.
escapeUnicode(buffer, c);
}
}
}
}
/**
* Per JSON specification, unicode characters are by default NOT escaped.
* Overriding this method allows for alternate behavior to escape those
* with your choice of encoding.
*
*
* protected void escapeUnicode(Appendable buffer, char c) throws IOException
* {
* // Unicode is backslash-u escaped
* buffer.append(String.format("\\u%04x", (int)c));
* }
*
*/
protected void escapeUnicode(Appendable buffer, char c) throws IOException
{
buffer.append(c);
}
/**
* Converts any object to JSON.
*
* @param object the object to convert
* @return the JSON string representation of the object
* @see #append(Appendable, Object)
*/
public String toJSON(Object object)
{
StringBuilder buffer = new StringBuilder(getStringBufferSize());
append(buffer, object);
return buffer.toString();
}
/**
* Appends the given object as JSON to string buffer.
* This method tests the given object type and calls other
* appends methods for each object type, see for example
* {@link #appendMap(Appendable, Map)}.
*
* @param buffer the buffer to append to
* @param object the object to convert to JSON
*/
public void append(Appendable buffer, Object object)
{
try
{
if (object == null)
{
buffer.append("null");
}
// Most likely first
else if (object instanceof Map)
{
appendMap(buffer, (Map, ?>)object);
}
else if (object instanceof String)
{
appendString(buffer, (String)object);
}
else if (object instanceof Number)
{
appendNumber(buffer, (Number)object);
}
else if (object instanceof Boolean)
{
appendBoolean(buffer, (Boolean)object);
}
else if (object.getClass().isArray())
{
appendArray(buffer, object);
}
else if (object instanceof Character)
{
appendString(buffer, object.toString());
}
else if (object instanceof Convertible)
{
appendJSON(buffer, (Convertible)object);
}
else if (object instanceof Generator)
{
appendJSON(buffer, (Generator)object);
}
else
{
// Check Convertor before Collection to support JSONCollectionConvertor.
Convertor convertor = getConvertor(object.getClass());
if (convertor != null)
{
appendJSON(buffer, convertor, object);
}
else if (object instanceof Collection)
{
appendArray(buffer, (Collection>)object);
}
else
{
appendString(buffer, object.toString());
}
}
}
catch (IOException e)
{
throw new RuntimeException(e);
}
}
public void appendNull(Appendable buffer)
{
try
{
buffer.append("null");
}
catch (IOException e)
{
throw new RuntimeException(e);
}
}
public void appendJSON(Appendable buffer, Convertor convertor, Object object)
{
appendJSON(buffer, new Convertible()
{
@Override
public void fromJSON(Map object)
{
}
@Override
public void toJSON(Output out)
{
convertor.toJSON(object, out);
}
});
}
public void appendJSON(Appendable buffer, Convertible converter)
{
ConvertableOutput out = new ConvertableOutput(buffer);
converter.toJSON(out);
out.complete();
}
public void appendJSON(Appendable buffer, Generator generator)
{
generator.addJSON(buffer);
}
public void appendMap(Appendable buffer, Map, ?> map)
{
try
{
if (map == null)
{
appendNull(buffer);
return;
}
buffer.append('{');
Iterator extends Map.Entry, ?>> iter = map.entrySet().iterator();
while (iter.hasNext())
{
Map.Entry, ?> entry = iter.next();
quotedEscape(buffer, entry.getKey().toString());
buffer.append(':');
append(buffer, entry.getValue());
if (iter.hasNext())
buffer.append(',');
}
buffer.append('}');
}
catch (IOException e)
{
throw new RuntimeException(e);
}
}
public void appendArray(Appendable buffer, Collection> collection)
{
try
{
if (collection == null)
{
appendNull(buffer);
return;
}
buffer.append('[');
Iterator> iter = collection.iterator();
while (iter.hasNext())
{
append(buffer, iter.next());
if (iter.hasNext())
buffer.append(',');
}
buffer.append(']');
}
catch (IOException e)
{
throw new RuntimeException(e);
}
}
public void appendArray(Appendable buffer, Object array)
{
try
{
if (array == null)
{
appendNull(buffer);
return;
}
buffer.append('[');
int length = Array.getLength(array);
for (int i = 0; i < length; i++)
{
if (i != 0)
buffer.append(',');
append(buffer, Array.get(array, i));
}
buffer.append(']');
}
catch (IOException e)
{
throw new RuntimeException(e);
}
}
public void appendBoolean(Appendable buffer, Boolean b)
{
try
{
if (b == null)
{
appendNull(buffer);
return;
}
buffer.append(b ? "true" : "false");
}
catch (IOException e)
{
throw new RuntimeException(e);
}
}
public void appendNumber(Appendable buffer, Number number)
{
try
{
if (number == null)
{
appendNull(buffer);
return;
}
buffer.append(String.valueOf(number));
}
catch (IOException e)
{
throw new RuntimeException(e);
}
}
public void appendString(Appendable buffer, String string)
{
if (string == null)
{
appendNull(buffer);
return;
}
quotedEscape(buffer, string);
}
/**
* Factory method that creates a Map when a JSON representation of {@code {...}} is parsed.
*
* @return a new Map representing the JSON object
*/
protected Map newMap()
{
return new HashMap<>();
}
/**
* Factory method that creates an array when a JSON representation of {@code [...]} is parsed.
*
* @param size the size of the array
* @return a new array representing the JSON array
* @deprecated use {@link #setArrayConverter(Function)} instead.
*/
@Deprecated
protected Object[] newArray(int size)
{
return new Object[size];
}
/**
* Every time a JSON array representation {@code [...]} is parsed, this method is called
* to (possibly) return a different JSON instance (for example configured with different
* converters) to parse the array items.
*
* @return a JSON instance to parse array items
*/
protected JSON contextForArray()
{
return this;
}
/**
* Every time a JSON object field representation {@code {"name": value}} is parsed,
* this method is called to (possibly) return a different JSON instance (for example
* configured with different converters) to parse the object field.
*
* @param field the field name
* @return a JSON instance to parse the object field
*/
protected JSON contextFor(String field)
{
return this;
}
protected Object convertTo(Class> type, Map map)
{
if (Convertible.class.isAssignableFrom(type))
{
try
{
Convertible convertible = (Convertible)type.getConstructor().newInstance();
convertible.fromJSON(map);
return convertible;
}
catch (Exception e)
{
throw new RuntimeException(e);
}
}
else
{
Convertor convertor = getConvertor(type);
if (convertor != null)
return convertor.fromJSON(map);
return map;
}
}
/**
* Registers a {@link Convertor} for the given class.
*
* @param forClass the class the convertor applies to
* @param convertor the convertor for the class
*/
public void addConvertor(Class> forClass, Convertor convertor)
{
addConvertorFor(forClass.getName(), convertor);
}
/**
* Registers a {@link JSON.Convertor} for a named class.
*
* @param name the name of the class the convertor applies to
* @param convertor the convertor for the class
*/
public void addConvertorFor(String name, Convertor convertor)
{
_convertors.put(name, convertor);
}
/**
* Unregisters a {@link Convertor} for a class.
*
* @param forClass the class the convertor applies to
* @return the convertor for the class
*/
public Convertor removeConvertor(Class> forClass)
{
return removeConvertorFor(forClass.getName());
}
/**
* Unregisters a {@link Convertor} for a named class.
*
* @param name the name of the class the convertor applies to
* @return the convertor for the class
*/
public Convertor removeConvertorFor(String name)
{
return _convertors.remove(name);
}
/**
* Looks up a convertor for a class.
* If no match is found for the class, then the interfaces
* for the class are tried.
* If still no match is found, then the super class and its
* interfaces are tried iteratively.
*
* @param forClass the class to look up the convertor
* @return a {@link JSON.Convertor} or null if none was found for the class
*/
protected Convertor getConvertor(Class> forClass)
{
Class> cls = forClass;
while (cls != null)
{
Convertor convertor = _convertors.get(cls.getName());
if (convertor != null)
return convertor;
Class>[] intfs = cls.getInterfaces();
for (Class> intf : intfs)
{
convertor = _convertors.get(intf.getName());
if (convertor != null)
return convertor;
}
cls = cls.getSuperclass();
}
return null;
}
/**
* Looks up a convertor for a class name.
*
* @param name name of the class to look up the convertor
* @return a {@link JSON.Convertor} or null if none were found.
*/
public Convertor getConvertorFor(String name)
{
return _convertors.get(name);
}
/**
* @return the function to customize the Java representation of JSON arrays
* @see #setArrayConverter(Function)
*/
public Function, Object> getArrayConverter()
{
return _arrayConverter;
}
/**
* Sets the function to convert JSON arrays from their default Java
* representation, a {@code List
*
* @param arrayConverter the function to customize the Java representation of JSON arrays
* @see #getArrayConverter()
*/
public void setArrayConverter(Function, Object> arrayConverter)
{
_arrayConverter = Objects.requireNonNull(arrayConverter);
}
/**
* Parses the given JSON source into an object.
* Although the JSON specification does not allow comments (of any kind)
* this method optionally strips out outer comments of this form:
*
* // An outer comment line.
* /* Another outer comment, multiline.
* // Yet another comment line.
* {
* "name": "the real JSON"
* }
* */ End of outer comment, multiline.
*
*
* @param source the JSON source to parse
* @param stripOuterComment whether to strip outer comments
* @return the object constructed from the JSON string representation
*/
public Object parse(Source source, boolean stripOuterComment)
{
int commentState = 0; // 0=no comment, 1="/", 2="/*", 3="/* *" -1="//"
if (!stripOuterComment)
return parse(source);
int stripState = 1; // 0=no strip, 1=wait for /*, 2= wait for */
Object o = null;
while (source.hasNext())
{
char c = source.peek();
// Handle // or /* comment.
if (commentState == 1)
{
switch (c)
{
case '/':
commentState = -1;
break;
case '*':
commentState = 2;
if (stripState == 1)
{
commentState = 0;
stripState = 2;
}
break;
default:
break;
}
}
// Handle /* C style */ comment.
else if (commentState > 1)
{
switch (c)
{
case '*':
commentState = 3;
break;
case '/':
if (commentState == 3)
{
commentState = 0;
if (stripState == 2)
return o;
}
break;
default:
commentState = 2;
break;
}
}
// Handle // comment.
else if (commentState < 0)
{
switch (c)
{
case '\r':
case '\n':
commentState = 0;
break;
default:
break;
}
}
// Handle unknown.
else
{
if (!Character.isWhitespace(c))
{
if (c == '/')
commentState = 1;
else if (c == '*')
commentState = 3;
else if (o == null)
{
o = parse(source);
continue;
}
}
}
source.next();
}
return o;
}
/**
* Parses the given JSON string into an object.
*
* @param string the JSON string to parse
* @return the object constructed from the JSON string representation
*/
public Object fromJSON(String string)
{
return parse(new StringSource(string), false);
}
/**
* Parses the JSON from the given Reader into an object.
*
* @param reader the Reader to read the JSON from
* @return the object constructed from the JSON string representation
*/
public Object fromJSON(Reader reader)
{
return parse(new ReaderSource(reader), false);
}
/**
* Parses the given JSON source into an object.
* Although the JSON specification does not allow comments (of any kind)
* this method strips out initial comments of this form:
*
* // An initial comment line.
* /* An initial
* multiline comment */
* {
* "name": "foo"
* }
*
* This method detects the object type and calls other
* parse methods for each object type, see for example
* {@link #parseArray(Source)}.
*
* @param source the JSON source to parse
* @return the object constructed from the JSON string representation
*/
public Object parse(Source source)
{
int commentState = 0; // 0=no comment, 1="/", 2="/*", 3="/* *" -1="//"
while (source.hasNext())
{
char c = source.peek();
// Handle // or /* comment.
if (commentState == 1)
{
switch (c)
{
case '/':
commentState = -1;
break;
case '*':
commentState = 2;
break;
default:
break;
}
}
// Handle /* C Style */ comment.
else if (commentState > 1)
{
switch (c)
{
case '*':
commentState = 3;
break;
case '/':
if (commentState == 3)
commentState = 0;
break;
default:
commentState = 2;
break;
}
}
// Handle // comment.
else if (commentState < 0)
{
switch (c)
{
case '\r':
case '\n':
commentState = 0;
break;
default:
break;
}
}
// Handle unknown.
else
{
switch (c)
{
case '{':
return parseObject(source);
case '[':
return parseArray(source);
case '"':
return parseString(source);
case '-':
return parseNumber(source);
case 'n':
complete("null", source);
return null;
case 't':
complete("true", source);
return Boolean.TRUE;
case 'f':
complete("false", source);
return Boolean.FALSE;
case 'u':
complete("undefined", source);
return null;
case 'N':
complete("NaN", source);
return null;
case '/':
commentState = 1;
break;
default:
if (Character.isDigit(c))
return parseNumber(source);
else if (Character.isWhitespace(c))
break;
return handleUnknown(source, c);
}
}
source.next();
}
return null;
}
protected Object handleUnknown(Source source, char c)
{
throw new IllegalStateException("unknown char '" + c + "'(" + (int)c + ") in " + source);
}
protected Object parseObject(Source source)
{
if (source.next() != '{')
throw new IllegalStateException();
Map map = newMap();
char next = seekTo("\"}", source);
while (source.hasNext())
{
if (next == '}')
{
source.next();
break;
}
String name = parseString(source);
seekTo(':', source);
source.next();
Object value = contextFor(name).parse(source);
map.put(name, value);
seekTo(",}", source);
next = source.next();
if (next == '}')
break;
else
next = seekTo("\"}", source);
}
String xclassname = (String)map.get("x-class");
if (xclassname != null)
{
Convertor c = getConvertorFor(xclassname);
if (c != null)
return c.fromJSON(map);
LOG.warn("No Convertor for x-class '{}'", xclassname);
}
String classname = (String)map.get("class");
if (classname != null)
{
try
{
Class> c = Loader.loadClass(classname);
return convertTo(c, map);
}
catch (ClassNotFoundException e)
{
LOG.warn("No class for '{}'", classname);
}
}
return map;
}
private Object defaultArrayConverter(List> list)
{
// Call newArray() to keep backward compatibility.
Object[] objects = newArray(list.size());
IntStream.range(0, list.size()).forEach(i -> objects[i] = list.get(i));
return objects;
}
protected Object parseArray(Source source)
{
if (source.next() != '[')
throw new IllegalStateException();
int size = 0;
List