com.rt.storage.api.client.json.JsonParser Maven / Gradle / Ivy
package com.rt.storage.api.client.json;
import com.rt.storage.api.client.json.JsonPolymorphicTypeMap.TypeDef;
import com.rt.storage.api.client.util.Beta;
import com.rt.storage.api.client.util.ClassInfo;
import com.rt.storage.api.client.util.Data;
import com.rt.storage.api.client.util.FieldInfo;
import com.rt.storage.api.client.util.GenericData;
import com.rt.storage.api.client.util.Preconditions;
import com.rt.storage.api.client.util.Sets;
import com.rt.storage.api.client.util.Types;
import java.io.Closeable;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* Abstract low-level JSON parser.
*
* Implementation has no fields and therefore thread-safe, but sub-classes are not necessarily
* thread-safe.
*
*
If a JSON map is encountered while using a destination class of type Map, then an {@link
* java.util.ArrayMap} is used by default for the parsed values.
*
* @since 1.3
* @author Yaniv Inbar
*/
public abstract class JsonParser implements Closeable {
/**
* Maps a polymorphic {@link Class} to its {@link Field} with the {@link JsonPolymorphicTypeMap}
* annotation, or {@code null} if there is no field with that annotation.
*/
private static WeakHashMap, Field> cachedTypemapFields =
new WeakHashMap, Field>();
/** Lock on the {@code cachedTypemapFields}. */
private static final Lock lock = new ReentrantLock();
/** Returns the JSON factory from which this generator was created. */
public abstract JsonFactory getFactory();
/**
* Closes the parser and the underlying input stream or reader, and releases any memory associated
* with it.
*/
public abstract void close() throws IOException;
/** Returns the next token from the stream or {@code null} to indicate end of input. */
public abstract JsonToken nextToken() throws IOException;
/**
* Returns the token the parser currently points to or {@code null} for none (at start of input or
* after end of input).
*/
public abstract JsonToken getCurrentToken();
/**
* Returns the most recent field name or {@code null} for array values or for root-level values.
*/
public abstract String getCurrentName() throws IOException;
/**
* Skips to the matching {@link JsonToken#END_ARRAY} if current token is {@link
* JsonToken#START_ARRAY}, the matching {@link JsonToken#END_OBJECT} if the current token is
* {@link JsonToken#START_OBJECT}, else does nothing.
*/
public abstract JsonParser skipChildren() throws IOException;
/**
* Returns a textual representation of the current token or {@code null} if {@link
* #getCurrentToken()} is {@code null}.
*/
public abstract String getText() throws IOException;
// TODO(yanivi): Jackson provides getTextCharacters(), getTextLength(), and getTextOffset()
/** Returns the byte value of the current token. */
public abstract byte getByteValue() throws IOException;
/** Returns the short value of the current token. */
public abstract short getShortValue() throws IOException;
/** Returns the int value of the current token. */
public abstract int getIntValue() throws IOException;
/** Returns the float value of the current token. */
public abstract float getFloatValue() throws IOException;
/** Returns the long value of the current token. */
public abstract long getLongValue() throws IOException;
/** Returns the double value of the current token. */
public abstract double getDoubleValue() throws IOException;
/** Returns the {@link BigInteger} value of the current token. */
public abstract BigInteger getBigIntegerValue() throws IOException;
/** Returns the {@link BigDecimal} value of the current token. */
public abstract BigDecimal getDecimalValue() throws IOException;
/**
* Parse a JSON object, array, or value into a new instance of the given destination class, and
* then closes the parser.
*
* @param destination class
* @param destinationClass destination class that has a public default constructor to use to
* create a new instance
* @return new instance of the parsed destination class
* @since 1.15
*/
public final T parseAndClose(Class destinationClass) throws IOException {
return parseAndClose(destinationClass, null);
}
/**
* {@link Beta}
* Parse a JSON object, array, or value into a new instance of the given destination class using
* {@link JsonParser#parse(Class, CustomizeJsonParser)}, and then closes the parser.
*
* @param destination class
* @param destinationClass destination class that has a public default constructor to use to
* create a new instance
* @param customizeParser optional parser customizer or {@code null} for none
* @return new instance of the parsed destination class
*/
@Beta
public final T parseAndClose(Class destinationClass, CustomizeJsonParser customizeParser)
throws IOException {
try {
return parse(destinationClass, customizeParser);
} finally {
close();
}
}
/**
* Skips the values of all keys in the current object until it finds the given key.
*
* Before this method is called, the parser must either point to the start or end of a JSON
* object or to a field name. After this method ends, the current token will either be the {@link
* JsonToken#END_OBJECT} of the current object if the key is not found, or the value of the key
* that was found.
*
* @param keyToFind key to find
*/
public final void skipToKey(String keyToFind) throws IOException {
skipToKey(Collections.singleton(keyToFind));
}
/**
* Skips the values of all keys in the current object until it finds one of the given keys.
*
*
Before this method is called, the parser must either point to the start or end of a JSON
* object or to a field name. After this method ends, the current token will either be the {@link
* JsonToken#END_OBJECT} of the current object if no matching key is found, or the value of the
* key that was found.
*
* @param keysToFind set of keys to look for
* @return name of the first matching key found or {@code null} if no match was found
* @since 1.10
*/
public final String skipToKey(Set keysToFind) throws IOException {
JsonToken curToken = startParsingObjectOrArray();
while (curToken == JsonToken.FIELD_NAME) {
String key = getText();
nextToken();
if (keysToFind.contains(key)) {
return key;
}
skipChildren();
curToken = nextToken();
}
return null;
}
/** Starts parsing that handles start of input by calling {@link #nextToken()}. */
private JsonToken startParsing() throws IOException {
JsonToken currentToken = getCurrentToken();
// token is null at start, so get next token
if (currentToken == null) {
currentToken = nextToken();
}
Preconditions.checkArgument(currentToken != null, "no JSON input found");
return currentToken;
}
/**
* Starts parsing an object or array by making sure the parser points to an object field name,
* first array value or end of object or array.
*
* If the parser is at the start of input, {@link #nextToken()} is called. The current token
* must then be {@link JsonToken#START_OBJECT}, {@link JsonToken#END_OBJECT}, {@link
* JsonToken#START_ARRAY}, {@link JsonToken#END_ARRAY}, or {@link JsonToken#FIELD_NAME}. For an
* object only, after the method is called, the current token must be either {@link
* JsonToken#FIELD_NAME} or {@link JsonToken#END_OBJECT}.
*/
private JsonToken startParsingObjectOrArray() throws IOException {
JsonToken currentToken = startParsing();
switch (currentToken) {
case START_OBJECT:
currentToken = nextToken();
Preconditions.checkArgument(
currentToken == JsonToken.FIELD_NAME || currentToken == JsonToken.END_OBJECT,
currentToken);
break;
case START_ARRAY:
currentToken = nextToken();
break;
default:
break;
}
return currentToken;
}
/**
* Parse a JSON Object from the given JSON parser -- which is closed after parsing completes --
* into the given destination object.
*
*
Before this method is called, the parser must either point to the start or end of a JSON
* object or to a field name.
*
* @param destination destination object
* @since 1.15
*/
public final void parseAndClose(Object destination) throws IOException {
parseAndClose(destination, null);
}
/**
* {@link Beta}
* Parse a JSON Object from the given JSON parser -- which is closed after parsing completes --
* into the given destination object, optionally using the given parser customizer.
*
*
Before this method is called, the parser must either point to the start or end of a JSON
* object or to a field name.
*
* @param destination destination object
* @param customizeParser optional parser customizer or {@code null} for none
*/
@Beta
public final void parseAndClose(Object destination, CustomizeJsonParser customizeParser)
throws IOException {
try {
parse(destination, customizeParser);
} finally {
close();
}
}
/**
* Parse a JSON object, array, or value into a new instance of the given destination class.
*
*
If it parses an object, after this method ends, the current token will be the object's
* ending {@link JsonToken#END_OBJECT}. If it parses an array, after this method ends, the current
* token will be the array's ending {@link JsonToken#END_ARRAY}.
*
* @param destination class
* @param destinationClass destination class that has a public default constructor to use to
* create a new instance
* @return new instance of the parsed destination class
* @since 1.15
*/
public final T parse(Class destinationClass) throws IOException {
return parse(destinationClass, null);
}
/**
* {@link Beta}
* Parse a JSON object, array, or value into a new instance of the given destination class,
* optionally using the given parser customizer.
*
* If it parses an object, after this method ends, the current token will be the object's
* ending {@link JsonToken#END_OBJECT}. If it parses an array, after this method ends, the current
* token will be the array's ending {@link JsonToken#END_ARRAY}.
*
* @param destination class
* @param destinationClass destination class that has a public default constructor to use to
* create a new instance
* @param customizeParser optional parser customizer or {@code null} for none
* @return new instance of the parsed destination class
*/
@Beta
public final T parse(Class destinationClass, CustomizeJsonParser customizeParser)
throws IOException {
@SuppressWarnings("unchecked")
T result = (T) parse(destinationClass, false, customizeParser);
return result;
}
/**
* Parse a JSON object, array, or value into a new instance of the given destination class.
*
* If it parses an object, after this method ends, the current token will be the object's
* ending {@link JsonToken#END_OBJECT}. If it parses an array, after this method ends, the current
* token will be the array's ending {@link JsonToken#END_ARRAY}.
*
* @param dataType Type into which the JSON should be parsed
* @param close {@code true} if {@link #close()} should be called after parsing
* @return new instance of the parsed dataType
* @since 1.15
*/
public Object parse(Type dataType, boolean close) throws IOException {
return parse(dataType, close, null);
}
/**
* {@link Beta}
* Parse a JSON object, array, or value into a new instance of the given destination class,
* optionally using the given parser customizer.
*
*
If it parses an object, after this method ends, the current token will be the object's
* ending {@link JsonToken#END_OBJECT}. If it parses an array, after this method ends, the current
* token will be the array's ending {@link JsonToken#END_ARRAY}.
*
* @param dataType Type into which the JSON should be parsed
* @param close {@code true} if {@link #close()} should be called after parsing
* @param customizeParser optional parser customizer or {@code null} for none
* @return new instance of the parsed dataType
* @since 1.10
*/
@Beta
public Object parse(Type dataType, boolean close, CustomizeJsonParser customizeParser)
throws IOException {
try {
if (!Void.class.equals(dataType)) {
startParsing();
}
return parseValue(null, dataType, new ArrayList(), null, customizeParser, true);
} finally {
if (close) {
close();
}
}
}
/**
* Parse a JSON object from the given JSON parser into the given destination object.
*
* Before this method is called, the parser must either point to the start or end of a JSON
* object or to a field name. After this method ends, the current token will be the {@link
* JsonToken#END_OBJECT} of the current object.
*
* @param destination destination object
* @since 1.15
*/
public final void parse(Object destination) throws IOException {
parse(destination, null);
}
/**
* {@link Beta}
* Parse a JSON object from the given JSON parser into the given destination object, optionally
* using the given parser customizer.
*
*
Before this method is called, the parser must either point to the start or end of a JSON
* object or to a field name. After this method ends, the current token will be the {@link
* JsonToken#END_OBJECT} of the current object.
*
* @param destination destination object
* @param customizeParser optional parser customizer or {@code null} for none
*/
@Beta
public final void parse(Object destination, CustomizeJsonParser customizeParser)
throws IOException {
ArrayList context = new ArrayList();
context.add(destination.getClass());
parse(context, destination, customizeParser);
}
/**
* Parses the next field from the given JSON parser into the given destination object.
*
* @param context destination context stack (possibly empty)
* @param destination destination object instance or {@code null} for none (for example empty
* context stack)
* @param customizeParser optional parser customizer or {@code null} for none
*/
private void parse(
ArrayList context, Object destination, CustomizeJsonParser customizeParser)
throws IOException {
if (destination instanceof GenericJson) {
((GenericJson) destination).setFactory(getFactory());
}
JsonToken curToken = startParsingObjectOrArray();
Class> destinationClass = destination.getClass();
ClassInfo classInfo = ClassInfo.of(destinationClass);
boolean isGenericData = GenericData.class.isAssignableFrom(destinationClass);
if (!isGenericData && Map.class.isAssignableFrom(destinationClass)) {
// The destination class is not a sub-class of GenericData but is of Map, so parse data
// using parseMap.
@SuppressWarnings("unchecked")
Map destinationMap = (Map) destination;
parseMap(
null,
destinationMap,
Types.getMapValueParameter(destinationClass),
context,
customizeParser);
return;
}
while (curToken == JsonToken.FIELD_NAME) {
String key = getText();
nextToken();
// stop at items for feeds
if (customizeParser != null && customizeParser.stopAt(destination, key)) {
return;
}
// get the field from the type information
FieldInfo fieldInfo = classInfo.getFieldInfo(key);
if (fieldInfo != null) {
// skip final fields
if (fieldInfo.isFinal() && !fieldInfo.isPrimitive()) {
throw new IllegalArgumentException("final array/object fields are not supported");
}
Field field = fieldInfo.getField();
int contextSize = context.size();
context.add(field.getGenericType());
Object fieldValue =
parseValue(
field, fieldInfo.getGenericType(), context, destination, customizeParser, true);
context.remove(contextSize);
fieldInfo.setValue(destination, fieldValue);
} else if (isGenericData) {
// store unknown field in generic JSON
GenericData object = (GenericData) destination;
object.set(key, parseValue(null, null, context, destination, customizeParser, true));
} else {
// unrecognized field, skip value.
if (customizeParser != null) {
customizeParser.handleUnrecognizedKey(destination, key);
}
skipChildren();
}
curToken = nextToken();
}
}
/**
* Parse a JSON Array from the given JSON parser (which is closed after parsing completes) into
* the given destination collection.
*
* @param destinationCollectionClass class of destination collection (must have a public default
* constructor)
* @param destinationItemClass class of destination collection item (must have a public default
* constructor)
* @since 1.15
*/
public final Collection parseArrayAndClose(
Class> destinationCollectionClass, Class destinationItemClass) throws IOException {
return parseArrayAndClose(destinationCollectionClass, destinationItemClass, null);
}
/**
* {@link Beta}
* Parse a JSON Array from the given JSON parser (which is closed after parsing completes) into
* the given destination collection, optionally using the given parser customizer.
*
* @param destinationCollectionClass class of destination collection (must have a public default
* constructor)
* @param destinationItemClass class of destination collection item (must have a public default
* constructor)
* @param customizeParser optional parser customizer or {@code null} for none
*/
@Beta
public final Collection parseArrayAndClose(
Class> destinationCollectionClass,
Class destinationItemClass,
CustomizeJsonParser customizeParser)
throws IOException {
try {
return parseArray(destinationCollectionClass, destinationItemClass, customizeParser);
} finally {
close();
}
}
/**
* Parse a JSON Array from the given JSON parser (which is closed after parsing completes) into
* the given destination collection.
*
* @param destinationCollection destination collection
* @param destinationItemClass class of destination collection item (must have a public default
* constructor)
* @since 1.15
*/
public final void parseArrayAndClose(
Collection super T> destinationCollection, Class destinationItemClass)
throws IOException {
parseArrayAndClose(destinationCollection, destinationItemClass, null);
}
/**
* {@link Beta}
* Parse a JSON Array from the given JSON parser (which is closed after parsing completes) into
* the given destination collection, optionally using the given parser customizer.
*
* @param destinationCollection destination collection
* @param destinationItemClass class of destination collection item (must have a public default
* constructor)
* @param customizeParser optional parser customizer or {@code null} for none
*/
@Beta
public final void parseArrayAndClose(
Collection super T> destinationCollection,
Class destinationItemClass,
CustomizeJsonParser customizeParser)
throws IOException {
try {
parseArray(destinationCollection, destinationItemClass, customizeParser);
} finally {
close();
}
}
/**
* Parse a JSON Array from the given JSON parser into the given destination collection.
*
* @param destinationCollectionClass class of destination collection (must have a public default
* constructor)
* @param destinationItemClass class of destination collection item (must have a public default
* constructor)
* @since 1.15
*/
public final Collection parseArray(
Class> destinationCollectionClass, Class destinationItemClass) throws IOException {
return parseArray(destinationCollectionClass, destinationItemClass, null);
}
/**
* {@link Beta}
* Parse a JSON Array from the given JSON parser into the given destination collection, optionally
* using the given parser customizer.
*
* @param destinationCollectionClass class of destination collection (must have a public default
* constructor)
* @param destinationItemClass class of destination collection item (must have a public default
* constructor)
* @param customizeParser optional parser customizer or {@code null} for none
*/
@Beta
public final Collection parseArray(
Class> destinationCollectionClass,
Class destinationItemClass,
CustomizeJsonParser customizeParser)
throws IOException {
@SuppressWarnings("unchecked")
Collection destinationCollection =
(Collection) Data.newCollectionInstance(destinationCollectionClass);
parseArray(destinationCollection, destinationItemClass, customizeParser);
return destinationCollection;
}
/**
* Parse a JSON Array from the given JSON parser into the given destination collection.
*
* @param destinationCollection destination collection
* @param destinationItemClass class of destination collection item (must have a public default
* constructor)
* @since 1.15
*/
public final void parseArray(
Collection super T> destinationCollection, Class destinationItemClass)
throws IOException {
parseArray(destinationCollection, destinationItemClass, null);
}
/**
* {@link Beta}
* Parse a JSON Array from the given JSON parser into the given destination collection, optionally
* using the given parser customizer.
*
* @param destinationCollection destination collection
* @param destinationItemClass class of destination collection item (must have a public default
* constructor)
* @param customizeParser optional parser customizer or {@code null} for none
*/
@Beta
public final void parseArray(
Collection super T> destinationCollection,
Class destinationItemClass,
CustomizeJsonParser customizeParser)
throws IOException {
parseArray(
null, destinationCollection, destinationItemClass, new ArrayList(), customizeParser);
}
/**
* Parse a JSON Array from the given JSON parser into the given destination collection, optionally
* using the given parser customizer.
*
* @param fieldContext field context or {@code null} for none
* @param destinationCollection destination collection
* @param destinationItemType type of destination collection item
* @param context destination context stack (possibly empty)
* @param customizeParser optional parser customizer or {@code null} for none
*/
private void parseArray(
Field fieldContext,
Collection destinationCollection,
Type destinationItemType,
ArrayList context,
CustomizeJsonParser customizeParser)
throws IOException {
JsonToken curToken = startParsingObjectOrArray();
while (curToken != JsonToken.END_ARRAY) {
@SuppressWarnings("unchecked")
T parsedValue =
(T)
parseValue(
fieldContext,
destinationItemType,
context,
destinationCollection,
customizeParser,
true);
destinationCollection.add(parsedValue);
curToken = nextToken();
}
}
/**
* Parse a JSON Object from the given JSON parser into the given destination map, optionally using
* the given parser customizer.
*
* @param fieldContext field context or {@code null} for none
* @param destinationMap destination map
* @param valueType valueType of the map value type parameter
* @param context destination context stack (possibly empty)
* @param customizeParser optional parser customizer or {@code null} for none
*/
private void parseMap(
Field fieldContext,
Map destinationMap,
Type valueType,
ArrayList context,
CustomizeJsonParser customizeParser)
throws IOException {
JsonToken curToken = startParsingObjectOrArray();
while (curToken == JsonToken.FIELD_NAME) {
String key = getText();
nextToken();
// stop at items for feeds
if (customizeParser != null && customizeParser.stopAt(destinationMap, key)) {
return;
}
Object value =
parseValue(fieldContext, valueType, context, destinationMap, customizeParser, true);
destinationMap.put(key, value);
curToken = nextToken();
}
}
/**
* Parse a value.
*
* @param fieldContext field context or {@code null} for none (for example into a map)
* @param valueType value type or {@code null} if not known (for example into a map)
* @param context destination context stack (possibly empty)
* @param destination destination object instance or {@code null} for none (for example empty
* context stack)
* @param customizeParser customize parser or {@code null} for none
* @param handlePolymorphic whether or not to check for polymorphic schema
* @return parsed value
*/
private final Object parseValue(
Field fieldContext,
Type valueType,
ArrayList context,
Object destination,
CustomizeJsonParser customizeParser,
boolean handlePolymorphic)
throws IOException {
valueType = Data.resolveWildcardTypeOrTypeVariable(context, valueType);
// resolve a parameterized type to a class
Class> valueClass = valueType instanceof Class> ? (Class>) valueType : null;
if (valueType instanceof ParameterizedType) {
valueClass = Types.getRawClass((ParameterizedType) valueType);
}
// Void means skip
if (valueClass == Void.class) {
skipChildren();
return null;
}
// value type is now null, class, parameterized type, or generic array type
JsonToken token = getCurrentToken();
try {
switch (token) {
case START_ARRAY:
case END_ARRAY:
boolean isArray = Types.isArray(valueType);
Preconditions.checkArgument(
valueType == null
|| isArray
|| valueClass != null && Types.isAssignableToOrFrom(valueClass, Collection.class),
"expected collection or array type but got %s",
valueType);
Collection