Skip to content

Commit 01a34b6

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 6764285 commit 01a34b6

File tree

14 files changed

+404
-80
lines changed

14 files changed

+404
-80
lines changed

build-logic/src/main/groovy/org.spongepowered.configurate.build.component.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ tasks.withType(Javadoc).configureEach {
7979
options.links(
8080
"https://lightbend.github.io/config/latest/api/",
8181
"https://fasterxml.github.io/jackson-core/javadoc/2.10/",
82-
"https://checkerframework.org/api/"
82+
// "https://checkerframework.org/api/"
8383
)
8484
options.linkSource()
8585
}

build-logic/src/main/groovy/org/spongepowered/configurate/build/ConfigurateExtension.groovy

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ class ConfigurateExtension {
3232
options.links(
3333
"https://lightbend.github.io/config/latest/api/",
3434
"https://fasterxml.github.io/jackson-core/javadoc/2.10/",
35-
"https://checkerframework.org/api/",
35+
// "https://checkerframework.org/api/",
3636
"https://www.javadoc.io/doc/io.leangen.geantyref/geantyref/1.3.11/"
3737
)
3838

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: 108 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -22,45 +22,65 @@
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;
29-
import java.lang.reflect.Constructor;
3033
import java.lang.reflect.Field;
31-
import java.lang.reflect.InvocationTargetException;
3234
import java.lang.reflect.Modifier;
3335
import java.util.HashMap;
3436
import java.util.Map;
3537
import java.util.function.Supplier;
3638

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

39-
static final ObjectFieldDiscoverer EMPTY_CONSTRUCTOR_INSTANCE = new ObjectFieldDiscoverer(type -> {
41+
private static final MethodHandles.Lookup OWN_LOOKUP = MethodHandles.lookup();
42+
43+
static final ObjectFieldDiscoverer EMPTY_CONSTRUCTOR_INSTANCE = new ObjectFieldDiscoverer((type, lookup) -> {
4044
try {
41-
final Constructor<?> constructor;
42-
constructor = erase(type.getType()).getDeclaredConstructor();
43-
constructor.setAccessible(true);
45+
final MethodHandle constructor;
46+
final Class<?> erased = erase(type.getType());
47+
constructor = LookupShim.privateLookupIn(erased, lookup == null ? OWN_LOOKUP : lookup)
48+
.findConstructor(erased, MethodType.methodType(void.class));
4449
return () -> {
4550
try {
46-
return constructor.newInstance();
47-
} catch (final InstantiationException | IllegalAccessException | InvocationTargetException e) {
48-
throw new RuntimeException(e);
51+
return constructor.invoke();
52+
} catch (final RuntimeException ex) {
53+
throw ex;
54+
} catch (final Throwable thr) {
55+
throw new RuntimeException(thr);
4956
}
5057
};
51-
} catch (final NoSuchMethodException e) {
58+
} catch (final NoSuchMethodException | IllegalAccessException e) {
5259
return null;
5360
}
5461
}, "Objects must have a zero-argument constructor to be able to create new instances", false);
5562

56-
private final CheckedFunction<AnnotatedType, @Nullable Supplier<Object>, SerializationException> instanceFactory;
63+
private final CheckedBiFunction<
64+
AnnotatedType,
65+
MethodHandles.@Nullable Lookup,
66+
@Nullable Supplier<Object>,
67+
SerializationException
68+
> instanceFactory;
5769
private final String instanceUnavailableErrorMessage;
5870
private final boolean requiresInstanceCreation;
5971

6072
ObjectFieldDiscoverer(
6173
final CheckedFunction<AnnotatedType, @Nullable Supplier<Object>, SerializationException> instanceFactory,
6274
final @Nullable String instanceUnavailableErrorMessage,
6375
final boolean requiresInstanceCreation
76+
) {
77+
this((type, lookup) -> instanceFactory.apply(type), instanceUnavailableErrorMessage, requiresInstanceCreation);
78+
}
79+
80+
ObjectFieldDiscoverer(
81+
final CheckedBiFunction<AnnotatedType, MethodHandles.@Nullable Lookup, @Nullable Supplier<Object>, SerializationException> instanceFactory,
82+
final @Nullable String instanceUnavailableErrorMessage,
83+
final boolean requiresInstanceCreation
6484
) {
6585
this.instanceFactory = instanceFactory;
6686
if (instanceUnavailableErrorMessage == null) {
@@ -72,60 +92,65 @@ class ObjectFieldDiscoverer implements FieldDiscoverer<Map<Field, Object>> {
7292
}
7393

7494
@Override
75-
public <V> @Nullable InstanceFactory<Map<Field, Object>> discover(final AnnotatedType target,
76-
final FieldCollector<Map<Field, Object>, V> collector) throws SerializationException {
95+
public <V> @Nullable InstanceFactory<Map<FieldHandles, Object>> discover(
96+
final AnnotatedType target,
97+
final FieldCollector<Map<FieldHandles, Object>, V> collector,
98+
final MethodHandles.@Nullable Lookup lookup
99+
) throws SerializationException {
77100
final Class<?> clazz = erase(target.getType());
78101
if (clazz.isInterface()) {
79102
throw new SerializationException(target.getType(), "ObjectMapper can only work with concrete types");
80103
}
81104

82-
final @Nullable Supplier<Object> maker = this.instanceFactory.apply(target);
105+
final @Nullable Supplier<Object> maker = this.instanceFactory.apply(target, lookup);
83106
if (maker == null && this.requiresInstanceCreation) {
84107
return null;
85108
}
86109

87110
AnnotatedType collectType = target;
88111
Class<?> collectClass = clazz;
89112
while (true) {
90-
collectFields(collectType, collector);
113+
collectFields(collectType, collector, lookup);
91114
collectClass = collectClass.getSuperclass();
92115
if (collectClass.equals(Object.class)) {
93116
break;
94117
}
95118
collectType = getExactSuperType(collectType, collectClass);
96119
}
97120

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

100123
@Override
101-
public Map<Field, Object> begin() {
124+
public Map<FieldHandles, Object> begin() {
102125
return new HashMap<>();
103126
}
104127

105128
@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()) {
129+
public void complete(final Object instance, final Map<FieldHandles, Object> intermediate) throws SerializationException {
130+
for (final Map.Entry<FieldHandles, Object> entry : intermediate.entrySet()) {
108131
try {
109132
// Handle implicit field initialization by detecting any existing information in the object
110133
if (entry.getValue() instanceof ImplicitProvider) {
111134
final @Nullable Object implicit = ((ImplicitProvider) entry.getValue()).provider.get();
112135
if (implicit != null) {
113-
if (entry.getKey().get(instance) == null) {
114-
entry.getKey().set(instance, implicit);
136+
if (entry.getKey().getter.invoke(instance) == null) {
137+
entry.getKey().setter.invoke(instance, implicit);
115138
}
116139
}
117140
} else {
118-
entry.getKey().set(instance, entry.getValue());
141+
entry.getKey().setter.invoke(instance, entry.getValue());
119142
}
120143
} catch (final IllegalAccessException e) {
121144
throw new SerializationException(target.getType(), e);
145+
} catch (final Throwable thr) {
146+
throw new SerializationException(target.getType(), "An unexpected error occurred while trying to set a field", thr);
122147
}
123148
}
124149
}
125150

126151
@Override
127-
public Object complete(final Map<Field, Object> intermediate) throws SerializationException {
128-
final Object instance = maker == null ? null : maker.get();
152+
public Object complete(final Map<FieldHandles, Object> intermediate) throws SerializationException {
153+
final @Nullable Object instance = maker == null ? null : maker.get();
129154
if (instance == null) {
130155
throw new SerializationException(target.getType(), ObjectFieldDiscoverer.this.instanceUnavailableErrorMessage);
131156
}
@@ -141,22 +166,70 @@ public boolean canCreateInstances() {
141166
};
142167
}
143168

144-
private void collectFields(final AnnotatedType clazz, final FieldCollector<Map<Field, Object>, ?> fieldMaker) {
169+
private <V> void collectFields(
170+
final AnnotatedType clazz,
171+
final FieldCollector<Map<FieldHandles, Object>, V> fieldMaker,
172+
final MethodHandles.@Nullable Lookup lookup
173+
) throws SerializationException {
145174
for (final Field field : erase(clazz.getType()).getDeclaredFields()) {
146175
if ((field.getModifiers() & (Modifier.STATIC | Modifier.TRANSIENT)) != 0) {
147176
continue;
148177
}
149178

150-
field.setAccessible(true);
151179
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);
180+
final FieldData.Deserializer<Map<FieldHandles, Object>> deserializer;
181+
final CheckedFunction<V, @Nullable Object, Exception> serializer;
182+
final FieldHandles handles;
183+
try {
184+
if (lookup != null) {
185+
handles = new FieldHandles(field, lookup);
186+
} else {
187+
handles = new FieldHandles(field);
188+
}
189+
} catch (final IllegalAccessException ex) {
190+
throw new SerializationException(fieldType, ex);
191+
}
192+
deserializer = (intermediate, val, implicitProvider) -> {
193+
if (val != null) {
194+
intermediate.put(handles, val);
195+
} else {
196+
intermediate.put(handles, new ImplicitProvider(implicitProvider));
197+
}
198+
};
199+
serializer = inst -> {
200+
try {
201+
return handles.getter.invoke(inst);
202+
} catch (final Exception ex) {
203+
throw ex;
204+
} catch (final Throwable thr) {
205+
throw new Exception(thr);
206+
}
207+
};
208+
fieldMaker.accept(
209+
field.getName(),
210+
fieldType,
211+
Types.combinedAnnotations(fieldType, field),
212+
deserializer,
213+
serializer
214+
);
215+
}
216+
}
217+
218+
static class FieldHandles {
219+
final MethodHandle getter;
220+
final MethodHandle setter;
221+
222+
FieldHandles(final Field field) throws IllegalAccessException {
223+
field.setAccessible(true);
224+
final MethodHandles.Lookup lookup = MethodHandles.publicLookup();
225+
226+
this.getter = lookup.unreflectGetter(field);
227+
this.setter = lookup.unreflectSetter(field);
228+
}
229+
230+
FieldHandles(final Field field, final MethodHandles.Lookup lookup) throws IllegalAccessException {
231+
this.getter = lookup.unreflectGetter(field);
232+
this.setter = lookup.unreflectSetter(field);
160233
}
161234
}
162235

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)