Skip to content

Commit b1a5fc4

Browse files
committed
Apply Kotlin Value Class unboxing to generated Property Accessors.
Unwrap wrapped value types if necessary when using generated property accessors. Closes #3087
1 parent 00d03c5 commit b1a5fc4

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
@@ -1481,7 +1481,7 @@ static Function<Object, Object> getWrapper(PersistentProperty<?> property) {
14811481

14821482
Parameter parameter = copy.getParameters()[kotlinCopyByProperty.getParameterPosition()];
14831483

1484-
return o -> ClassUtils.isAssignableValue(parameter.getType(), o) || vh == null ? o : vh.wrap(o);
1484+
return o -> ClassUtils.isAssignableValue(parameter.getType(), o) || vh == null ? o : vh.applyWrapping(o);
14851485
}
14861486

14871487
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;
@@ -205,6 +206,8 @@ static class ValueBoxing {
205206

206207
private final KFunction<?> wrapperConstructor;
207208

209+
private final KProperty<?> valueProperty;
210+
208211
private final boolean applyBoxing;
209212

210213
private final @Nullable ValueBoxing next;
@@ -263,7 +266,6 @@ private ValueBoxing(BoxingRules rules, KType type, KClass<?> kClass, boolean opt
263266
boolean applyBoxing;
264267

265268
if (kClass.isValue()) {
266-
267269
wrapperConstructor = kClass.getConstructors().iterator().next();
268270
KParameter nested = wrapperConstructor.getParameters().get(0);
269271
KType nestedType = nested.getType();
@@ -280,10 +282,12 @@ private ValueBoxing(BoxingRules rules, KType type, KClass<?> kClass, boolean opt
280282
}
281283

282284
Assert.notNull(nestedClass, () -> String.format("Cannot resolve nested class from type %s", nestedType));
283-
285+
this.valueProperty = kClass.getMembers().stream().filter(it -> it instanceof KProperty<?>)
286+
.map(KProperty.class::cast).findFirst().get();
284287
next = new ValueBoxing(rules, nestedType, nestedClass, nested.isOptional());
285288
} else {
286289
applyBoxing = false;
290+
this.valueProperty = null;
287291
}
288292

289293
this.kClass = kClass;
@@ -378,20 +382,46 @@ public ValueBoxing getNext() {
378382
}
379383

380384
/**
381-
* Apply wrapping into the boxing wrapper type if applicable.
385+
* Wrap the value into the boxing wrapper type if requested. Already wrapped values are left unchanged.
382386
*
383387
* @param o
384388
* @return
385389
*/
386390
@Nullable
387391
public Object wrap(@Nullable Object o) {
392+
return doWrap(o, false, ValueBoxing::wrap);
393+
}
394+
395+
/**
396+
* Apply wrapping into the boxing wrapper type if applicable. For types, that do not require wrapping but are
397+
* wrapped, the component type is being unwrapped.
398+
*
399+
* @param o
400+
* @return
401+
* @since 3.2.6
402+
*/
403+
@Nullable
404+
Object applyWrapping(@Nullable Object o) {
405+
return doWrap(o, true, ValueBoxing::applyWrapping);
406+
}
407+
408+
/**
409+
* Apply staged wrapping into the boxing wrapper type if value boxing is requested. Otherwise, apply unwrapping and
410+
* pass on the result into {@code nextWrapStage}.
411+
*/
412+
@Nullable
413+
Object doWrap(@Nullable Object o, boolean unwrap, BiFunction<ValueBoxing, Object, Object> nextWrapStage) {
388414

389415
if (applyBoxing) {
390-
return o == null || kClass.isInstance(o) ? o : wrapperConstructor.call(next.wrap(o));
416+
return o == null || kClass.isInstance(o) ? o : wrapperConstructor.call(nextWrapStage.apply(next, o));
417+
} else if (unwrap && kClass.isValue()) {
418+
if (o != null && kClass.isInstance(o)) {
419+
o = valueProperty.getGetter().call(o);
420+
}
391421
}
392422

393423
if (hasNext()) {
394-
return next.wrap(o);
424+
return nextWrapStage.apply(next, o);
395425
}
396426

397427
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)