Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Java Records when present in JVM. #2201

Merged
merged 4 commits into from Oct 11, 2022
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -38,12 +38,17 @@
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.lang.reflect.Array;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

Expand Down Expand Up @@ -94,22 +99,32 @@ private List<String> getFieldNames(Field f) {
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)) {
return new RecordAdapter<>(raw, getBoundFields(gson, type, raw, true, true));
}

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) {
Expand All @@ -122,7 +137,7 @@ private static void checkAccessible(Object object, Field field) {
}

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());
Expand All @@ -138,16 +153,18 @@ private ReflectiveTypeAdapterFactory.BoundField createBoundField(

@SuppressWarnings("unchecked")
final TypeAdapter<Object> typeAdapter = (TypeAdapter<Object>) mapped;
return new ReflectiveTypeAdapterFactory.BoundField(name, serialize, deserialize) {
@Override void write(JsonWriter writer, Object value)
throws IOException, IllegalAccessException {
return new ReflectiveTypeAdapterFactory.BoundField(name, field.getName(), serialize, deserialize) {
@Override void write(JsonWriter writer, Object source)
throws IOException, ReflectiveOperationException {
if (!serialized) return;
if (blockInaccessible) {
checkAccessible(value, field);
if (blockInaccessible && accessor == null) {
checkAccessible(source, field);
}

Object fieldValue = field.get(value);
if (fieldValue == value) {
Object fieldValue = (accessor != null)
? accessor.invoke(source)
: field.get(source);
if (fieldValue == source) {
// avoid direct recursion
return;
}
Expand All @@ -156,20 +173,31 @@ private ReflectiveTypeAdapterFactory.BoundField createBoundField(
: 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 {
Object fieldValue = typeAdapter.read(reader);
if (fieldValue != null || !isPrimitive) {
target[index] = fieldValue;
}
}

@Override
void readIntoField(JsonReader reader, Object target)
throws IOException, IllegalAccessException {
Object fieldValue = typeAdapter.read(reader);
staale marked this conversation as resolved.
Show resolved Hide resolved
if (fieldValue != null || !isPrimitive) {
if (blockInaccessible) {
checkAccessible(value, field);
checkAccessible(target, field);
}
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;
Expand Down Expand Up @@ -197,8 +225,19 @@ private Map<String, BoundField> getBoundFields(Gson context, TypeToken<?> type,
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
// If there is a static field on a record, there will not be an accessor. Instead we will use the default
// field logic for dealing with statics.
Method accessor = null;
if (isRecord && !Modifier.isStatic(field.getModifiers())) {
accessor = ReflectionHelper.getAccessor(raw, field);
}

// If blockInaccessible, skip and perform access check later
// If blockInaccessible, skip and perform access check later. When constructing a BoundedField for a Record
// field, blockInaccessible is always true, thus makeAccessible will never get called. This is not an issue
// though, as we will use the accessor method instead for reading record fields, and the constructor for
// writing fields.
if (!blockInaccessible) {
ReflectionHelper.makeAccessible(field);
}
Expand All @@ -208,7 +247,7 @@ private Map<String, BoundField> getBoundFields(Gson context, TypeToken<?> type,
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;
Expand All @@ -226,34 +265,76 @@ private Map<String, BoundField> getBoundFields(Gson context, TypeToken<?> type,

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, ReflectiveOperationException;

/** 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;

/** 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.
*/
abstract static class Adapter<T, A> extends TypeAdapter<T> {
staale marked this conversation as resolved.
Show resolved Hide resolved
protected final Map<String, BoundField> boundFields;

Adapter(ObjectConstructor<T> constructor, Map<String, BoundField> boundFields) {
this.constructor = constructor;
protected 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);
} catch (ReflectiveOperationException e) {
throw ReflectionHelper.createExceptionForRecordReflectionException(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();
Expand All @@ -263,7 +344,7 @@ public static final class Adapter<T> extends TypeAdapter<T> {
if (field == null || !field.deserialized) {
in.skipValue();
} else {
field.read(in, instance);
readField(accumulator, in, field);
}
}
} catch (IllegalStateException e) {
Expand All @@ -272,24 +353,111 @@ public static final class Adapter<T> extends TypeAdapter<T> {
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 BoundedField 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[]> {
// The actual record constructor.
private final Constructor<? super 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<? super T> raw, Map<String, BoundField> boundFields) {
super(boundFields);
this.constructor = ReflectionHelper.getCanonicalRecordConstructor(raw);
staale marked this conversation as resolved.
Show resolved Hide resolved
// Ensure the constructor is accessible
ReflectionHelper.makeAccessible(this.constructor);

String[] componentNames = ReflectionHelper.getRecordComponentNames(raw);
for (int i = 0; i < componentNames.length; i++) {
componentIndices.put(componentNames[i], i);
}
Class<?>[] parameterTypes = constructor.getParameterTypes();

out.beginObject();
try {
for (BoundField boundField : boundFields.values()) {
boundField.write(out, value);
// 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++) {
if (parameterTypes[i].isPrimitive()) {
// Voodoo magic, we create a new instance of this primitive type using reflection via an
// array. The array has 1 element, that of course will be initialized to the primitives
// default value. We then retrieve this value back from the array to get the properly
// initialized default value for the primitve type.
constructorArgsDefaults[i] = Array.get(Array.newInstance(parameterTypes[i], 1), 0);
staale marked this conversation as resolved.
Show resolved Hide resolved
}
} catch (IllegalAccessException e) {
throw ReflectionHelper.createExceptionForUnexpectedIllegalAccess(e);
}
out.endObject();
}

@Override
Object[] createAccumulator() {
return constructorArgsDefaults.clone();
}

@Override
void readField(Object[] accumulator, JsonReader in, BoundField field) throws IOException {
Integer fieldIndex = componentIndices.get(field.fieldName);
if (fieldIndex == null) {
throw new IllegalStateException(
"Could not find the index in the constructor "
+ constructor
+ " for field with name "
+ field.name
+ ", unable to determine which argument in the constructor the field corresponds"
+ " to. This is unexpected behaviour, 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 arguments.");
}
field.readIntoArray(in, fieldIndex, accumulator);
}

@Override
@SuppressWarnings("unchecked")
T finalize(Object[] accumulator) {
try {
return (T) constructor.newInstance(accumulator);
} catch (ReflectiveOperationException e) {
throw new RuntimeException(
"Failed to invoke " + constructor + " with args " + Arrays.toString(accumulator), e);
}
}
}
}