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 31a44e1a8a..5caeb107f0 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 @@ -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; @@ -94,22 +99,32 @@ private List getFieldNames(Field f) { return fieldNames; } - @Override public TypeAdapter create(Gson gson, final TypeToken type) { + @Override + public TypeAdapter create(Gson gson, final TypeToken type) { Class 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 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) { @@ -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()); @@ -138,16 +153,18 @@ private ReflectiveTypeAdapterFactory.BoundField createBoundField( @SuppressWarnings("unchecked") final TypeAdapter typeAdapter = (TypeAdapter) 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; } @@ -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); if (fieldValue != null || !isPrimitive) { if (blockInaccessible) { - checkAccessible(value, field); + checkAccessible(target, field); } - field.set(value, fieldValue); + field.set(target, fieldValue); } } }; } - private Map getBoundFields(Gson context, TypeToken type, Class raw, boolean blockInaccessible) { + private Map getBoundFields(Gson context, TypeToken type, Class raw, + boolean blockInaccessible, boolean isRecord) { Map result = new LinkedHashMap<>(); if (raw.isInterface()) { return result; @@ -197,8 +225,19 @@ private Map 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); } @@ -208,7 +247,7 @@ private Map 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; @@ -226,34 +265,76 @@ private Map 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 extends TypeAdapter { - private final ObjectConstructor constructor; - private final Map boundFields; + /** + * Base class for Adapters produced by this factory. + * + *

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 type of objects that this Adapter creates. + * @param type of accumulator used to build the deserialization result. + */ + public static abstract class Adapter extends TypeAdapter { + protected final Map boundFields; - Adapter(ObjectConstructor constructor, Map boundFields) { - this.constructor = constructor; + protected Adapter(Map 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(); @@ -263,7 +344,7 @@ public static final class Adapter extends TypeAdapter { if (field == null || !field.deserialized) { in.skipValue(); } else { - field.read(in, instance); + readField(accumulator, in, field); } } } catch (IllegalStateException e) { @@ -272,24 +353,111 @@ public static final class Adapter extends TypeAdapter { 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 extends Adapter { + private final ObjectConstructor constructor; + + FieldReflectionAdapter(ObjectConstructor constructor, Map 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 extends Adapter { + // The actual record constructor. + private final Constructor 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 componentIndices = new HashMap<>(); + + RecordAdapter(Class raw, Map boundFields) { + super(boundFields); + this.constructor = ReflectionHelper.getCanonicalRecordConstructor(raw); + // 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); } - } 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); + } } } } diff --git a/gson/src/main/java/com/google/gson/internal/reflect/ReflectionHelper.java b/gson/src/main/java/com/google/gson/internal/reflect/ReflectionHelper.java index 97230ff6f5..f55b30f5ba 100644 --- a/gson/src/main/java/com/google/gson/internal/reflect/ReflectionHelper.java +++ b/gson/src/main/java/com/google/gson/internal/reflect/ReflectionHelper.java @@ -2,26 +2,64 @@ import com.google.gson.JsonIOException; import com.google.gson.internal.GsonBuildConfig; -import java.lang.reflect.Constructor; -import java.lang.reflect.Field; + +import java.lang.reflect.*; public class ReflectionHelper { - private ReflectionHelper() { } + + private static final RecordHelper RECORD_HELPER; + + static { + RecordHelper instance; + try { + // Try to construct the RecordSupportedHelper, if this fails, records are not supported on this JVM. + instance = new RecordSupportedHelper(); + } catch (NoSuchMethodException e) { + instance = new RecordNotSupportedHelper(); + } + RECORD_HELPER = instance; + } + + private ReflectionHelper() {} /** - * Tries making the field accessible, wrapping any thrown exception in a - * {@link JsonIOException} with descriptive message. + * Tries making the field accessible, wrapping any thrown exception in a {@link JsonIOException} + * with descriptive message. * * @param field field to make accessible * @throws JsonIOException if making the field accessible fails */ public static void makeAccessible(Field field) throws JsonIOException { + makeAccessible("field '" + field.getDeclaringClass().getName() + "#" + field.getName() + "'", field); + } + + /** + * Tries making the constructor accessible, wrapping any thrown exception in a {@link JsonIOException} + * with descriptive message. + * + * @param constructor constructor to make accessible + * @throws JsonIOException if making the constructor accessible fails + */ + public static void makeAccessible(Constructor constructor) throws JsonIOException { + makeAccessible( + "constructor " + constructor + " in " + constructor.getDeclaringClass().getName(), + constructor + ); + } + + /** + * Internal implementation of making an {@link AccessibleObject} accessible. + * + * @param description describe what we are attempting to make accessible + * @param object the object that {@link AccessibleObject#setAccessible(boolean)} should be called on. + * @throws JsonIOException if making the object accessible fails + */ + private static void makeAccessible(String description, AccessibleObject object) throws JsonIOException { try { - field.setAccessible(true); + object.setAccessible(true); } catch (Exception exception) { - throw new JsonIOException("Failed making field '" + field.getDeclaringClass().getName() + "#" - + field.getName() + "' accessible; either change its visibility or write a custom " - + "TypeAdapter for its declaring type", exception); + throw new JsonIOException("Failed making " + description + "' accessible; either change its visibility " + + "or write a custom TypeAdapter for its declaring type", exception); } } @@ -65,10 +103,149 @@ public static String tryMakeAccessible(Constructor constructor) { } } - public static RuntimeException createExceptionForUnexpectedIllegalAccess(IllegalAccessException exception) { + /** If records are supported on the JVM, this is equivalent to a call to Class.isRecord() */ + public static boolean isRecord(Class raw) { + return RECORD_HELPER.isRecord(raw); + } + + public static String[] getRecordComponentNames(Class raw) { + return RECORD_HELPER.getRecordComponentNames(raw); + } + + /** Looks up the record accessor method that corresponds to the given record field */ + public static Method getAccessor(Class raw, Field field) { + return RECORD_HELPER.getAccessor(raw, field); + } + + public static Constructor getCanonicalRecordConstructor(Class raw) { + return RECORD_HELPER.getCanonicalRecordConstructor(raw); + } + + public static RuntimeException createExceptionForUnexpectedIllegalAccess( + IllegalAccessException exception) { throw new RuntimeException("Unexpected IllegalAccessException occurred (Gson " + GsonBuildConfig.VERSION + "). " + "Certain ReflectionAccessFilter features require Java >= 9 to work correctly. If you are not using " + "ReflectionAccessFilter, report this to the Gson maintainers.", exception); } + + + public static RuntimeException createExceptionForRecordReflectionException( + ReflectiveOperationException exception) { + throw new RuntimeException("Unexpected ReflectiveOperationException occurred " + + "(Gson " + GsonBuildConfig.VERSION + "). " + + "To support Java records, reflection is utilized to read out information " + + "about records. All these invocations happens after it is established " + + "that records exists in the JVM. This exception is unexpected behaviour.", + exception); + } + + /** + * Internal abstraction over reflection when Records are supported. + */ + private abstract static class RecordHelper { + abstract boolean isRecord(Class clazz); + + abstract String[] getRecordComponentNames(Class clazz); + + abstract Constructor getCanonicalRecordConstructor(Class raw); + + public abstract Method getAccessor(Class raw, Field field); + } + + private static class RecordSupportedHelper extends RecordHelper { + private final Method isRecord; + private final Method getRecordComponents; + private final Method getName; + private final Method getType; + private final Method getAccessor; + + private RecordSupportedHelper() throws NoSuchMethodException { + isRecord = Class.class.getMethod("isRecord"); + getRecordComponents = Class.class.getMethod("getRecordComponents"); + Class recordComponentType = getRecordComponents.getReturnType().getComponentType(); + getName = recordComponentType.getMethod("getName"); + getType = recordComponentType.getMethod("getType"); + getAccessor = recordComponentType.getMethod("getAccessor"); + } + + @Override + boolean isRecord(Class raw) { + try { + return Boolean.class.cast(isRecord.invoke(raw)).booleanValue(); + } catch (ReflectiveOperationException e) { + throw createExceptionForRecordReflectionException(e); + } + } + + @Override + String[] getRecordComponentNames(Class raw) { + try { + Object[] recordComponents = (Object[]) getRecordComponents.invoke(raw); + String[] componentNames = new String[recordComponents.length]; + for (int i = 0; i < recordComponents.length; i++) { + componentNames[i] = (String) getName.invoke(recordComponents[i]); + } + return componentNames; + } catch (ReflectiveOperationException e) { + throw createExceptionForRecordReflectionException(e); + } + } + + @Override + public Constructor getCanonicalRecordConstructor(Class raw) { + try { + Object[] recordComponents = (Object[]) getRecordComponents.invoke(raw); + Class[] recordComponentTypes = new Class[recordComponents.length]; + for (int i = 0; i < recordComponents.length; i++) { + recordComponentTypes[i] = (Class) getType.invoke(recordComponents[i]); + } + // Uses getDeclaredConstructor because implicit constructor has same visibility as record and might + // therefore not be public + return raw.getDeclaredConstructor(recordComponentTypes); + } catch (ReflectiveOperationException e) { + throw createExceptionForRecordReflectionException(e); + } + } + + @Override + public Method getAccessor(Class raw, Field field) { + try { + // Records consists of record components, each with a unique name, a corresponding field and accessor method + // with the same name. Ref.: https://docs.oracle.com/javase/specs/jls/se17/html/jls-8.html#jls-8.10.3 + return raw.getMethod(field.getName()); + } catch (ReflectiveOperationException e) { + throw createExceptionForRecordReflectionException(e); + } + } + } + + /** + * Instance used when records are not supported + */ + private static class RecordNotSupportedHelper extends RecordHelper { + + @Override + boolean isRecord(Class clazz) { + return false; + } + + @Override + String[] getRecordComponentNames(Class clazz) { + throw new UnsupportedOperationException( + "Records are not supported on this JVM, this method should not be called"); + } + + @Override + Constructor getCanonicalRecordConstructor(Class raw) { + throw new UnsupportedOperationException( + "Records are not supported on this JVM, this method should not be called"); + } + + @Override + public Method getAccessor(Class raw, Field field) { + throw new UnsupportedOperationException( + "Records are not supported on this JVM, this method should not be called"); + } + } } diff --git a/gson/src/test/java/com/google/gson/internal/bind/ReflectiveTypeAdapterFactoryTest.java b/gson/src/test/java/com/google/gson/internal/bind/ReflectiveTypeAdapterFactoryTest.java new file mode 100644 index 0000000000..08c92f805d --- /dev/null +++ b/gson/src/test/java/com/google/gson/internal/bind/ReflectiveTypeAdapterFactoryTest.java @@ -0,0 +1,83 @@ +package com.google.gson.internal.bind; + +import static org.junit.Assert.*; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.TypeAdapter; +import com.google.gson.internal.reflect.ReflectionHelperTest; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +import java.io.IOException; +import java.nio.file.attribute.GroupPrincipal; +import java.nio.file.attribute.UserPrincipal; +import java.security.Principal; +import org.junit.AssumptionViolatedException; +import org.junit.Before; +import org.junit.Test; + +public class ReflectiveTypeAdapterFactoryTest { + + // The class jdk.net.UnixDomainPrincipal is one of the few Record types that are included in the + // JDK. + // We use this to test serialization and deserialization of Record classes, so we do not need to + // have + // record support at the language level for these tests. This class was added in JDK 16. + Class unixDomainPrincipalClass; + + @Before + public void setUp() throws Exception { + try { + Class.forName("java.lang.Record"); + unixDomainPrincipalClass = Class.forName("jdk.net.UnixDomainPrincipal"); + } catch (ClassNotFoundException e) { + // Records not supported, ignore + throw new AssumptionViolatedException("java.lang.Record not supported"); + } + } + + @Test + public void testCustomAdapterForRecords() { + Gson gson = new Gson(); + TypeAdapter recordAdapter = gson.getAdapter(unixDomainPrincipalClass); + TypeAdapter defaultReflectionAdapter = gson.getAdapter(UserPrincipal.class); + assertNotEquals(recordAdapter.getClass(), defaultReflectionAdapter.getClass()); + } + + @Test + public void testSerializeRecords() throws ReflectiveOperationException { + Gson gson = + new GsonBuilder() + .registerTypeAdapter(UserPrincipal.class, new PrincipalTypeAdapter<>()) + .registerTypeAdapter(GroupPrincipal.class, new PrincipalTypeAdapter<>()) + .create(); + + UserPrincipal userPrincipal = gson.fromJson("\"user\"", UserPrincipal.class); + GroupPrincipal groupPrincipal = gson.fromJson("\"group\"", GroupPrincipal.class); + Object recordInstance = + unixDomainPrincipalClass + .getDeclaredConstructor(UserPrincipal.class, GroupPrincipal.class) + .newInstance(userPrincipal, groupPrincipal); + String serialized = gson.toJson(recordInstance); + Object deserializedRecordInstance = gson.fromJson(serialized, unixDomainPrincipalClass); + + assertEquals(recordInstance, deserializedRecordInstance); + assertEquals("{\"user\":\"user\",\"group\":\"group\"}", serialized); + } + + private static class PrincipalTypeAdapter extends TypeAdapter { + @Override + public void write(JsonWriter out, T principal) throws IOException { + out.value(principal.getName()); + } + + @Override + public T read(JsonReader in) throws IOException { + final String name = in.nextString(); + // This type adapter is only used for Group and User Principal, both of which are implemented by PrincipalImpl. + @SuppressWarnings("unchecked") + T principal = (T) new ReflectionHelperTest.PrincipalImpl(name); + return principal; + } + } +} diff --git a/gson/src/test/java/com/google/gson/internal/reflect/ReflectionHelperTest.java b/gson/src/test/java/com/google/gson/internal/reflect/ReflectionHelperTest.java new file mode 100644 index 0000000000..7d0c9833f2 --- /dev/null +++ b/gson/src/test/java/com/google/gson/internal/reflect/ReflectionHelperTest.java @@ -0,0 +1,90 @@ +package com.google.gson.internal.reflect; + +import static org.junit.Assert.*; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.nio.file.attribute.GroupPrincipal; +import java.nio.file.attribute.UserPrincipal; +import java.util.Objects; +import org.junit.AssumptionViolatedException; +import org.junit.Before; +import org.junit.Test; + +public class ReflectionHelperTest { + + @Before + public void setUp() throws Exception { + try { + Class.forName("java.lang.Record"); + } catch (ClassNotFoundException e) { + // Records not supported, ignore + throw new AssumptionViolatedException("java.lang.Record not supported"); + } + } + + @Test + public void testJava17Record() throws ClassNotFoundException { + Class unixDomainPrincipalClass = Class.forName("jdk.net.UnixDomainPrincipal"); + // UnixDomainPrincipal is a record + assertTrue(ReflectionHelper.isRecord(unixDomainPrincipalClass)); + // with 2 components + assertArrayEquals( + new String[] {"user", "group"}, + ReflectionHelper.getRecordComponentNames(unixDomainPrincipalClass)); + // Check canonical constructor + Constructor constructor = + ReflectionHelper.getCanonicalRecordConstructor(unixDomainPrincipalClass); + assertNotNull(constructor); + assertArrayEquals( + new Class[] {UserPrincipal.class, GroupPrincipal.class}, + constructor.getParameterTypes()); + } + + @Test + public void testJava17RecordAccessors() throws ReflectiveOperationException { + // Create an instance of UnixDomainPrincipal, using our custom implementation of UserPrincipal, + // and GroupPrincipal. Then attempt to access each component of the record using our accessor + // methods. + Class unixDomainPrincipalClass = Class.forName("jdk.net.UnixDomainPrincipal"); + Object unixDomainPrincipal = + ReflectionHelper.getCanonicalRecordConstructor(unixDomainPrincipalClass) + .newInstance(new PrincipalImpl("user"), new PrincipalImpl("group")); + for (String componentName : + ReflectionHelper.getRecordComponentNames(unixDomainPrincipalClass)) { + Field componentField = unixDomainPrincipalClass.getDeclaredField(componentName); + Method accessor = ReflectionHelper.getAccessor(unixDomainPrincipalClass, componentField); + Object principal = accessor.invoke(unixDomainPrincipal); + + assertEquals(new PrincipalImpl(componentName), principal); + } + } + + /** Implementation of {@link UserPrincipal} and {@link GroupPrincipal} just for record tests. */ + public static class PrincipalImpl implements UserPrincipal, GroupPrincipal { + private final String name; + + public PrincipalImpl(String name) { + this.name = name; + } + + @Override + public String getName() { + return name; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PrincipalImpl principal = (PrincipalImpl) o; + return Objects.equals(name, principal.name); + } + + @Override + public int hashCode() { + return Objects.hash(name); + } + } +}