Skip to content

Commit

Permalink
feat(core): Optionally use a user-provided Lookup when building objec…
Browse files Browse the repository at this point in the history
…t mappers

This allows Configurate to read fields in otherwise closed modules.
  • Loading branch information
zml2008 committed Jun 2, 2023
1 parent a1baa61 commit f539c96
Show file tree
Hide file tree
Showing 12 changed files with 408 additions and 76 deletions.
2 changes: 1 addition & 1 deletion core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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}.
*
* <p>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.</p>
*
* @param target type to inspect
* @param collector collector for discovered fields.
* @param lookup a lookup for reflective access to access-controlled members
* @param <V> 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 <V> @Nullable InstanceFactory<I> discover(
final AnnotatedType target,
final FieldCollector<I, V> 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}.
Expand All @@ -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
*/
<V> @Nullable InstanceFactory<I> discover(AnnotatedType target, FieldCollector<I, V> collector) throws SerializationException;
@Deprecated
default <V> @Nullable InstanceFactory<I> discover(
final AnnotatedType target,
final FieldCollector<I, V> collector
) throws SerializationException {
return null;
}

/**
* A handler that controls the deserialization process for an object.
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -22,45 +22,73 @@

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<Map<Field, Object>> {
class ObjectFieldDiscoverer implements FieldDiscoverer<Map<ObjectFieldDiscoverer.FieldHandles, Object>> {

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<AnnotatedType, @Nullable Supplier<Object>, SerializationException> instanceFactory;
private final CheckedBiFunction<
AnnotatedType,
MethodHandles.@Nullable Lookup,
@Nullable Supplier<Object>,
SerializationException
> instanceFactory;
private final String instanceUnavailableErrorMessage;
private final boolean requiresInstanceCreation;

ObjectFieldDiscoverer(
final CheckedFunction<AnnotatedType, @Nullable Supplier<Object>, SerializationException> instanceFactory,
final @Nullable String instanceUnavailableErrorMessage,
final boolean requiresInstanceCreation
) {
this((type, lookup) -> instanceFactory.apply(type), instanceUnavailableErrorMessage, requiresInstanceCreation);
}

ObjectFieldDiscoverer(
final CheckedBiFunction<AnnotatedType, MethodHandles.@Nullable Lookup, @Nullable Supplier<Object>, SerializationException> instanceFactory,
final @Nullable String instanceUnavailableErrorMessage,
final boolean requiresInstanceCreation
) {
this.instanceFactory = instanceFactory;
if (instanceUnavailableErrorMessage == null) {
Expand All @@ -72,60 +100,65 @@ class ObjectFieldDiscoverer implements FieldDiscoverer<Map<Field, Object>> {
}

@Override
public <V> @Nullable InstanceFactory<Map<Field, Object>> discover(final AnnotatedType target,
final FieldCollector<Map<Field, Object>, V> collector) throws SerializationException {
public <V> @Nullable InstanceFactory<Map<FieldHandles, Object>> discover(
final AnnotatedType target,
final FieldCollector<Map<FieldHandles, Object>, 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<Object> maker = this.instanceFactory.apply(target);
final @Nullable Supplier<Object> maker = this.instanceFactory.apply(target, lookup);
if (maker == null && this.requiresInstanceCreation) {
return null;
}

AnnotatedType collectType = target;
Class<?> collectClass = clazz;
while (true) {
collectFields(collectType, collector);
collectFields(collectType, collector, lookup);
collectClass = collectClass.getSuperclass();
if (collectClass.equals(Object.class)) {
break;
}
collectType = getExactSuperType(collectType, collectClass);
}

return new MutableInstanceFactory<Map<Field, Object>>() {
return new MutableInstanceFactory<Map<FieldHandles, Object>>() {

@Override
public Map<Field, Object> begin() {
public Map<FieldHandles, Object> begin() {
return new HashMap<>();
}

@Override
public void complete(final Object instance, final Map<Field, Object> intermediate) throws SerializationException {
for (final Map.Entry<Field, Object> entry : intermediate.entrySet()) {
public void complete(final Object instance, final Map<FieldHandles, Object> intermediate) throws SerializationException {
for (final Map.Entry<FieldHandles, Object> 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<Field, Object> intermediate) throws SerializationException {
final Object instance = maker == null ? null : maker.get();
public Object complete(final Map<FieldHandles, Object> intermediate) throws SerializationException {
final @Nullable Object instance = maker == null ? null : maker.get();
if (instance == null) {
throw new SerializationException(target.getType(), ObjectFieldDiscoverer.this.instanceUnavailableErrorMessage);
}
Expand All @@ -141,22 +174,70 @@ public boolean canCreateInstances() {
};
}

private void collectFields(final AnnotatedType clazz, final FieldCollector<Map<Field, Object>, ?> fieldMaker) {
private <V> void collectFields(
final AnnotatedType clazz,
final FieldCollector<Map<FieldHandles, Object>, 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<Map<FieldHandles, Object>> deserializer;
final CheckedFunction<V, @Nullable Object, Exception> 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);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -361,6 +362,18 @@ default <A extends Annotation> Builder addConstraint(final Class<A> definition,
*/
Builder addPostProcessor(PostProcessor.Factory factory);

/**
* Set a custom lookup to access fields.
*
* <p>This allows Configurate to reflectively modify classes
* without opening them for reflective access.</p>
*
* @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.
*
Expand Down
Loading

0 comments on commit f539c96

Please sign in to comment.