From 01a34b6be9093273d3e8ac41851d5e979cce2411 Mon Sep 17 00:00:00 2001 From: zml Date: Wed, 14 Dec 2022 19:22:02 -0800 Subject: [PATCH] feat(core): Optionally use a user-provided Lookup when building object mappers This allows Configurate to read fields in otherwise closed modules. --- ...powered.configurate.build.component.gradle | 2 +- .../build/ConfigurateExtension.groovy | 2 +- core/build.gradle | 2 +- .../objectmapping/FieldDiscoverer.java | 36 ++++- .../configurate/objectmapping/LookupShim.java | 30 ++++ .../objectmapping/ObjectFieldDiscoverer.java | 143 +++++++++++++----- .../objectmapping/ObjectMapper.java | 13 ++ .../ObjectMapperFactoryImpl.java | 23 ++- .../objectmapping/RecordFieldDiscoverer.java | 7 +- .../configurate/util/CheckedBiFunction.java | 63 ++++++++ .../objectmapping/RecordFieldDiscoverer.java | 47 ++++-- ...ava => DisabledObjectFieldDiscoverer.java} | 82 ++++++---- .../configurate/objectmapping/LookupShim.java | 30 ++++ .../configurate/kotlin/ObjectMapping.kt | 4 +- 14 files changed, 404 insertions(+), 80 deletions(-) create mode 100644 core/src/main/java/org/spongepowered/configurate/objectmapping/LookupShim.java create mode 100644 core/src/main/java/org/spongepowered/configurate/util/CheckedBiFunction.java rename core/src/main/java9/org/spongepowered/configurate/objectmapping/{ObjectFieldDiscoverer.java => DisabledObjectFieldDiscoverer.java} (66%) create mode 100644 core/src/main/java9/org/spongepowered/configurate/objectmapping/LookupShim.java diff --git a/build-logic/src/main/groovy/org.spongepowered.configurate.build.component.gradle b/build-logic/src/main/groovy/org.spongepowered.configurate.build.component.gradle index 6844c3fcc..86985603f 100644 --- a/build-logic/src/main/groovy/org.spongepowered.configurate.build.component.gradle +++ b/build-logic/src/main/groovy/org.spongepowered.configurate.build.component.gradle @@ -79,7 +79,7 @@ tasks.withType(Javadoc).configureEach { options.links( "https://lightbend.github.io/config/latest/api/", "https://fasterxml.github.io/jackson-core/javadoc/2.10/", - "https://checkerframework.org/api/" + // "https://checkerframework.org/api/" ) options.linkSource() } diff --git a/build-logic/src/main/groovy/org/spongepowered/configurate/build/ConfigurateExtension.groovy b/build-logic/src/main/groovy/org/spongepowered/configurate/build/ConfigurateExtension.groovy index 616b217b8..663f9b2c2 100644 --- a/build-logic/src/main/groovy/org/spongepowered/configurate/build/ConfigurateExtension.groovy +++ b/build-logic/src/main/groovy/org/spongepowered/configurate/build/ConfigurateExtension.groovy @@ -32,7 +32,7 @@ class ConfigurateExtension { options.links( "https://lightbend.github.io/config/latest/api/", "https://fasterxml.github.io/jackson-core/javadoc/2.10/", - "https://checkerframework.org/api/", + // "https://checkerframework.org/api/", "https://www.javadoc.io/doc/io.leangen.geantyref/geantyref/1.3.11/" ) diff --git a/core/build.gradle b/core/build.gradle index bdec1941e..e289f8f36 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -36,7 +36,7 @@ sourceSets { main { multirelease { alternateVersions( - // 9, // VarHandles // TODO: temporarily disabled, cannot write final fields + 9, // private Lookup, ~~VarHandles~~ // TODO: handles temporarily disabled, cannot write final fields 10, // immutable collections 16 // FieldDiscoverer for records ) diff --git a/core/src/main/java/org/spongepowered/configurate/objectmapping/FieldDiscoverer.java b/core/src/main/java/org/spongepowered/configurate/objectmapping/FieldDiscoverer.java index d4e1235a5..37e31fce0 100644 --- a/core/src/main/java/org/spongepowered/configurate/objectmapping/FieldDiscoverer.java +++ b/core/src/main/java/org/spongepowered/configurate/objectmapping/FieldDiscoverer.java @@ -22,6 +22,7 @@ import org.spongepowered.configurate.serialize.SerializationException; import org.spongepowered.configurate.util.CheckedFunction; +import java.lang.invoke.MethodHandles; import java.lang.reflect.AnnotatedElement; import java.lang.reflect.AnnotatedType; import java.util.function.Supplier; @@ -127,6 +128,31 @@ static FieldDiscoverer emptyConstructorObject() { return ObjectFieldDiscoverer.EMPTY_CONSTRUCTOR_INSTANCE; } + /** + * Inspect the {@code target} type for fields to be supplied to + * the {@code collector}. + * + *

If the target type is handleable, a non-null value must be returned. + * Fields can only be collected from one source at the moment, so if the + * instance factory is null any discovered fields will be discarded.

+ * + * @param target type to inspect + * @param collector collector for discovered fields. + * @param lookup a lookup for reflective access to access-controlled members + * @param object type + * @return a factory for handling the construction of object instances, or + * {@code null} if {@code target} is not of a handleable type. + * @throws SerializationException if any fields have invalid data + * @since 4.2.0 + */ + default @Nullable InstanceFactory discover( + final AnnotatedType target, + final FieldCollector collector, + final MethodHandles.@Nullable Lookup lookup + ) throws SerializationException { + return this.discover(target, collector); + } + /** * Inspect the {@code target} type for fields to be supplied to * the {@code collector}. @@ -142,8 +168,16 @@ static FieldDiscoverer emptyConstructorObject() { * {@code null} if {@code target} is not of a handleable type. * @throws SerializationException if any fields have invalid data * @since 4.0.0 + * @deprecated for removal since 4.2.0, use the module-aware + * {@link #discover(AnnotatedType, FieldCollector, MethodHandles.Lookup)} instead */ - @Nullable InstanceFactory discover(AnnotatedType target, FieldCollector collector) throws SerializationException; + @Deprecated + default @Nullable InstanceFactory discover( + final AnnotatedType target, + final FieldCollector collector + ) throws SerializationException { + return null; + } /** * A handler that controls the deserialization process for an object. diff --git a/core/src/main/java/org/spongepowered/configurate/objectmapping/LookupShim.java b/core/src/main/java/org/spongepowered/configurate/objectmapping/LookupShim.java new file mode 100644 index 000000000..3713436d0 --- /dev/null +++ b/core/src/main/java/org/spongepowered/configurate/objectmapping/LookupShim.java @@ -0,0 +1,30 @@ +/* + * Configurate + * Copyright (C) zml and Configurate contributors + * + * 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 org.spongepowered.configurate.objectmapping; + +import java.lang.invoke.MethodHandles; + +final class LookupShim { + + private LookupShim() { + } + + static MethodHandles.Lookup privateLookupIn(final Class clazz, final MethodHandles.Lookup existingLookup) throws IllegalAccessException { + return existingLookup.in(clazz); + } + +} diff --git a/core/src/main/java/org/spongepowered/configurate/objectmapping/ObjectFieldDiscoverer.java b/core/src/main/java/org/spongepowered/configurate/objectmapping/ObjectFieldDiscoverer.java index 2f08a7d12..b8daf464c 100644 --- a/core/src/main/java/org/spongepowered/configurate/objectmapping/ObjectFieldDiscoverer.java +++ b/core/src/main/java/org/spongepowered/configurate/objectmapping/ObjectFieldDiscoverer.java @@ -22,38 +22,50 @@ import org.checkerframework.checker.nullness.qual.Nullable; import org.spongepowered.configurate.serialize.SerializationException; +import org.spongepowered.configurate.util.CheckedBiFunction; import org.spongepowered.configurate.util.CheckedFunction; import org.spongepowered.configurate.util.Types; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; import java.lang.reflect.AnnotatedType; -import java.lang.reflect.Constructor; import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Modifier; import java.util.HashMap; import java.util.Map; import java.util.function.Supplier; -class ObjectFieldDiscoverer implements FieldDiscoverer> { +class ObjectFieldDiscoverer implements FieldDiscoverer> { - static final ObjectFieldDiscoverer EMPTY_CONSTRUCTOR_INSTANCE = new ObjectFieldDiscoverer(type -> { + private static final MethodHandles.Lookup OWN_LOOKUP = MethodHandles.lookup(); + + static final ObjectFieldDiscoverer EMPTY_CONSTRUCTOR_INSTANCE = new ObjectFieldDiscoverer((type, lookup) -> { try { - final Constructor constructor; - constructor = erase(type.getType()).getDeclaredConstructor(); - constructor.setAccessible(true); + final MethodHandle constructor; + final Class erased = erase(type.getType()); + constructor = LookupShim.privateLookupIn(erased, lookup == null ? OWN_LOOKUP : lookup) + .findConstructor(erased, MethodType.methodType(void.class)); return () -> { try { - return constructor.newInstance(); - } catch (final InstantiationException | IllegalAccessException | InvocationTargetException e) { - throw new RuntimeException(e); + return constructor.invoke(); + } catch (final RuntimeException ex) { + throw ex; + } catch (final Throwable thr) { + throw new RuntimeException(thr); } }; - } catch (final NoSuchMethodException e) { + } catch (final NoSuchMethodException | IllegalAccessException e) { return null; } }, "Objects must have a zero-argument constructor to be able to create new instances", false); - private final CheckedFunction, SerializationException> instanceFactory; + private final CheckedBiFunction< + AnnotatedType, + MethodHandles.@Nullable Lookup, + @Nullable Supplier, + SerializationException + > instanceFactory; private final String instanceUnavailableErrorMessage; private final boolean requiresInstanceCreation; @@ -61,6 +73,14 @@ class ObjectFieldDiscoverer implements FieldDiscoverer> { final CheckedFunction, SerializationException> instanceFactory, final @Nullable String instanceUnavailableErrorMessage, final boolean requiresInstanceCreation + ) { + this((type, lookup) -> instanceFactory.apply(type), instanceUnavailableErrorMessage, requiresInstanceCreation); + } + + ObjectFieldDiscoverer( + final CheckedBiFunction, SerializationException> instanceFactory, + final @Nullable String instanceUnavailableErrorMessage, + final boolean requiresInstanceCreation ) { this.instanceFactory = instanceFactory; if (instanceUnavailableErrorMessage == null) { @@ -72,14 +92,17 @@ class ObjectFieldDiscoverer implements FieldDiscoverer> { } @Override - public @Nullable InstanceFactory> discover(final AnnotatedType target, - final FieldCollector, V> collector) throws SerializationException { + public @Nullable InstanceFactory> discover( + final AnnotatedType target, + final FieldCollector, V> collector, + final MethodHandles.@Nullable Lookup lookup + ) throws SerializationException { final Class clazz = erase(target.getType()); if (clazz.isInterface()) { throw new SerializationException(target.getType(), "ObjectMapper can only work with concrete types"); } - final @Nullable Supplier maker = this.instanceFactory.apply(target); + final @Nullable Supplier maker = this.instanceFactory.apply(target, lookup); if (maker == null && this.requiresInstanceCreation) { return null; } @@ -87,7 +110,7 @@ class ObjectFieldDiscoverer implements FieldDiscoverer> { AnnotatedType collectType = target; Class collectClass = clazz; while (true) { - collectFields(collectType, collector); + collectFields(collectType, collector, lookup); collectClass = collectClass.getSuperclass(); if (collectClass.equals(Object.class)) { break; @@ -95,37 +118,39 @@ class ObjectFieldDiscoverer implements FieldDiscoverer> { collectType = getExactSuperType(collectType, collectClass); } - return new MutableInstanceFactory>() { + return new MutableInstanceFactory>() { @Override - public Map begin() { + public Map begin() { return new HashMap<>(); } @Override - public void complete(final Object instance, final Map intermediate) throws SerializationException { - for (final Map.Entry entry : intermediate.entrySet()) { + public void complete(final Object instance, final Map intermediate) throws SerializationException { + for (final Map.Entry entry : intermediate.entrySet()) { try { // Handle implicit field initialization by detecting any existing information in the object if (entry.getValue() instanceof ImplicitProvider) { final @Nullable Object implicit = ((ImplicitProvider) entry.getValue()).provider.get(); if (implicit != null) { - if (entry.getKey().get(instance) == null) { - entry.getKey().set(instance, implicit); + if (entry.getKey().getter.invoke(instance) == null) { + entry.getKey().setter.invoke(instance, implicit); } } } else { - entry.getKey().set(instance, entry.getValue()); + entry.getKey().setter.invoke(instance, entry.getValue()); } } catch (final IllegalAccessException e) { throw new SerializationException(target.getType(), e); + } catch (final Throwable thr) { + throw new SerializationException(target.getType(), "An unexpected error occurred while trying to set a field", thr); } } } @Override - public Object complete(final Map intermediate) throws SerializationException { - final Object instance = maker == null ? null : maker.get(); + public Object complete(final Map intermediate) throws SerializationException { + final @Nullable Object instance = maker == null ? null : maker.get(); if (instance == null) { throw new SerializationException(target.getType(), ObjectFieldDiscoverer.this.instanceUnavailableErrorMessage); } @@ -141,22 +166,70 @@ public boolean canCreateInstances() { }; } - private void collectFields(final AnnotatedType clazz, final FieldCollector, ?> fieldMaker) { + private void collectFields( + final AnnotatedType clazz, + final FieldCollector, V> fieldMaker, + final MethodHandles.@Nullable Lookup lookup + ) throws SerializationException { for (final Field field : erase(clazz.getType()).getDeclaredFields()) { if ((field.getModifiers() & (Modifier.STATIC | Modifier.TRANSIENT)) != 0) { continue; } - field.setAccessible(true); final AnnotatedType fieldType = getFieldType(field, clazz); - fieldMaker.accept(field.getName(), fieldType, Types.combinedAnnotations(fieldType, field), - (intermediate, val, implicitProvider) -> { - if (val != null) { - intermediate.put(field, val); - } else { - intermediate.put(field, new ImplicitProvider(implicitProvider)); - } - }, field::get); + final FieldData.Deserializer> deserializer; + final CheckedFunction serializer; + final FieldHandles handles; + try { + if (lookup != null) { + handles = new FieldHandles(field, lookup); + } else { + handles = new FieldHandles(field); + } + } catch (final IllegalAccessException ex) { + throw new SerializationException(fieldType, ex); + } + deserializer = (intermediate, val, implicitProvider) -> { + if (val != null) { + intermediate.put(handles, val); + } else { + intermediate.put(handles, new ImplicitProvider(implicitProvider)); + } + }; + serializer = inst -> { + try { + return handles.getter.invoke(inst); + } catch (final Exception ex) { + throw ex; + } catch (final Throwable thr) { + throw new Exception(thr); + } + }; + fieldMaker.accept( + field.getName(), + fieldType, + Types.combinedAnnotations(fieldType, field), + deserializer, + serializer + ); + } + } + + static class FieldHandles { + final MethodHandle getter; + final MethodHandle setter; + + FieldHandles(final Field field) throws IllegalAccessException { + field.setAccessible(true); + final MethodHandles.Lookup lookup = MethodHandles.publicLookup(); + + this.getter = lookup.unreflectGetter(field); + this.setter = lookup.unreflectSetter(field); + } + + FieldHandles(final Field field, final MethodHandles.Lookup lookup) throws IllegalAccessException { + this.getter = lookup.unreflectGetter(field); + this.setter = lookup.unreflectSetter(field); } } diff --git a/core/src/main/java/org/spongepowered/configurate/objectmapping/ObjectMapper.java b/core/src/main/java/org/spongepowered/configurate/objectmapping/ObjectMapper.java index 5f8d2e5f1..df3c5c117 100644 --- a/core/src/main/java/org/spongepowered/configurate/objectmapping/ObjectMapper.java +++ b/core/src/main/java/org/spongepowered/configurate/objectmapping/ObjectMapper.java @@ -28,6 +28,7 @@ import org.spongepowered.configurate.util.NamingScheme; import java.lang.annotation.Annotation; +import java.lang.invoke.MethodHandles; import java.lang.reflect.Type; import java.util.List; @@ -361,6 +362,18 @@ default Builder addConstraint(final Class definition, */ Builder addPostProcessor(PostProcessor.Factory factory); + /** + * Set a custom lookup to access fields. + * + *

This allows Configurate to reflectively modify classes + * without opening them for reflective access.

+ * + * @param lookup the lookup to use + * @return this builder + * @since 4.2.0 + */ + Builder lookup(MethodHandles.Lookup lookup); + /** * Create a new factory using the current configuration. * diff --git a/core/src/main/java/org/spongepowered/configurate/objectmapping/ObjectMapperFactoryImpl.java b/core/src/main/java/org/spongepowered/configurate/objectmapping/ObjectMapperFactoryImpl.java index 2426c87e4..a749bd6c9 100644 --- a/core/src/main/java/org/spongepowered/configurate/objectmapping/ObjectMapperFactoryImpl.java +++ b/core/src/main/java/org/spongepowered/configurate/objectmapping/ObjectMapperFactoryImpl.java @@ -43,6 +43,7 @@ import org.spongepowered.configurate.util.NamingSchemes; import java.lang.annotation.Annotation; +import java.lang.invoke.MethodHandles; import java.lang.reflect.AnnotatedElement; import java.lang.reflect.AnnotatedType; import java.lang.reflect.Modifier; @@ -74,6 +75,7 @@ protected boolean removeEldestEntry(final Map.Entry> eldes private final Map, List>>> constraints; private final Map, List>>> processors; private final List postProcessors; + private final MethodHandles.@Nullable Lookup lookup; ObjectMapperFactoryImpl(final Builder builder) { this.resolverFactories = new ArrayList<>(builder.resolvers); @@ -104,6 +106,8 @@ protected boolean removeEldestEntry(final Map.Entry> eldes this.postProcessors = new ArrayList<>(builder.postProcessors); Collections.reverse(this.postProcessors); + + this.lookup = builder.lookup; } @Override @@ -136,8 +140,16 @@ private ObjectMapper computeMapper(final Type type) throws SerializationExcep private @Nullable ObjectMapper newMapper(final Type type, final FieldDiscoverer discoverer) throws SerializationException { final List> fields = new ArrayList<>(); - final FieldDiscoverer.@Nullable InstanceFactory candidate = discoverer.discover(annotate(type), - (name, fieldType, container, deserializer, serializer) -> makeData(fields, name, fieldType, container, deserializer, serializer)); + final FieldDiscoverer.@Nullable InstanceFactory candidate; + try { + candidate = discoverer.discover( + annotate(type), + (name, fieldType, container, deserializer, serializer) -> makeData(fields, name, fieldType, container, deserializer, serializer), + this.lookup == null ? null : LookupShim.privateLookupIn(erase(type), this.lookup) + ); + } catch (final IllegalAccessException ex) { + throw new SerializationException(type, "Could not create lookup in target class", ex); + } if (candidate == null) { return null; @@ -358,6 +370,7 @@ static class Builder implements ObjectMapper.Factory.Builder { private final List>> constraints = new ArrayList<>(); private final List>> processors = new ArrayList<>(); private final List postProcessors = new ArrayList<>(); + private MethodHandles.@Nullable Lookup lookup; @Override public ObjectMapper.Factory.Builder defaultNamingScheme(final NamingScheme scheme) { @@ -397,6 +410,12 @@ public Builder addPostProcessor(final PostProcessor.Factory factory) { return this; } + @Override + public ObjectMapper.Factory.Builder lookup(final MethodHandles.Lookup lookup) { + this.lookup = requireNonNull(lookup, "lookup"); + return this; + } + @Override public ObjectMapper.Factory build() { return new ObjectMapperFactoryImpl(this); diff --git a/core/src/main/java/org/spongepowered/configurate/objectmapping/RecordFieldDiscoverer.java b/core/src/main/java/org/spongepowered/configurate/objectmapping/RecordFieldDiscoverer.java index 64619ed95..6dbf69792 100644 --- a/core/src/main/java/org/spongepowered/configurate/objectmapping/RecordFieldDiscoverer.java +++ b/core/src/main/java/org/spongepowered/configurate/objectmapping/RecordFieldDiscoverer.java @@ -88,8 +88,11 @@ private RecordFieldDiscoverer() { * @return an instance factory if this class is a record */ @Override - public @Nullable InstanceFactory<@Nullable Object[]> discover(final AnnotatedType target, - final FieldCollector<@Nullable Object[], V> collector) throws SerializationException { + public @Nullable InstanceFactory<@Nullable Object[]> discover( + final AnnotatedType target, + final FieldCollector<@Nullable Object[], V> collector, + final MethodHandles.@Nullable Lookup lookup // see J16 source set for this + ) throws SerializationException { if (CLASS_IS_RECORD != null && CLASS_GET_RECORD_COMPONENTS != null && RECORD_COMPONENT_GET_ANNOTATED_TYPE != null && RECORD_COMPONENT_GET_NAME != null && RECORD_COMPONENT_GET_ACCESSOR != null) { final Class clazz = erase(target.getType()); diff --git a/core/src/main/java/org/spongepowered/configurate/util/CheckedBiFunction.java b/core/src/main/java/org/spongepowered/configurate/util/CheckedBiFunction.java new file mode 100644 index 000000000..bd2607c93 --- /dev/null +++ b/core/src/main/java/org/spongepowered/configurate/util/CheckedBiFunction.java @@ -0,0 +1,63 @@ +/* + * Configurate + * Copyright (C) zml and Configurate contributors + * + * 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 org.spongepowered.configurate.util; + +import static java.util.Objects.requireNonNull; + +import org.checkerframework.checker.nullness.qual.NonNull; + +import java.util.function.BiFunction; + +/** + * A function with two inputs and one output which + * may throw a checked exception. + * + * @param the first input parameter type + * @param the second input parameter type + * @param the output parameter type + * @param the type thrown + * @since 4.2.0 + */ +@FunctionalInterface +public interface CheckedBiFunction { + + /** + * Perform the action. + * + * @param one first parameter + * @param two second parameter + * @return return value + * @throws E thrown when defined by types accepting this function + * @since 4.2.0 + */ + O apply(I1 one, I2 two) throws E; + + /** + * Convert a JDK {@link BiFunction} into its checked variant. + * + * @param func the function + * @param first parameter type + * @param second parameter type + * @param return type + * @return the function as a checked function + * @since 4.2.0 + */ + static CheckedBiFunction from(final BiFunction func) { + return requireNonNull(func, "func")::apply; + } + +} diff --git a/core/src/main/java16/org/spongepowered/configurate/objectmapping/RecordFieldDiscoverer.java b/core/src/main/java16/org/spongepowered/configurate/objectmapping/RecordFieldDiscoverer.java index 316ada957..14317d326 100644 --- a/core/src/main/java16/org/spongepowered/configurate/objectmapping/RecordFieldDiscoverer.java +++ b/core/src/main/java16/org/spongepowered/configurate/objectmapping/RecordFieldDiscoverer.java @@ -23,11 +23,13 @@ import org.spongepowered.configurate.serialize.SerializationException; import org.spongepowered.configurate.util.Types; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; import java.lang.reflect.AnnotatedElement; import java.lang.reflect.AnnotatedType; import java.lang.reflect.Constructor; import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.RecordComponent; @@ -51,8 +53,11 @@ private RecordFieldDiscoverer() { * @return an instance factory if this class is a record */ @Override - public @Nullable InstanceFactory<@Nullable Object[]> discover(final AnnotatedType target, - final FieldCollector<@Nullable Object[], V> collector) throws SerializationException { + public @Nullable InstanceFactory<@Nullable Object[]> discover( + final AnnotatedType target, + final FieldCollector<@Nullable Object[], V> collector, + final MethodHandles.@Nullable Lookup lookup + ) throws SerializationException { final Class clazz = erase(target.getType()); if (!clazz.isRecord()) { return null; @@ -64,14 +69,19 @@ private RecordFieldDiscoverer() { // each component is itself annotatable, plus attached backing field and accessor method, so we have to get them all final RecordComponent component = recordComponents[i]; final Method accessor = component.getAccessor(); - accessor.setAccessible(true); + final MethodHandle accessorHandle; + if (lookup != null) { + accessorHandle = lookup.unreflect(accessor); + } else { + accessor.setAccessible(true); + accessorHandle = MethodHandles.publicLookup().unreflect(accessor); + } final String name = component.getName(); final AnnotatedType genericType = component.getAnnotatedType(); constructorParams[i] = erase(genericType.getType()); // to add to the canonical constructor final Field backingField = clazz.getDeclaredField(name); - backingField.setAccessible(true); // Then we put everything together: resolve the type, calculate annotations, and submit a field final AnnotatedType resolvedType = resolveExactType(genericType, target); @@ -84,13 +94,27 @@ private RecordFieldDiscoverer() { } else { intermediate[targetIdx] = implicitSupplier.get(); } - }, accessor::invoke + }, instance -> { + try { + return accessorHandle.invoke(instance); + } catch (final Exception ex) { + throw ex; + } catch (final Throwable thr) { + throw new Exception(thr); + } + } ); } // canonical constructor, which we'll use to make new instances - final Constructor clazzConstructor = clazz.getDeclaredConstructor(constructorParams); - clazzConstructor.setAccessible(true); + final MethodHandle clazzConstructor; + if (lookup != null) { + clazzConstructor = lookup.findConstructor(clazz, MethodType.methodType(void.class, constructorParams)); + } else { + final Constructor temp = clazz.getDeclaredConstructor(constructorParams); + temp.setAccessible(true); + clazzConstructor = MethodHandles.publicLookup().unreflectConstructor(temp); + } return new InstanceFactory<>() { @Override @@ -108,8 +132,8 @@ public Object complete(final @Nullable Object[] intermediate) throws Serializati } try { - return clazzConstructor.newInstance(intermediate); - } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { + return clazzConstructor.invokeWithArguments(intermediate); + } catch (final Throwable e) { throw new SerializationException(target.getType(), e); } } @@ -121,6 +145,9 @@ public boolean canCreateInstances() { }; } catch (final NoSuchFieldException | NoSuchMethodException ex) { throw new SerializationException(target.getType(), "Record class did not have fields and accessors aligning specification", ex); + } catch (final IllegalAccessException ex) { + throw new SerializationException(target.getType(), "Record class was not accessible! Try passing a MethodHandles.Lookup instance in " + + "the appropriate module to set the value", ex); } } diff --git a/core/src/main/java9/org/spongepowered/configurate/objectmapping/ObjectFieldDiscoverer.java b/core/src/main/java9/org/spongepowered/configurate/objectmapping/DisabledObjectFieldDiscoverer.java similarity index 66% rename from core/src/main/java9/org/spongepowered/configurate/objectmapping/ObjectFieldDiscoverer.java rename to core/src/main/java9/org/spongepowered/configurate/objectmapping/DisabledObjectFieldDiscoverer.java index a72a8ae66..a0490d1c8 100644 --- a/core/src/main/java9/org/spongepowered/configurate/objectmapping/ObjectFieldDiscoverer.java +++ b/core/src/main/java9/org/spongepowered/configurate/objectmapping/DisabledObjectFieldDiscoverer.java @@ -22,71 +22,97 @@ import org.checkerframework.checker.nullness.qual.Nullable; import org.spongepowered.configurate.serialize.SerializationException; +import org.spongepowered.configurate.util.CheckedBiFunction; import org.spongepowered.configurate.util.CheckedFunction; import org.spongepowered.configurate.util.Types; +import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; import java.lang.invoke.VarHandle; import java.lang.reflect.AnnotatedType; -import java.lang.reflect.Constructor; import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Modifier; import java.util.HashMap; import java.util.Map; import java.util.Objects; import java.util.function.Supplier; -class ObjectFieldDiscoverer implements FieldDiscoverer> { +class DisabledObjectFieldDiscoverer implements FieldDiscoverer> { private static final MethodHandles.Lookup OWN_LOOKUP = MethodHandles.lookup(); - static final ObjectFieldDiscoverer EMPTY_CONSTRUCTOR_INSTANCE = new ObjectFieldDiscoverer(type -> { + static final ObjectFieldDiscoverer EMPTY_CONSTRUCTOR_INSTANCE = new ObjectFieldDiscoverer((type, lookup) -> { try { - final Constructor constructor; - constructor = erase(type.getType()).getDeclaredConstructor(); - constructor.setAccessible(true); + final MethodHandle constructor; + final Class erased = erase(type.getType()); + constructor = MethodHandles.privateLookupIn(erased, lookup == null ? OWN_LOOKUP : lookup) + .findConstructor(erased, MethodType.methodType(void.class)); return () -> { try { - return constructor.newInstance(); - } catch (final InstantiationException | IllegalAccessException | InvocationTargetException e) { - throw new RuntimeException(e); + return constructor.invoke(); + } catch (final RuntimeException ex) { + throw ex; + } catch (final Throwable thr) { + throw new RuntimeException(thr); } }; - } catch (final NoSuchMethodException e) { + } catch (final NoSuchMethodException | IllegalAccessException e) { return null; } - }, "Objects must have a zero-argument constructor to be able to create new instances"); - - private final CheckedFunction, SerializationException> instanceFactory; + }, "Objects must have a zero-argument constructor to be able to create new instances", false); + + private final CheckedBiFunction< + AnnotatedType, + MethodHandles.@Nullable Lookup, + @Nullable Supplier, + SerializationException + > instanceFactory; private final String instanceUnavailableErrorMessage; + private final boolean requiresInstanceCreation; - ObjectFieldDiscoverer( + DisabledObjectFieldDiscoverer( final CheckedFunction, SerializationException> instanceFactory, - final @Nullable String instanceUnavailableErrorMessage + final @Nullable String instanceUnavailableErrorMessage, + final boolean requiresInstanceCreation + ) { + this((type, lookup) -> instanceFactory.apply(type), instanceUnavailableErrorMessage, requiresInstanceCreation); + } + + DisabledObjectFieldDiscoverer( + final CheckedBiFunction, SerializationException> instanceFactory, + final @Nullable String instanceUnavailableErrorMessage, + final boolean requiresInstanceCreation ) { this.instanceFactory = instanceFactory; this.instanceUnavailableErrorMessage = Objects.requireNonNullElse( instanceUnavailableErrorMessage, "Unable to create instances for this type!" ); + this.requiresInstanceCreation = requiresInstanceCreation; } @Override - public @Nullable InstanceFactory> discover(final AnnotatedType target, - final FieldCollector, V> collector) throws SerializationException { + public @Nullable InstanceFactory> discover( + final AnnotatedType target, + final FieldCollector, V> collector, + final MethodHandles.@Nullable Lookup lookup + ) throws SerializationException { final Class clazz = erase(target.getType()); if (clazz.isInterface()) { throw new SerializationException(target.getType(), "ObjectMapper can only work with concrete types"); } - final @Nullable Supplier maker = this.instanceFactory.apply(target); + final @Nullable Supplier maker = this.instanceFactory.apply(target, lookup); + if (maker == null && this.requiresInstanceCreation) { + return null; + } AnnotatedType collectType = target; Class collectClass = clazz; while (true) { try { - collectFields(collectType, collector); + collectFields(collectType, collector, lookup); } catch (final IllegalAccessException ex) { throw new SerializationException(collectType.getType(), "Unable to access field in type", ex); } @@ -107,7 +133,7 @@ public Map begin() { @Override public void complete(final Object instance, final Map intermediate) { - for (Map.Entry entry : intermediate.entrySet()) { + for (final Map.Entry entry : intermediate.entrySet()) { // Handle implicit field initialization by detecting any existing information in the object if (entry.getValue() instanceof ImplicitProvider) { final @Nullable Object implicit = ((ImplicitProvider) entry.getValue()).provider.get(); @@ -124,9 +150,9 @@ public void complete(final Object instance, final Map interme @Override public Object complete(final Map intermediate) throws SerializationException { - final Object instance = maker == null ? null : maker.get(); + final @Nullable Object instance = maker == null ? null : maker.get(); if (instance == null) { - throw new SerializationException(target.getType(), ObjectFieldDiscoverer.this.instanceUnavailableErrorMessage); + throw new SerializationException(target.getType(), DisabledObjectFieldDiscoverer.this.instanceUnavailableErrorMessage); } complete(instance, intermediate); return instance; @@ -140,10 +166,14 @@ public boolean canCreateInstances() { }; } - private void collectFields(final AnnotatedType clazz, final FieldCollector, ?> fieldMaker) throws IllegalAccessException { + private void collectFields( + final AnnotatedType clazz, + final FieldCollector, ?> fieldMaker, + final MethodHandles.@Nullable Lookup source + ) throws IllegalAccessException { final Class erased = erase(clazz.getType()); - final MethodHandles.Lookup lookup = MethodHandles.privateLookupIn(erased, OWN_LOOKUP); - for (Field field : erased.getDeclaredFields()) { + final MethodHandles.Lookup lookup = MethodHandles.privateLookupIn(erased, source == null ? OWN_LOOKUP : source); + for (final Field field : erased.getDeclaredFields()) { if ((field.getModifiers() & (Modifier.STATIC | Modifier.TRANSIENT)) != 0) { continue; } diff --git a/core/src/main/java9/org/spongepowered/configurate/objectmapping/LookupShim.java b/core/src/main/java9/org/spongepowered/configurate/objectmapping/LookupShim.java new file mode 100644 index 000000000..974b86dde --- /dev/null +++ b/core/src/main/java9/org/spongepowered/configurate/objectmapping/LookupShim.java @@ -0,0 +1,30 @@ +/* + * Configurate + * Copyright (C) zml and Configurate contributors + * + * 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 org.spongepowered.configurate.objectmapping; + +import java.lang.invoke.MethodHandles; + +final class LookupShim { + + private LookupShim() { + } + + static MethodHandles.Lookup privateLookupIn(final Class clazz, final MethodHandles.Lookup existingLookup) throws IllegalAccessException { + return MethodHandles.privateLookupIn(clazz, existingLookup); + } + +} diff --git a/extra/kotlin/src/main/kotlin/org/spongepowered/configurate/kotlin/ObjectMapping.kt b/extra/kotlin/src/main/kotlin/org/spongepowered/configurate/kotlin/ObjectMapping.kt index 2aca9b1bf..6199c4089 100644 --- a/extra/kotlin/src/main/kotlin/org/spongepowered/configurate/kotlin/ObjectMapping.kt +++ b/extra/kotlin/src/main/kotlin/org/spongepowered/configurate/kotlin/ObjectMapping.kt @@ -25,6 +25,7 @@ import org.spongepowered.configurate.objectmapping.FieldDiscoverer import org.spongepowered.configurate.objectmapping.ObjectMapper import org.spongepowered.configurate.objectmapping.ObjectMapper.Factory import org.spongepowered.configurate.util.Types.combinedAnnotations +import java.lang.invoke.MethodHandles import java.lang.reflect.AnnotatedElement import java.lang.reflect.AnnotatedType import kotlin.reflect.KAnnotatedElement @@ -87,7 +88,8 @@ internal inline fun typeTokenOf() = object : TypeToken() {} private object DataClassFieldDiscoverer : FieldDiscoverer> { override fun discover( target: AnnotatedType, - collector: FieldDiscoverer.FieldCollector, V> + collector: FieldDiscoverer.FieldCollector, V>, + lookup: MethodHandles.Lookup? // include the argument here, even though Kotlin doesn't really support module access control yet ): FieldDiscoverer.InstanceFactory>? { val klass = erase(target.type).kotlin if (!klass.isData) {