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 Dec 15, 2022
1 parent 6764285 commit 01a34b6
Show file tree
Hide file tree
Showing 14 changed files with 404 additions and 80 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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/"
)

Expand Down
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,65 @@

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());
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<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 +92,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 +166,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 01a34b6

Please sign in to comment.