gson-comments/gson/src/main/java/com/google/gson/internal/bind/ReflectiveTypeAdapterFactor...

535 lines
22 KiB
Java

/*
* Copyright (C) 2011 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.gson.internal.bind;
import com.google.gson.FieldNamingStrategy;
import com.google.gson.Gson;
import com.google.gson.JsonIOException;
import com.google.gson.JsonParseException;
import com.google.gson.JsonSyntaxException;
import com.google.gson.ReflectionAccessFilter;
import com.google.gson.ReflectionAccessFilter.FilterResult;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.annotations.JsonAdapter;
import com.google.gson.annotations.SerializedName;
import com.google.gson.internal.$Gson$Types;
import com.google.gson.internal.ConstructorConstructor;
import com.google.gson.internal.Excluder;
import com.google.gson.internal.ObjectConstructor;
import com.google.gson.internal.Primitives;
import com.google.gson.internal.ReflectionAccessFilterHelper;
import com.google.gson.internal.reflect.ReflectionHelper;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;
import java.io.IOException;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* Type adapter that reflects over the fields and methods of a class.
*/
public final class ReflectiveTypeAdapterFactory implements TypeAdapterFactory {
private final ConstructorConstructor constructorConstructor;
private final FieldNamingStrategy fieldNamingPolicy;
private final Excluder excluder;
private final JsonAdapterAnnotationTypeAdapterFactory jsonAdapterFactory;
private final List<ReflectionAccessFilter> reflectionFilters;
public ReflectiveTypeAdapterFactory(ConstructorConstructor constructorConstructor,
FieldNamingStrategy fieldNamingPolicy, Excluder excluder,
JsonAdapterAnnotationTypeAdapterFactory jsonAdapterFactory,
List<ReflectionAccessFilter> reflectionFilters) {
this.constructorConstructor = constructorConstructor;
this.fieldNamingPolicy = fieldNamingPolicy;
this.excluder = excluder;
this.jsonAdapterFactory = jsonAdapterFactory;
this.reflectionFilters = reflectionFilters;
}
private boolean includeField(Field f, boolean serialize) {
return !excluder.excludeClass(f.getType(), serialize) && !excluder.excludeField(f, serialize);
}
/** first element holds the default name */
private List<String> getFieldNames(Field f) {
SerializedName annotation = f.getAnnotation(SerializedName.class);
if (annotation == null) {
String name = fieldNamingPolicy.translateName(f);
return Collections.singletonList(name);
}
String serializedName = annotation.value();
String[] alternates = annotation.alternate();
if (alternates.length == 0) {
return Collections.singletonList(serializedName);
}
List<String> fieldNames = new ArrayList<>(alternates.length + 1);
fieldNames.add(serializedName);
Collections.addAll(fieldNames, alternates);
return fieldNames;
}
@Override
public <T> TypeAdapter<T> create(Gson gson, final TypeToken<T> type) {
Class<? super T> raw = type.getRawType();
if (!Object.class.isAssignableFrom(raw)) {
return null; // it's a primitive!
}
FilterResult filterResult =
ReflectionAccessFilterHelper.getFilterResult(reflectionFilters, raw);
if (filterResult == FilterResult.BLOCK_ALL) {
throw new JsonIOException(
"ReflectionAccessFilter does not permit using reflection for " + raw
+ ". Register a TypeAdapter for this type or adjust the access filter.");
}
boolean blockInaccessible = filterResult == FilterResult.BLOCK_INACCESSIBLE;
// If the type is actually a Java Record, we need to use the RecordAdapter instead. This will always be false
// on JVMs that do not support records.
if (ReflectionHelper.isRecord(raw)) {
@SuppressWarnings("unchecked")
TypeAdapter<T> adapter = (TypeAdapter<T>) new RecordAdapter<>(raw,
getBoundFields(gson, type, raw, blockInaccessible, true), blockInaccessible);
return adapter;
}
ObjectConstructor<T> constructor = constructorConstructor.get(type);
return new FieldReflectionAdapter<>(constructor, getBoundFields(gson, type, raw, blockInaccessible, false));
}
private static <M extends AccessibleObject & Member> void checkAccessible(Object object, M member) {
if (!ReflectionAccessFilterHelper.canAccess(member, Modifier.isStatic(member.getModifiers()) ? null : object)) {
String memberDescription = ReflectionHelper.getAccessibleObjectDescription(member, true);
throw new JsonIOException(memberDescription + " is not accessible and ReflectionAccessFilter does not"
+ " permit making it accessible. Register a TypeAdapter for the declaring type, adjust the"
+ " access filter or increase the visibility of the element and its declaring type.");
}
}
private BoundField createBoundField(
final Gson context, final Field field, final Method accessor, final String name,
final TypeToken<?> fieldType, boolean serialize, boolean deserialize,
final boolean blockInaccessible) {
final boolean isPrimitive = Primitives.isPrimitive(fieldType.getRawType());
int modifiers = field.getModifiers();
final boolean isStaticFinalField = Modifier.isStatic(modifiers) && Modifier.isFinal(modifiers);
JsonAdapter annotation = field.getAnnotation(JsonAdapter.class);
TypeAdapter<?> mapped = null;
if (annotation != null) {
// This is not safe; requires that user has specified correct adapter class for @JsonAdapter
mapped = jsonAdapterFactory.getTypeAdapter(
constructorConstructor, context, fieldType, annotation);
}
final boolean jsonAdapterPresent = mapped != null;
if (mapped == null) mapped = context.getAdapter(fieldType);
@SuppressWarnings("unchecked")
final TypeAdapter<Object> typeAdapter = (TypeAdapter<Object>) mapped;
return new BoundField(name, field, serialize, deserialize) {
@Override void write(JsonWriter writer, Object source)
throws IOException, IllegalAccessException {
if (!serialized) return;
if (blockInaccessible) {
if (accessor == null) {
checkAccessible(source, field);
} else {
// Note: This check might actually be redundant because access check for canonical
// constructor should have failed already
checkAccessible(source, accessor);
}
}
Object fieldValue;
if (accessor != null) {
try {
fieldValue = accessor.invoke(source);
} catch (InvocationTargetException e) {
String accessorDescription = ReflectionHelper.getAccessibleObjectDescription(accessor, false);
throw new JsonIOException("Accessor " + accessorDescription + " threw exception", e.getCause());
}
} else {
fieldValue = field.get(source);
}
if (fieldValue == source) {
// avoid direct recursion
return;
}
writer.name(name);
TypeAdapter<Object> t = jsonAdapterPresent ? typeAdapter
: new TypeAdapterRuntimeTypeWrapper<>(context, typeAdapter, fieldType.getType());
t.write(writer, fieldValue);
}
@Override
void readIntoArray(JsonReader reader, int index, Object[] target) throws IOException, JsonParseException {
Object fieldValue = typeAdapter.read(reader);
if (fieldValue == null && isPrimitive) {
throw new JsonParseException("null is not allowed as value for record component '" + fieldName + "'"
+ " of primitive type; at path " + reader.getPath());
}
target[index] = fieldValue;
}
@Override
void readIntoField(JsonReader reader, Object target)
throws IOException, IllegalAccessException {
Object fieldValue = typeAdapter.read(reader);
if (fieldValue != null || !isPrimitive) {
if (blockInaccessible) {
checkAccessible(target, field);
} else if (isStaticFinalField) {
// Reflection does not permit setting value of `static final` field, even after calling `setAccessible`
// Handle this here to avoid causing IllegalAccessException when calling `Field.set`
String fieldDescription = ReflectionHelper.getAccessibleObjectDescription(field, false);
throw new JsonIOException("Cannot set value of 'static final' " + fieldDescription);
}
field.set(target, fieldValue);
}
}
};
}
private Map<String, BoundField> getBoundFields(Gson context, TypeToken<?> type, Class<?> raw,
boolean blockInaccessible, boolean isRecord) {
Map<String, BoundField> result = new LinkedHashMap<>();
if (raw.isInterface()) {
return result;
}
Class<?> originalRaw = raw;
while (raw != Object.class) {
Field[] fields = raw.getDeclaredFields();
// For inherited fields, check if access to their declaring class is allowed
if (raw != originalRaw && fields.length > 0) {
FilterResult filterResult = ReflectionAccessFilterHelper.getFilterResult(reflectionFilters, raw);
if (filterResult == FilterResult.BLOCK_ALL) {
throw new JsonIOException("ReflectionAccessFilter does not permit using reflection for " + raw
+ " (supertype of " + originalRaw + "). Register a TypeAdapter for this type"
+ " or adjust the access filter.");
}
blockInaccessible = filterResult == FilterResult.BLOCK_INACCESSIBLE;
}
for (Field field : fields) {
boolean serialize = includeField(field, true);
boolean deserialize = includeField(field, false);
if (!serialize && !deserialize) {
continue;
}
// The accessor method is only used for records. If the type is a record, we will read out values
// via its accessor method instead of via reflection. This way we will bypass the accessible restrictions
Method accessor = null;
if (isRecord) {
// If there is a static field on a record, there will not be an accessor. Instead we will use the default
// field serialization logic, but for deserialization the field is excluded for simplicity. Note that Gson
// ignores static fields by default, but GsonBuilder.excludeFieldsWithModifiers can overwrite this.
if (Modifier.isStatic(field.getModifiers())) {
deserialize = false;
} else {
accessor = ReflectionHelper.getAccessor(raw, field);
// If blockInaccessible, skip and perform access check later
if (!blockInaccessible) {
ReflectionHelper.makeAccessible(accessor);
}
// @SerializedName can be placed on accessor method, but it is not supported there
// If field and method have annotation it is not easily possible to determine if accessor method
// is implicit and has inherited annotation, or if it is explicitly declared with custom annotation
if (accessor.getAnnotation(SerializedName.class) != null
&& field.getAnnotation(SerializedName.class) == null) {
String methodDescription = ReflectionHelper.getAccessibleObjectDescription(accessor, false);
throw new JsonIOException("@SerializedName on " + methodDescription + " is not supported");
}
}
}
// If blockInaccessible, skip and perform access check later
// For Records if the accessor method is used the field does not have to be made accessible
if (!blockInaccessible && accessor == null) {
ReflectionHelper.makeAccessible(field);
}
Type fieldType = $Gson$Types.resolve(type.getType(), raw, field.getGenericType());
List<String> fieldNames = getFieldNames(field);
BoundField previous = null;
for (int i = 0, size = fieldNames.size(); i < size; ++i) {
String name = fieldNames.get(i);
if (i != 0) serialize = false; // only serialize the default name
BoundField boundField = createBoundField(context, field, accessor, name,
TypeToken.get(fieldType), serialize, deserialize, blockInaccessible);
BoundField replaced = result.put(name, boundField);
if (previous == null) previous = replaced;
}
if (previous != null) {
throw new IllegalArgumentException("Class " + originalRaw.getName()
+ " declares multiple JSON fields named '" + previous.name + "'; conflict is caused"
+ " by fields " + ReflectionHelper.fieldToString(previous.field) + " and " + ReflectionHelper.fieldToString(field));
}
}
type = TypeToken.get($Gson$Types.resolve(type.getType(), raw, raw.getGenericSuperclass()));
raw = type.getRawType();
}
return result;
}
static abstract class BoundField {
final String name;
final Field field;
/** Name of the underlying field */
final String fieldName;
final boolean serialized;
final boolean deserialized;
protected BoundField(String name, Field field, boolean serialized, boolean deserialized) {
this.name = name;
this.field = field;
this.fieldName = field.getName();
this.serialized = serialized;
this.deserialized = deserialized;
}
/** Read this field value from the source, and append its JSON value to the writer */
abstract void write(JsonWriter writer, Object source) throws IOException, IllegalAccessException;
/** Read the value into the target array, used to provide constructor arguments for records */
abstract void readIntoArray(JsonReader reader, int index, Object[] target) throws IOException, JsonParseException;
/** Read the value from the reader, and set it on the corresponding field on target via reflection */
abstract void readIntoField(JsonReader reader, Object target) throws IOException, IllegalAccessException;
}
/**
* Base class for Adapters produced by this factory.
*
* <p>The {@link RecordAdapter} is a special case to handle records for JVMs that support it, for
* all other types we use the {@link FieldReflectionAdapter}. This class encapsulates the common
* logic for serialization and deserialization. During deserialization, we construct an
* accumulator A, which we use to accumulate values from the source JSON. After the object has been read in
* full, the {@link #finalize(Object)} method is used to convert the accumulator to an instance
* of T.
*
* @param <T> type of objects that this Adapter creates.
* @param <A> type of accumulator used to build the deserialization result.
*/
// This class is public because external projects check for this class with `instanceof` (even though it is internal)
public static abstract class Adapter<T, A> extends TypeAdapter<T> {
final Map<String, BoundField> boundFields;
Adapter(Map<String, BoundField> boundFields) {
this.boundFields = boundFields;
}
@Override
public void write(JsonWriter out, T value) throws IOException {
if (value == null) {
out.nullValue();
return;
}
out.beginObject();
try {
for (BoundField boundField : boundFields.values()) {
boundField.write(out, value);
}
} catch (IllegalAccessException e) {
throw ReflectionHelper.createExceptionForUnexpectedIllegalAccess(e);
}
out.endObject();
}
@Override
public T read(JsonReader in) throws IOException {
if (in.peek() == JsonToken.NULL) {
in.nextNull();
return null;
}
A accumulator = createAccumulator();
try {
in.beginObject();
while (in.hasNext()) {
String name = in.nextName();
BoundField field = boundFields.get(name);
if (field == null || !field.deserialized) {
in.skipValue();
} else {
readField(accumulator, in, field);
}
}
} catch (IllegalStateException e) {
throw new JsonSyntaxException(e);
} catch (IllegalAccessException e) {
throw ReflectionHelper.createExceptionForUnexpectedIllegalAccess(e);
}
in.endObject();
return finalize(accumulator);
}
/** Create the Object that will be used to collect each field value */
abstract A createAccumulator();
/**
* Read a single BoundField into the accumulator. The JsonReader will be pointed at the
* start of the value for the BoundField to read from.
*/
abstract void readField(A accumulator, JsonReader in, BoundField field)
throws IllegalAccessException, IOException;
/** Convert the accumulator to a final instance of T. */
abstract T finalize(A accumulator);
}
private static final class FieldReflectionAdapter<T> extends Adapter<T, T> {
private final ObjectConstructor<T> constructor;
FieldReflectionAdapter(ObjectConstructor<T> constructor, Map<String, BoundField> boundFields) {
super(boundFields);
this.constructor = constructor;
}
@Override
T createAccumulator() {
return constructor.construct();
}
@Override
void readField(T accumulator, JsonReader in, BoundField field)
throws IllegalAccessException, IOException {
field.readIntoField(in, accumulator);
}
@Override
T finalize(T accumulator) {
return accumulator;
}
}
private static final class RecordAdapter<T> extends Adapter<T, Object[]> {
static final Map<Class<?>, Object> PRIMITIVE_DEFAULTS = primitiveDefaults();
// The canonical constructor of the record
private final Constructor<T> constructor;
// Array of arguments to the constructor, initialized with default values for primitives
private final Object[] constructorArgsDefaults;
// Map from component names to index into the constructors arguments.
private final Map<String, Integer> componentIndices = new HashMap<>();
RecordAdapter(Class<T> raw, Map<String, BoundField> boundFields, boolean blockInaccessible) {
super(boundFields);
constructor = ReflectionHelper.getCanonicalRecordConstructor(raw);
if (blockInaccessible) {
checkAccessible(null, constructor);
} else {
// Ensure the constructor is accessible
ReflectionHelper.makeAccessible(constructor);
}
String[] componentNames = ReflectionHelper.getRecordComponentNames(raw);
for (int i = 0; i < componentNames.length; i++) {
componentIndices.put(componentNames[i], i);
}
Class<?>[] parameterTypes = constructor.getParameterTypes();
// We need to ensure that we are passing non-null values to primitive fields in the constructor. To do this,
// we create an Object[] where all primitives are initialized to non-null values.
constructorArgsDefaults = new Object[parameterTypes.length];
for (int i = 0; i < parameterTypes.length; i++) {
// This will correctly be null for non-primitive types:
constructorArgsDefaults[i] = PRIMITIVE_DEFAULTS.get(parameterTypes[i]);
}
}
private static Map<Class<?>, Object> primitiveDefaults() {
Map<Class<?>, Object> zeroes = new HashMap<>();
zeroes.put(byte.class, (byte) 0);
zeroes.put(short.class, (short) 0);
zeroes.put(int.class, 0);
zeroes.put(long.class, 0L);
zeroes.put(float.class, 0F);
zeroes.put(double.class, 0D);
zeroes.put(char.class, '\0');
zeroes.put(boolean.class, false);
return zeroes;
}
@Override
Object[] createAccumulator() {
return constructorArgsDefaults.clone();
}
@Override
void readField(Object[] accumulator, JsonReader in, BoundField field) throws IOException {
// Obtain the component index from the name of the field backing it
Integer componentIndex = componentIndices.get(field.fieldName);
if (componentIndex == null) {
throw new IllegalStateException(
"Could not find the index in the constructor '" + ReflectionHelper.constructorToString(constructor) + "'"
+ " for field with name '" + field.fieldName + "',"
+ " unable to determine which argument in the constructor the field corresponds"
+ " to. This is unexpected behavior, as we expect the RecordComponents to have the"
+ " same names as the fields in the Java class, and that the order of the"
+ " RecordComponents is the same as the order of the canonical constructor parameters.");
}
field.readIntoArray(in, componentIndex, accumulator);
}
@Override
T finalize(Object[] accumulator) {
try {
return constructor.newInstance(accumulator);
} catch (IllegalAccessException e) {
throw ReflectionHelper.createExceptionForUnexpectedIllegalAccess(e);
}
// Note: InstantiationException should be impossible because record class is not abstract;
// IllegalArgumentException should not be possible unless a bad adapter returns objects of the wrong type
catch (InstantiationException | IllegalArgumentException e) {
throw new RuntimeException(
"Failed to invoke constructor '" + ReflectionHelper.constructorToString(constructor) + "'"
+ " with args " + Arrays.toString(accumulator), e);
}
catch (InvocationTargetException e) {
// TODO: JsonParseException ?
throw new RuntimeException(
"Failed to invoke constructor '" + ReflectionHelper.constructorToString(constructor) + "'"
+ " with args " + Arrays.toString(accumulator), e.getCause());
}
}
}
}