Skip to content

Commit f539c96

Browse files
committed
feat(core): Optionally use a user-provided Lookup when building object mappers
This allows Configurate to read fields in otherwise closed modules.
1 parent a1baa61 commit f539c96

File tree

12 files changed

+408
-76
lines changed

12 files changed

+408
-76
lines changed

core/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ sourceSets {
3636
main {
3737
multirelease {
3838
alternateVersions(
39-
// 9, // VarHandles // TODO: temporarily disabled, cannot write final fields
39+
9, // private Lookup, ~~VarHandles~~ // TODO: handles temporarily disabled, cannot write final fields
4040
10, // immutable collections
4141
16 // FieldDiscoverer for records
4242
)

core/src/main/java/org/spongepowered/configurate/objectmapping/FieldDiscoverer.java

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import org.spongepowered.configurate.serialize.SerializationException;
2323
import org.spongepowered.configurate.util.CheckedFunction;
2424

25+
import java.lang.invoke.MethodHandles;
2526
import java.lang.reflect.AnnotatedElement;
2627
import java.lang.reflect.AnnotatedType;
2728
import java.util.function.Supplier;
@@ -127,6 +128,31 @@ static FieldDiscoverer<?> emptyConstructorObject() {
127128
return ObjectFieldDiscoverer.EMPTY_CONSTRUCTOR_INSTANCE;
128129
}
129130

131+
/**
132+
* Inspect the {@code target} type for fields to be supplied to
133+
* the {@code collector}.
134+
*
135+
* <p>If the target type is handleable, a non-null value must be returned.
136+
* Fields can only be collected from one source at the moment, so if the
137+
* instance factory is null any discovered fields will be discarded.</p>
138+
*
139+
* @param target type to inspect
140+
* @param collector collector for discovered fields.
141+
* @param lookup a lookup for reflective access to access-controlled members
142+
* @param <V> object type
143+
* @return a factory for handling the construction of object instances, or
144+
* {@code null} if {@code target} is not of a handleable type.
145+
* @throws SerializationException if any fields have invalid data
146+
* @since 4.2.0
147+
*/
148+
default <V> @Nullable InstanceFactory<I> discover(
149+
final AnnotatedType target,
150+
final FieldCollector<I, V> collector,
151+
final MethodHandles.@Nullable Lookup lookup
152+
) throws SerializationException {
153+
return this.discover(target, collector);
154+
}
155+
130156
/**
131157
* Inspect the {@code target} type for fields to be supplied to
132158
* the {@code collector}.
@@ -142,8 +168,16 @@ static FieldDiscoverer<?> emptyConstructorObject() {
142168
* {@code null} if {@code target} is not of a handleable type.
143169
* @throws SerializationException if any fields have invalid data
144170
* @since 4.0.0
171+
* @deprecated for removal since 4.2.0, use the module-aware
172+
* {@link #discover(AnnotatedType, FieldCollector, MethodHandles.Lookup)} instead
145173
*/
146-
<V> @Nullable InstanceFactory<I> discover(AnnotatedType target, FieldCollector<I, V> collector) throws SerializationException;
174+
@Deprecated
175+
default <V> @Nullable InstanceFactory<I> discover(
176+
final AnnotatedType target,
177+
final FieldCollector<I, V> collector
178+
) throws SerializationException {
179+
return null;
180+
}
147181

148182
/**
149183
* A handler that controls the deserialization process for an object.
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* Configurate
3+
* Copyright (C) zml and Configurate contributors
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.spongepowered.configurate.objectmapping;
18+
19+
import java.lang.invoke.MethodHandles;
20+
21+
final class LookupShim {
22+
23+
private LookupShim() {
24+
}
25+
26+
static MethodHandles.Lookup privateLookupIn(final Class<?> clazz, final MethodHandles.Lookup existingLookup) throws IllegalAccessException {
27+
return existingLookup.in(clazz);
28+
}
29+
30+
}

core/src/main/java/org/spongepowered/configurate/objectmapping/ObjectFieldDiscoverer.java

Lines changed: 115 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -22,45 +22,73 @@
2222

2323
import org.checkerframework.checker.nullness.qual.Nullable;
2424
import org.spongepowered.configurate.serialize.SerializationException;
25+
import org.spongepowered.configurate.util.CheckedBiFunction;
2526
import org.spongepowered.configurate.util.CheckedFunction;
2627
import org.spongepowered.configurate.util.Types;
2728

29+
import java.lang.invoke.MethodHandle;
30+
import java.lang.invoke.MethodHandles;
31+
import java.lang.invoke.MethodType;
2832
import java.lang.reflect.AnnotatedType;
2933
import java.lang.reflect.Constructor;
3034
import java.lang.reflect.Field;
31-
import java.lang.reflect.InvocationTargetException;
3235
import java.lang.reflect.Modifier;
3336
import java.util.HashMap;
3437
import java.util.Map;
3538
import java.util.function.Supplier;
3639

37-
class ObjectFieldDiscoverer implements FieldDiscoverer<Map<Field, Object>> {
40+
class ObjectFieldDiscoverer implements FieldDiscoverer<Map<ObjectFieldDiscoverer.FieldHandles, Object>> {
3841

39-
static final ObjectFieldDiscoverer EMPTY_CONSTRUCTOR_INSTANCE = new ObjectFieldDiscoverer(type -> {
42+
private static final MethodHandles.Lookup OWN_LOOKUP = MethodHandles.lookup();
43+
44+
static final ObjectFieldDiscoverer EMPTY_CONSTRUCTOR_INSTANCE = new ObjectFieldDiscoverer((type, lookup) -> {
4045
try {
41-
final Constructor<?> constructor;
42-
constructor = erase(type.getType()).getDeclaredConstructor();
43-
constructor.setAccessible(true);
46+
final MethodHandle constructor;
47+
final Class<?> erased = erase(type.getType());
48+
if (lookup == null) { // legacy
49+
final Constructor<?> construct = erased.getDeclaredConstructor();
50+
construct.setAccessible(true);
51+
constructor = OWN_LOOKUP.unreflectConstructor(construct);
52+
} else {
53+
constructor = LookupShim.privateLookupIn(erased, lookup)
54+
.findConstructor(erased, MethodType.methodType(void.class));
55+
}
56+
4457
return () -> {
4558
try {
46-
return constructor.newInstance();
47-
} catch (final InstantiationException | IllegalAccessException | InvocationTargetException e) {
48-
throw new RuntimeException(e);
59+
return constructor.invoke();
60+
} catch (final RuntimeException ex) {
61+
throw ex;
62+
} catch (final Throwable thr) {
63+
throw new RuntimeException(thr);
4964
}
5065
};
51-
} catch (final NoSuchMethodException e) {
66+
} catch (final NoSuchMethodException | IllegalAccessException e) {
5267
return null;
5368
}
5469
}, "Objects must have a zero-argument constructor to be able to create new instances", false);
5570

56-
private final CheckedFunction<AnnotatedType, @Nullable Supplier<Object>, SerializationException> instanceFactory;
71+
private final CheckedBiFunction<
72+
AnnotatedType,
73+
MethodHandles.@Nullable Lookup,
74+
@Nullable Supplier<Object>,
75+
SerializationException
76+
> instanceFactory;
5777
private final String instanceUnavailableErrorMessage;
5878
private final boolean requiresInstanceCreation;
5979

6080
ObjectFieldDiscoverer(
6181
final CheckedFunction<AnnotatedType, @Nullable Supplier<Object>, SerializationException> instanceFactory,
6282
final @Nullable String instanceUnavailableErrorMessage,
6383
final boolean requiresInstanceCreation
84+
) {
85+
this((type, lookup) -> instanceFactory.apply(type), instanceUnavailableErrorMessage, requiresInstanceCreation);
86+
}
87+
88+
ObjectFieldDiscoverer(
89+
final CheckedBiFunction<AnnotatedType, MethodHandles.@Nullable Lookup, @Nullable Supplier<Object>, SerializationException> instanceFactory,
90+
final @Nullable String instanceUnavailableErrorMessage,
91+
final boolean requiresInstanceCreation
6492
) {
6593
this.instanceFactory = instanceFactory;
6694
if (instanceUnavailableErrorMessage == null) {
@@ -72,60 +100,65 @@ class ObjectFieldDiscoverer implements FieldDiscoverer<Map<Field, Object>> {
72100
}
73101

74102
@Override
75-
public <V> @Nullable InstanceFactory<Map<Field, Object>> discover(final AnnotatedType target,
76-
final FieldCollector<Map<Field, Object>, V> collector) throws SerializationException {
103+
public <V> @Nullable InstanceFactory<Map<FieldHandles, Object>> discover(
104+
final AnnotatedType target,
105+
final FieldCollector<Map<FieldHandles, Object>, V> collector,
106+
final MethodHandles.@Nullable Lookup lookup
107+
) throws SerializationException {
77108
final Class<?> clazz = erase(target.getType());
78109
if (clazz.isInterface()) {
79110
throw new SerializationException(target.getType(), "ObjectMapper can only work with concrete types");
80111
}
81112

82-
final @Nullable Supplier<Object> maker = this.instanceFactory.apply(target);
113+
final @Nullable Supplier<Object> maker = this.instanceFactory.apply(target, lookup);
83114
if (maker == null && this.requiresInstanceCreation) {
84115
return null;
85116
}
86117

87118
AnnotatedType collectType = target;
88119
Class<?> collectClass = clazz;
89120
while (true) {
90-
collectFields(collectType, collector);
121+
collectFields(collectType, collector, lookup);
91122
collectClass = collectClass.getSuperclass();
92123
if (collectClass.equals(Object.class)) {
93124
break;
94125
}
95126
collectType = getExactSuperType(collectType, collectClass);
96127
}
97128

98-
return new MutableInstanceFactory<Map<Field, Object>>() {
129+
return new MutableInstanceFactory<Map<FieldHandles, Object>>() {
99130

100131
@Override
101-
public Map<Field, Object> begin() {
132+
public Map<FieldHandles, Object> begin() {
102133
return new HashMap<>();
103134
}
104135

105136
@Override
106-
public void complete(final Object instance, final Map<Field, Object> intermediate) throws SerializationException {
107-
for (final Map.Entry<Field, Object> entry : intermediate.entrySet()) {
137+
public void complete(final Object instance, final Map<FieldHandles, Object> intermediate) throws SerializationException {
138+
for (final Map.Entry<FieldHandles, Object> entry : intermediate.entrySet()) {
108139
try {
109140
// Handle implicit field initialization by detecting any existing information in the object
110141
if (entry.getValue() instanceof ImplicitProvider) {
111142
final @Nullable Object implicit = ((ImplicitProvider) entry.getValue()).provider.get();
112143
if (implicit != null) {
113-
if (entry.getKey().get(instance) == null) {
114-
entry.getKey().set(instance, implicit);
144+
if (entry.getKey().getter.invoke(instance) == null) {
145+
entry.getKey().setter.invoke(instance, implicit);
115146
}
116147
}
117148
} else {
118-
entry.getKey().set(instance, entry.getValue());
149+
entry.getKey().setter.invoke(instance, entry.getValue());
119150
}
120151
} catch (final IllegalAccessException e) {
121152
throw new SerializationException(target.getType(), e);
153+
} catch (final Throwable thr) {
154+
throw new SerializationException(target.getType(), "An unexpected error occurred while trying to set a field", thr);
122155
}
123156
}
124157
}
125158

126159
@Override
127-
public Object complete(final Map<Field, Object> intermediate) throws SerializationException {
128-
final Object instance = maker == null ? null : maker.get();
160+
public Object complete(final Map<FieldHandles, Object> intermediate) throws SerializationException {
161+
final @Nullable Object instance = maker == null ? null : maker.get();
129162
if (instance == null) {
130163
throw new SerializationException(target.getType(), ObjectFieldDiscoverer.this.instanceUnavailableErrorMessage);
131164
}
@@ -141,22 +174,70 @@ public boolean canCreateInstances() {
141174
};
142175
}
143176

144-
private void collectFields(final AnnotatedType clazz, final FieldCollector<Map<Field, Object>, ?> fieldMaker) {
177+
private <V> void collectFields(
178+
final AnnotatedType clazz,
179+
final FieldCollector<Map<FieldHandles, Object>, V> fieldMaker,
180+
final MethodHandles.@Nullable Lookup lookup
181+
) throws SerializationException {
145182
for (final Field field : erase(clazz.getType()).getDeclaredFields()) {
146183
if ((field.getModifiers() & (Modifier.STATIC | Modifier.TRANSIENT)) != 0) {
147184
continue;
148185
}
149186

150-
field.setAccessible(true);
151187
final AnnotatedType fieldType = getFieldType(field, clazz);
152-
fieldMaker.accept(field.getName(), fieldType, Types.combinedAnnotations(fieldType, field),
153-
(intermediate, val, implicitProvider) -> {
154-
if (val != null) {
155-
intermediate.put(field, val);
156-
} else {
157-
intermediate.put(field, new ImplicitProvider(implicitProvider));
158-
}
159-
}, field::get);
188+
final FieldData.Deserializer<Map<FieldHandles, Object>> deserializer;
189+
final CheckedFunction<V, @Nullable Object, Exception> serializer;
190+
final FieldHandles handles;
191+
try {
192+
if (lookup != null) {
193+
handles = new FieldHandles(field, lookup);
194+
} else {
195+
handles = new FieldHandles(field);
196+
}
197+
} catch (final IllegalAccessException ex) {
198+
throw new SerializationException(fieldType, ex);
199+
}
200+
deserializer = (intermediate, val, implicitProvider) -> {
201+
if (val != null) {
202+
intermediate.put(handles, val);
203+
} else {
204+
intermediate.put(handles, new ImplicitProvider(implicitProvider));
205+
}
206+
};
207+
serializer = inst -> {
208+
try {
209+
return handles.getter.invoke(inst);
210+
} catch (final Exception ex) {
211+
throw ex;
212+
} catch (final Throwable thr) {
213+
throw new Exception(thr);
214+
}
215+
};
216+
fieldMaker.accept(
217+
field.getName(),
218+
fieldType,
219+
Types.combinedAnnotations(fieldType, field),
220+
deserializer,
221+
serializer
222+
);
223+
}
224+
}
225+
226+
static class FieldHandles {
227+
final MethodHandle getter;
228+
final MethodHandle setter;
229+
230+
FieldHandles(final Field field) throws IllegalAccessException {
231+
field.setAccessible(true);
232+
final MethodHandles.Lookup lookup = MethodHandles.publicLookup();
233+
234+
this.getter = lookup.unreflectGetter(field);
235+
this.setter = lookup.unreflectSetter(field);
236+
}
237+
238+
FieldHandles(final Field field, final MethodHandles.Lookup lookup) throws IllegalAccessException {
239+
this.getter = lookup.unreflectGetter(field);
240+
this.setter = lookup.unreflectSetter(field);
160241
}
161242
}
162243

core/src/main/java/org/spongepowered/configurate/objectmapping/ObjectMapper.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import org.spongepowered.configurate.util.NamingScheme;
2929

3030
import java.lang.annotation.Annotation;
31+
import java.lang.invoke.MethodHandles;
3132
import java.lang.reflect.Type;
3233
import java.util.List;
3334

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

365+
/**
366+
* Set a custom lookup to access fields.
367+
*
368+
* <p>This allows Configurate to reflectively modify classes
369+
* without opening them for reflective access.</p>
370+
*
371+
* @param lookup the lookup to use
372+
* @return this builder
373+
* @since 4.2.0
374+
*/
375+
Builder lookup(MethodHandles.Lookup lookup);
376+
364377
/**
365378
* Create a new factory using the current configuration.
366379
*

0 commit comments

Comments
 (0)