Skip to content

Commit 1c24244

Browse files
mp911deapp
authored and
app
committed
Apply Kotlin Value Class unboxing to generated Property Accessors.
Unwrap wrapped value types if necessary when using generated property accessors. Closes spring-projects#3087
1 parent d6fb17f commit 1c24244

File tree

4 files changed

+70
-6
lines changed

4 files changed

+70
-6
lines changed

src/main/java/org/springframework/data/mapping/model/ClassGeneratingPropertyAccessorFactory.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -1478,7 +1478,7 @@ static Function<Object, Object> getWrapper(PersistentProperty<?> property) {
14781478

14791479
Parameter parameter = copy.getParameters()[kotlinCopyByProperty.getParameterPosition()];
14801480

1481-
return o -> ClassUtils.isAssignableValue(parameter.getType(), o) || vh == null ? o : vh.wrap(o);
1481+
return o -> ClassUtils.isAssignableValue(parameter.getType(), o) || vh == null ? o : vh.applyWrapping(o);
14821482
}
14831483

14841484
return Function.identity();

src/main/java/org/springframework/data/mapping/model/KotlinValueUtils.java

+35-5
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import java.util.Arrays;
3434
import java.util.Collections;
3535
import java.util.List;
36+
import java.util.function.BiFunction;
3637

3738
import org.springframework.lang.Nullable;
3839
import org.springframework.util.Assert;
@@ -200,6 +201,8 @@ static final class ValueBoxing {
200201

201202
private final KFunction<?> wrapperConstructor;
202203

204+
private final KProperty<?> valueProperty;
205+
203206
private final boolean applyBoxing;
204207

205208
private final @Nullable ValueBoxing next;
@@ -258,7 +261,6 @@ private ValueBoxing(BoxingRules rules, KType type, KClass<?> kClass, boolean opt
258261
boolean applyBoxing;
259262

260263
if (kClass.isValue()) {
261-
262264
wrapperConstructor = kClass.getConstructors().iterator().next();
263265
KParameter nested = wrapperConstructor.getParameters().get(0);
264266
KType nestedType = nested.getType();
@@ -275,10 +277,12 @@ private ValueBoxing(BoxingRules rules, KType type, KClass<?> kClass, boolean opt
275277
}
276278

277279
Assert.notNull(nestedClass, () -> String.format("Cannot resolve nested class from type %s", nestedType));
278-
280+
this.valueProperty = kClass.getMembers().stream().filter(it -> it instanceof KProperty<?>)
281+
.map(KProperty.class::cast).findFirst().get();
279282
next = new ValueBoxing(rules, nestedType, nestedClass, nested.isOptional());
280283
} else {
281284
applyBoxing = false;
285+
this.valueProperty = null;
282286
}
283287

284288
this.kClass = kClass;
@@ -373,20 +377,46 @@ public ValueBoxing getNext() {
373377
}
374378

375379
/**
376-
* Apply wrapping into the boxing wrapper type if applicable.
380+
* Wrap the value into the boxing wrapper type if requested. Already wrapped values are left unchanged.
377381
*
378382
* @param o
379383
* @return
380384
*/
381385
@Nullable
382386
public Object wrap(@Nullable Object o) {
387+
return doWrap(o, false, ValueBoxing::wrap);
388+
}
389+
390+
/**
391+
* Apply wrapping into the boxing wrapper type if applicable. For types, that do not require wrapping but are
392+
* wrapped, the component type is being unwrapped.
393+
*
394+
* @param o
395+
* @return
396+
* @since 3.2.6
397+
*/
398+
@Nullable
399+
Object applyWrapping(@Nullable Object o) {
400+
return doWrap(o, true, ValueBoxing::applyWrapping);
401+
}
402+
403+
/**
404+
* Apply staged wrapping into the boxing wrapper type if value boxing is requested. Otherwise, apply unwrapping and
405+
* pass on the result into {@code nextWrapStage}.
406+
*/
407+
@Nullable
408+
Object doWrap(@Nullable Object o, boolean unwrap, BiFunction<ValueBoxing, Object, Object> nextWrapStage) {
383409

384410
if (applyBoxing) {
385-
return o == null || kClass.isInstance(o) ? o : wrapperConstructor.call(next.wrap(o));
411+
return o == null || kClass.isInstance(o) ? o : wrapperConstructor.call(nextWrapStage.apply(next, o));
412+
} else if (unwrap && kClass.isValue()) {
413+
if (o != null && kClass.isInstance(o)) {
414+
o = valueProperty.getGetter().call(o);
415+
}
386416
}
387417

388418
if (hasNext()) {
389-
return next.wrap(o);
419+
return nextWrapStage.apply(next, o);
390420
}
391421

392422
return o;

src/test/java/org/springframework/data/mapping/model/KotlinPropertyAccessorFactoryTests.java

+28
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import kotlin.reflect.KClass;
2222
import kotlin.reflect.jvm.internal.KotlinReflectionInternalError;
2323

24+
import java.lang.reflect.Constructor;
2425
import java.util.function.Function;
2526
import java.util.stream.Stream;
2627

@@ -255,6 +256,33 @@ void genericNullableInlineClassesShouldWork(PersistentPropertyAccessorFactory fa
255256
assertThat(propertyAccessor.getProperty(recursive)).isEqualTo(newOuter);
256257
}
257258

259+
@MethodSource("factories")
260+
@ParameterizedTest // GH-1947
261+
void shouldUnwrapValueTypeIfNecessary(PersistentPropertyAccessorFactory factory) throws Exception {
262+
263+
BasicPersistentEntity<Object, SamplePersistentProperty> entity = mappingContext
264+
.getRequiredPersistentEntity(MyEntity.class);
265+
266+
Constructor<?> declaredConstructor = MyValueClass.class.getDeclaredConstructor(String.class);
267+
268+
Object instance = createInstance(entity, parameter -> {
269+
270+
String name = parameter.getName();
271+
272+
return switch (name) {
273+
case "id" -> 1L;
274+
case "name" -> "foo";
275+
default -> "bar";
276+
};
277+
278+
});
279+
280+
var propertyAccessor = factory.getPropertyAccessor(entity, instance);
281+
var createdBy = entity.getRequiredPersistentProperty("createdBy");
282+
283+
propertyAccessor.setProperty(createdBy, BeanUtils.instantiateClass(declaredConstructor, "baz"));
284+
}
285+
258286
private Object createInstance(BasicPersistentEntity<?, SamplePersistentProperty> entity,
259287
Function<Parameter<?, ?>, Object> parameterProvider) {
260288
return instantiators.getInstantiatorFor(entity).createInstance(entity,

src/test/kotlin/org/springframework/data/mapping/model/InlineClasses.kt

+6
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,12 @@ value class MyGenericValue<T>(val id: T)
5858
@JvmInline
5959
value class MyGenericBoundValue<T : CharSequence>(val id: T)
6060

61+
data class MyEntity(
62+
val id: Long = 0L,
63+
val name: String,
64+
val createdBy: MyValueClass = MyValueClass("UNKNOWN"),
65+
)
66+
6167
data class WithGenericValue(
6268
// ctor: WithGenericValue(CharSequence string, CharSequence charseq, Object recursive, DefaultConstructorMarker $constructor_marker)
6369
val string: MyGenericBoundValue<String>,

0 commit comments

Comments
 (0)