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..97254f1c8 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,58 @@ 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()); + if (lookup == null) { // legacy + final Constructor construct = erased.getDeclaredConstructor(); + construct.setAccessible(true); + constructor = OWN_LOOKUP.unreflectConstructor(construct); + } else { + constructor = LookupShim.privateLookupIn(erased, 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 +81,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 +100,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 +118,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 +126,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 +174,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 f6a543161..bb714f18f 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 @@ -88,6 +89,7 @@ private object DataClassFieldDiscoverer : FieldDiscoverer discover( target: AnnotatedType, 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) {