/*
* Copyright (c) 2010 Google 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.google.api.client.json;
import com.google.api.client.json.JsonPolymorphicTypeMap.TypeDef;
import com.google.api.client.util.Beta;
import com.google.api.client.util.ClassInfo;
import com.google.api.client.util.Data;
import com.google.api.client.util.FieldInfo;
import com.google.api.client.util.GenericData;
import com.google.api.client.util.Preconditions;
import com.google.api.client.util.Sets;
import com.google.api.client.util.Types;
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.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. See
* <a href="https://code.google.com/p/google-http-java-client/wiki/JSON">
* https://code.google.com/p/google-http-java-client/wiki/JSON</a>
*
* <p>
* Implementation has no fields and therefore thread-safe, but sub-classes are not necessarily
* thread-safe.
* </p>
* <p>
* <p>
* If a JSON map is encountered while using a destination class of type Map, then an
* {@link ArrayMap} is used by default for the parsed values.
* </p>
*
* @since 1.3
* @author Yaniv Inbar
*/
public abstract class JsonParser {
/**
* 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<Class<?>, Field> cachedTypemapFields =
new WeakHashMap<Class<?>, 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 <T> 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> T parseAndClose(Class<T> destinationClass) throws IOException {
return parseAndClose(destinationClass, null);
}
/**
* {@link Beta} <br/>
* 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 <T> 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> T parseAndClose(Class<T> 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.
*
* <p>
* 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.
* </p>
*
* @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.
*
* <p>
* 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.
* </p>
*
* @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<String> 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.
*
* <p>
* 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}.
* </p>
*/
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.
*
* <p>
* Before this method is called, the parser must either point to the start or end of a JSON object
* or to a field name.
* </p>
*
* @param destination destination object
* @since 1.15
*/
public final void parseAndClose(Object destination) throws IOException {
parseAndClose(destination, null);
}
/**
* {@link Beta} <br/>
* 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.
*
* <p>
* Before this method is called, the parser must either point to the start or end of a JSON object
* or to a field name.
* </p>
*
* @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.
*
* <p>
* 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}.
* </p>
*
* @param <T> 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> T parse(Class<T> destinationClass) throws IOException {
return parse(destinationClass, null);
}
/**
* {@link Beta} <br/>
* Parse a JSON object, array, or value into a new instance of the given destination class,
* optionally using the given parser customizer.
*
* <p>
* 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}.
* </p>
*
* @param <T> 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> T parse(Class<T> 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.
*
* <p>
* 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}.
* </p>
*
* @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} <br/>
* Parse a JSON object, array, or value into a new instance of the given destination class,
* optionally using the given parser customizer.
*
* <p>
* 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}.
* </p>
*
* @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<Type>(), null, customizeParser, true);
} finally {
if (close) {
close();
}
}
}
/**
* Parse a JSON object from the given JSON parser into the given destination object.
*
* <p>
* 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.
* </p>
*
* @param destination destination object
* @since 1.15
*/
public final void parse(Object destination) throws IOException {
parse(destination, null);
}
/**
* {@link Beta} <br/>
* Parse a JSON object from the given JSON parser into the given destination object, optionally
* using the given parser customizer.
*
* <p>
* 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.
* </p>
*
* @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<Type> context = new ArrayList<Type>();
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<Type> 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<String, Object> destinationMap = (Map<String, Object>) 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 <T> Collection<T> parseArrayAndClose(
Class<?> destinationCollectionClass, Class<T> destinationItemClass) throws IOException {
return parseArrayAndClose(destinationCollectionClass, destinationItemClass, null);
}
/**
* {@link Beta} <br/>
* 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 <T> Collection<T> parseArrayAndClose(Class<?> destinationCollectionClass,
Class<T> 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 <T> void parseArrayAndClose(
Collection<? super T> destinationCollection, Class<T> destinationItemClass)
throws IOException {
parseArrayAndClose(destinationCollection, destinationItemClass, null);
}
/**
* {@link Beta} <br/>
* 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 <T> void parseArrayAndClose(Collection<? super T> destinationCollection,
Class<T> 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 <T> Collection<T> parseArray(
Class<?> destinationCollectionClass, Class<T> destinationItemClass) throws IOException {
return parseArray(destinationCollectionClass, destinationItemClass, null);
}
/**
* {@link Beta} <br/>
* 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 <T> Collection<T> parseArray(Class<?> destinationCollectionClass,
Class<T> destinationItemClass, CustomizeJsonParser customizeParser) throws IOException {
@SuppressWarnings("unchecked")
Collection<T> destinationCollection =
(Collection<T>) 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 <T> void parseArray(
Collection<? super T> destinationCollection, Class<T> destinationItemClass)
throws IOException {
parseArray(destinationCollection, destinationItemClass, null);
}
/**
* {@link Beta} <br/>
* 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 <T> void parseArray(Collection<? super T> destinationCollection,
Class<T> destinationItemClass, CustomizeJsonParser customizeParser) throws IOException {
parseArray(
null, destinationCollection, destinationItemClass, new ArrayList<Type>(), 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 <T> void parseArray(Field fieldContext, Collection<T> destinationCollection,
Type destinationItemType, ArrayList<Type> 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<String, Object> destinationMap, Type valueType,
ArrayList<Type> 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<Type> 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 (getCurrentToken()) {
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<Object> collectionValue = null;
if (customizeParser != null && fieldContext != null) {
collectionValue = customizeParser.newInstanceForArray(destination, fieldContext);
}
if (collectionValue == null) {
collectionValue = Data.newCollectionInstance(valueType);
}
Type subType = null;
if (isArray) {
subType = Types.getArrayComponentType(valueType);
} else if (valueClass != null && Iterable.class.isAssignableFrom(valueClass)) {
subType = Types.getIterableParameter(valueType);
}
subType = Data.resolveWildcardTypeOrTypeVariable(context, subType);
parseArray(fieldContext, collectionValue, subType, context, customizeParser);
if (isArray) {
return Types.toArray(collectionValue, Types.getRawArrayComponentType(context, subType));
}
return collectionValue;
case FIELD_NAME:
case START_OBJECT:
case END_OBJECT:
Preconditions.checkArgument(
!Types.isArray(valueType), "expected object or map type but got %s", valueType);
// Check if we're parsing into a polymorphic datatype.
Field typemapField = handlePolymorphic ? getCachedTypemapFieldFor(valueClass) : null;
Object newInstance = null;
if (valueClass != null && customizeParser != null) {
newInstance = customizeParser.newInstanceForObject(destination, valueClass);
}
boolean isMap = valueClass != null && Types.isAssignableToOrFrom(valueClass, Map.class);
if (typemapField != null) {
newInstance = new GenericJson();
} else if (newInstance == null) {
// check if it is a map to avoid ClassCastException to Map
if (isMap || valueClass == null) {
newInstance = Data.newMapInstance(valueClass);
} else {
newInstance = Types.newInstance(valueClass);
}
}
int contextSize = context.size();
if (valueType != null) {
context.add(valueType);
}
if (isMap && !GenericData.class.isAssignableFrom(valueClass)) {
Type subValueType = Map.class.isAssignableFrom(valueClass)
? Types.getMapValueParameter(valueType) : null;
if (subValueType != null) {
@SuppressWarnings("unchecked")
Map<String, Object> destinationMap = (Map<String, Object>) newInstance;
parseMap(fieldContext, destinationMap, subValueType, context, customizeParser);
return newInstance;
}
}
parse(context, newInstance, customizeParser);
if (valueType != null) {
context.remove(contextSize);
}
if (typemapField == null) {
return newInstance;
}
// Get the correct type out of the naively parsed data.
Object typeValueObject = ((GenericJson) newInstance).get(typemapField.getName());
Preconditions.checkArgument(
typeValueObject != null, "No value specified for @JsonPolymorphicTypeMap field");
String typeValue = typeValueObject.toString();
JsonPolymorphicTypeMap typeMap = typemapField.getAnnotation(JsonPolymorphicTypeMap.class);
Class<?> typeClass = null;
for (TypeDef typeDefinition : typeMap.typeDefinitions()) {
if (typeDefinition.key().equals(typeValue)) {
typeClass = typeDefinition.ref();
break;
}
}
Preconditions.checkArgument(
typeClass != null, "No TypeDef annotation found with key: " + typeValue);
JsonFactory factory = getFactory();
// TODO(ngmiceli): Avoid having to parse JSON content twice. Optimize when type is first.
JsonParser parser = factory.createJsonParser(factory.toString(newInstance));
parser.startParsing();
return parser.parseValue(fieldContext, typeClass, context, null, null, false);
case VALUE_TRUE:
case VALUE_FALSE:
Preconditions.checkArgument(valueType == null || valueClass == boolean.class
|| valueClass != null && valueClass.isAssignableFrom(Boolean.class),
"expected type Boolean or boolean but got %s", valueType);
return token == JsonToken.VALUE_TRUE ? Boolean.TRUE : Boolean.FALSE;
case VALUE_NUMBER_FLOAT:
case VALUE_NUMBER_INT:
Preconditions.checkArgument(
fieldContext == null || fieldContext.getAnnotation(JsonString.class) == null,
"number type formatted as a JSON number cannot use @JsonString annotation");
if (valueClass == null || valueClass.isAssignableFrom(BigDecimal.class)) {
return getDecimalValue();
}
if (valueClass == BigInteger.class) {
return getBigIntegerValue();
}
if (valueClass == Double.class || valueClass == double.class) {
return getDoubleValue();
}
if (valueClass == Long.class || valueClass == long.class) {
return getLongValue();
}
if (valueClass == Float.class || valueClass == float.class) {
return getFloatValue();
}
if (valueClass == Integer.class || valueClass == int.class) {
return getIntValue();
}
if (valueClass == Short.class || valueClass == short.class) {
return getShortValue();
}
if (valueClass == Byte.class || valueClass == byte.class) {
return getByteValue();
}
throw new IllegalArgumentException("expected numeric type but got " + valueType);
case VALUE_STRING:
Preconditions.checkArgument(valueClass == null
|| !Number.class.isAssignableFrom(valueClass) || fieldContext != null
&& fieldContext.getAnnotation(JsonString.class) != null,
"number field formatted as a JSON string must use the @JsonString annotation");
// TODO(yanivi): "special" values like Double.POSITIVE_INFINITY?
return Data.parsePrimitiveValue(valueType, getText());
case VALUE_NULL:
Preconditions.checkArgument(valueClass == null || !valueClass.isPrimitive(),
"primitive number field but found a JSON null");
if (valueClass != null
&& 0 != (valueClass.getModifiers() & (Modifier.ABSTRACT | Modifier.INTERFACE))) {
if (Types.isAssignableToOrFrom(valueClass, Collection.class)) {
return Data.nullOf(Data.newCollectionInstance(valueType).getClass());
}
if (Types.isAssignableToOrFrom(valueClass, Map.class)) {
return Data.nullOf(Data.newMapInstance(valueClass).getClass());
}
}
return Data.nullOf(Types.getRawArrayComponentType(context, valueType));
default:
throw new IllegalArgumentException("unexpected JSON node type: " + token);
}
} catch (IllegalArgumentException e) {
// build context string
StringBuilder contextStringBuilder = new StringBuilder();
String currentName = getCurrentName();
if (currentName != null) {
contextStringBuilder.append("key ").append(currentName);
}
if (fieldContext != null) {
if (currentName != null) {
contextStringBuilder.append(", ");
}
contextStringBuilder.append("field ").append(fieldContext);
}
throw new IllegalArgumentException(contextStringBuilder.toString(), e);
}
}
/**
* Finds the {@link Field} on the given {@link Class} that has the {@link JsonPolymorphicTypeMap}
* annotation, or {@code null} if there is none.
*
* <p>
* The class must contain exactly zero or one {@link JsonPolymorphicTypeMap} annotation.
* </p>
*
* @param key The {@link Class} to search in, or {@code null}
* @return The {@link Field} with the {@link JsonPolymorphicTypeMap} annotation, or {@code null}
* either if there is none or if the key is {@code null}
*/
private static Field getCachedTypemapFieldFor(Class<?> key) {
if (key == null) {
return null;
}
lock.lock();
try {
// Must use containsKey because we do store null values for when the class has no
// JsonPolymorphicTypeMap field.
if (cachedTypemapFields.containsKey(key)) {
return cachedTypemapFields.get(key);
}
// Find the field that determines the type and cache it.
Field value = null;
Collection<FieldInfo> fieldInfos = ClassInfo.of(key).getFieldInfos();
for (FieldInfo fieldInfo : fieldInfos) {
Field field = fieldInfo.getField();
JsonPolymorphicTypeMap typemapAnnotation =
field.getAnnotation(JsonPolymorphicTypeMap.class);
if (typemapAnnotation != null) {
Preconditions.checkArgument(value == null,
"Class contains more than one field with @JsonPolymorphicTypeMap annotation: %s",
key);
Preconditions.checkArgument(Data.isPrimitive(field.getType()),
"Field which has the @JsonPolymorphicTypeMap, %s, is not a supported type: %s", key,
field.getType());
value = field;
// Check for duplicate typeDef keys
TypeDef[] typeDefs = typemapAnnotation.typeDefinitions();
HashSet<String> typeDefKeys = Sets.newHashSet();
Preconditions.checkArgument(
typeDefs.length > 0, "@JsonPolymorphicTypeMap must have at least one @TypeDef");
for (TypeDef typeDef : typeDefs) {
Preconditions.checkArgument(typeDefKeys.add(typeDef.key()),
"Class contains two @TypeDef annotations with identical key: %s", typeDef.key());
}
}
}
cachedTypemapFields.put(key, value);
return value;
} finally {
lock.unlock();
}
}
}