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) {