diff options
Diffstat (limited to 'gson/src/main/java/com/google/gson/internal/bind/ReflectiveTypeAdapterFactory.java')
-rw-r--r-- | gson/src/main/java/com/google/gson/internal/bind/ReflectiveTypeAdapterFactory.java | 347 |
1 files changed, 292 insertions, 55 deletions
diff --git a/gson/src/main/java/com/google/gson/internal/bind/ReflectiveTypeAdapterFactory.java b/gson/src/main/java/com/google/gson/internal/bind/ReflectiveTypeAdapterFactory.java index 95d01ace..5ddac50e 100644 --- a/gson/src/main/java/com/google/gson/internal/bind/ReflectiveTypeAdapterFactory.java +++ b/gson/src/main/java/com/google/gson/internal/bind/ReflectiveTypeAdapterFactory.java @@ -19,6 +19,7 @@ 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; @@ -38,11 +39,18 @@ 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; @@ -88,88 +96,137 @@ public final class ReflectiveTypeAdapterFactory implements TypeAdapterFactory { List<String> fieldNames = new ArrayList<>(alternates.length + 1); fieldNames.add(serializedName); - for (String alternate : alternates) { - fieldNames.add(alternate); - } + Collections.addAll(fieldNames, alternates); return fieldNames; } - @Override public <T> TypeAdapter<T> create(Gson gson, final TypeToken<T> type) { + @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); + 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."); + 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 Adapter<>(constructor, getBoundFields(gson, type, raw, blockInaccessible)); + return new FieldReflectionAdapter<>(constructor, getBoundFields(gson, type, raw, blockInaccessible, false)); } - private static void checkAccessible(Object object, Field field) { - if (!ReflectionAccessFilterHelper.canAccess(field, Modifier.isStatic(field.getModifiers()) ? null : object)) { - throw new JsonIOException("Field '" + field.getDeclaringClass().getName() + "#" - + field.getName() + "' is not accessible and ReflectionAccessFilter does not " - + "permit making it accessible. Register a TypeAdapter for the declaring type " - + "or adjust the access filter."); + 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 ReflectiveTypeAdapterFactory.BoundField createBoundField( - final Gson context, final Field field, final String name, + 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()); - // special casing primitives here saves ~5% on Android... + + 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); - final TypeAdapter<?> typeAdapter = mapped; - return new ReflectiveTypeAdapterFactory.BoundField(name, serialize, deserialize) { - @SuppressWarnings({"unchecked", "rawtypes"}) // the type adapter and field type always agree - @Override void write(JsonWriter writer, Object value) + @SuppressWarnings("unchecked") + final TypeAdapter<Object> typeAdapter = (TypeAdapter<Object>) mapped; + return new ReflectiveTypeAdapterFactory.BoundField(name, field.getName(), serialize, deserialize) { + @Override void write(JsonWriter writer, Object source) throws IOException, IllegalAccessException { if (!serialized) return; if (blockInaccessible) { - checkAccessible(value, field); + 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 = field.get(value); - if (fieldValue == value) { + 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 t = jsonAdapterPresent ? typeAdapter - : new TypeAdapterRuntimeTypeWrapper(context, typeAdapter, fieldType.getType()); + TypeAdapter<Object> t = jsonAdapterPresent ? typeAdapter + : new TypeAdapterRuntimeTypeWrapper<>(context, typeAdapter, fieldType.getType()); t.write(writer, fieldValue); } - @Override void read(JsonReader reader, Object value) + + @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(value, field); + 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(value, fieldValue); + field.set(target, fieldValue); } } }; } - private Map<String, BoundField> getBoundFields(Gson context, TypeToken<?> type, Class<?> raw, boolean blockInaccessible) { + 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; @@ -184,9 +241,9 @@ public final class ReflectiveTypeAdapterFactory implements TypeAdapterFactory { 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."); + 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; } @@ -197,9 +254,36 @@ public final class ReflectiveTypeAdapterFactory implements TypeAdapterFactory { 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 - if (!blockInaccessible) { + // 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()); @@ -208,7 +292,7 @@ public final class ReflectiveTypeAdapterFactory implements TypeAdapterFactory { 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, 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; @@ -226,34 +310,75 @@ public final class ReflectiveTypeAdapterFactory implements TypeAdapterFactory { static abstract class BoundField { final String name; + /** Name of the underlying field */ + final String fieldName; final boolean serialized; final boolean deserialized; - protected BoundField(String name, boolean serialized, boolean deserialized) { + protected BoundField(String name, String fieldName, boolean serialized, boolean deserialized) { this.name = name; + this.fieldName = fieldName; this.serialized = serialized; this.deserialized = deserialized; } - abstract void write(JsonWriter writer, Object value) throws IOException, IllegalAccessException; - abstract void read(JsonReader reader, Object value) throws IOException, IllegalAccessException; + + /** 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; } - public static final class Adapter<T> extends TypeAdapter<T> { - private final ObjectConstructor<T> constructor; - private final Map<String, BoundField> boundFields; + /** + * 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(ObjectConstructor<T> constructor, Map<String, BoundField> boundFields) { - this.constructor = constructor; + Adapter(Map<String, BoundField> boundFields) { this.boundFields = boundFields; } - @Override public T read(JsonReader in) throws IOException { + @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; } - T instance = constructor.construct(); + A accumulator = createAccumulator(); try { in.beginObject(); @@ -263,7 +388,7 @@ public final class ReflectiveTypeAdapterFactory implements TypeAdapterFactory { if (field == null || !field.deserialized) { in.skipValue(); } else { - field.read(in, instance); + readField(accumulator, in, field); } } } catch (IllegalStateException e) { @@ -272,24 +397,136 @@ public final class ReflectiveTypeAdapterFactory implements TypeAdapterFactory { throw ReflectionHelper.createExceptionForUnexpectedIllegalAccess(e); } in.endObject(); - return instance; + return finalize(accumulator); } - @Override public void write(JsonWriter out, T value) throws IOException { - if (value == null) { - out.nullValue(); - return; + /** 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); } - out.beginObject(); + 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 { - for (BoundField boundField : boundFields.values()) { - boundField.write(out, value); - } + return constructor.newInstance(accumulator); } catch (IllegalAccessException e) { throw ReflectionHelper.createExceptionForUnexpectedIllegalAccess(e); } - out.endObject(); + // 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()); + } } } } |