Skip to content

Add support for Kotlin Value Classes #2866

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 11 commits into from
9 changes: 8 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

<groupId>org.springframework.data</groupId>
<artifactId>spring-data-commons</artifactId>
<version>3.2.0-SNAPSHOT</version>
<version>3.2.0-GH-2806-SNAPSHOT</version>

<name>Spring Data Core</name>
<description>Core Spring concepts underpinning every Spring Data module.</description>
Expand Down Expand Up @@ -33,6 +33,7 @@
<scala>2.11.7</scala>
<xmlbeam>1.4.24</xmlbeam>
<java-module-name>spring.data.commons</java-module-name>
<kotlin.api.target>1.8</kotlin.api.target>

</properties>

Expand Down Expand Up @@ -126,6 +127,12 @@
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId>
<version>${kotlin}</version>
</dependency>

<!-- RxJava -->

<dependency>
Expand Down
24 changes: 24 additions & 0 deletions src/main/asciidoc/object-mapping.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -436,3 +436,27 @@ You can exclude properties by annotating these with `@Transient`.
2. How to represent properties in your data store?
Using the same field/column name for different values typically leads to corrupt data so you should annotate least one of the properties using an explicit field/column name.
3. Using `@AccessType(PROPERTY)` cannot be used as the super-property cannot be set.

[[mapping.kotlin.value.classes]]
=== Kotlin Value Classes

Kotlin Value Classes are designed for a more expressive domain model to make underlying concepts explicit.
Spring Data can read and write types that define properties using Value Classes.

Consider the following domain model:

====
[source,kotlin]
----
@JvmInline
value class EmailAddress(val theAddress: String) <1>

data class Contact(val id: String, val name:String, val emailAddress: EmailAddress) <2>
----

<1> A simple value class with a non-nullable value type.
<2> Data class defining a property using the `EmailAddress` value class.
====

NOTE: Non-nullable properties using non-primitive value types are flattened in the compiled class to the value type.
Nullable primitive value types or nullable value-in-value types are represented with their wrapper type and that affects how value types are represented in the database.
Original file line number Diff line number Diff line change
Expand Up @@ -168,8 +168,8 @@ static <T> Object setProperty(PersistentProperty<?> property, T bean, @Nullable
KCallable<?> copy = copyMethodCache.computeIfAbsent(type, it -> getCopyMethod(it, property));

if (copy == null) {
throw new UnsupportedOperationException(String.format(
"Kotlin class %s has no .copy(…) method for property %s", type.getName(), property.getName()));
throw new UnsupportedOperationException(String.format("Kotlin class %s has no .copy(…) method for property %s",
type.getName(), property.getName()));
}

return copy.callBy(getCallArgs(copy, property, bean, value));
Expand All @@ -179,7 +179,6 @@ private static <T> Map<KParameter, Object> getCallArgs(KCallable<?> callable, Pe
T bean, @Nullable Object value) {

Map<KParameter, Object> args = new LinkedHashMap<>(2, 1);

List<KParameter> parameters = callable.getParameters();

for (KParameter parameter : parameters) {
Expand All @@ -190,7 +189,8 @@ private static <T> Map<KParameter, Object> getCallArgs(KCallable<?> callable, Pe

if (parameter.getKind() == Kind.VALUE && parameter.getName() != null
&& parameter.getName().equals(property.getName())) {
args.put(parameter, value);

args.put(parameter, KotlinValueUtils.getCopyValueHierarchy(parameter).wrap(value));
}
}
return args;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,15 @@
import static org.springframework.asm.Opcodes.*;
import static org.springframework.data.mapping.model.BytecodeUtil.*;

import kotlin.reflect.KParameter;
import kotlin.reflect.KParameter.Kind;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Parameter;
import java.security.ProtectionDomain;
import java.util.ArrayList;
import java.util.Collections;
Expand All @@ -48,12 +52,16 @@
import org.springframework.data.mapping.PersistentPropertyAccessor;
import org.springframework.data.mapping.SimpleAssociationHandler;
import org.springframework.data.mapping.SimplePropertyHandler;
import org.springframework.data.mapping.model.KotlinCopyMethod.KotlinCopyByProperty;
import org.springframework.data.mapping.model.KotlinValueUtils.ValueBoxing;
import org.springframework.data.util.Optionals;
import org.springframework.data.util.TypeInformation;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ConcurrentLruCache;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;

/**
* A factory that can generate byte code to speed-up dynamic property access. Uses the {@link PersistentEntity}'s
Expand All @@ -76,6 +84,9 @@ public class ClassGeneratingPropertyAccessorFactory implements PersistentPropert
private volatile Map<TypeInformation<?>, Class<PersistentPropertyAccessor<?>>> propertyAccessorClasses = new HashMap<>(
32);

private final ConcurrentLruCache<PersistentProperty<?>, Function<Object, Object>> wrapperCache = new ConcurrentLruCache<>(
256, KotlinValueBoxingAdapter::getWrapper);

@Override
public <T> PersistentPropertyAccessor<T> getPropertyAccessor(PersistentEntity<?, ?> entity, T bean) {

Expand All @@ -96,7 +107,14 @@ public <T> PersistentPropertyAccessor<T> getPropertyAccessor(PersistentEntity<?,
args[0] = bean;

try {
return (PersistentPropertyAccessor<T>) constructor.newInstance(args);

PersistentPropertyAccessor<T> accessor = (PersistentPropertyAccessor<T>) constructor.newInstance(args);

if (KotlinDetector.isKotlinType(entity.getType())) {
return new KotlinValueBoxingAdapter<>(entity, accessor, wrapperCache);
}

return accessor;
} catch (Exception e) {
throw new IllegalArgumentException(String.format("Cannot create persistent property accessor for %s", entity), e);
} finally {
Expand Down Expand Up @@ -1431,7 +1449,7 @@ static boolean supportsMutation(PersistentProperty<?> property) {
* Check whether the owning type of {@link PersistentProperty} declares a {@literal copy} method or {@literal copy}
* method with parameter defaulting.
*
* @param type must not be {@literal null}.
* @param property must not be {@literal null}.
* @return
*/
private static boolean hasKotlinCopyMethod(PersistentProperty<?> property) {
Expand All @@ -1444,4 +1462,70 @@ private static boolean hasKotlinCopyMethod(PersistentProperty<?> property) {

return false;
}

/**
* Adapter to encapsulate Kotlin's value class boxing when properties are nullable.
*
* @param entity the entity that could use value class properties.
* @param delegate the property accessor to delegate to.
* @param wrapperCache cache for wrapping functions.
* @param <T>
* @since 3.2
*/
record KotlinValueBoxingAdapter<T> (PersistentEntity<?, ?> entity, PersistentPropertyAccessor<T> delegate,
ConcurrentLruCache<PersistentProperty<?>, Function<Object, Object>> wrapperCache)
implements
PersistentPropertyAccessor<T> {

@Override
public void setProperty(PersistentProperty<?> property, @Nullable Object value) {
delegate.setProperty(property, wrapperCache.get(property).apply(value));
}

/**
* Create a wrapper function if the {@link PersistentProperty} uses value classes.
*
* @param property the persistent property to inspect.
* @return a wrapper function to wrap a value class component into the hierarchy of value classes or
* {@link Function#identity()} if wrapping is not necessary.
* @see KotlinValueUtils#getCopyValueHierarchy(KParameter)
*/
static Function<Object, Object> getWrapper(PersistentProperty<?> property) {

Optional<KotlinCopyMethod> kotlinCopyMethod = KotlinCopyMethod.findCopyMethod(property.getOwner().getType())
.filter(it -> it.supportsProperty(property));

if (kotlinCopyMethod.isPresent()
&& kotlinCopyMethod.filter(it -> it.forProperty(property).isPresent()).isPresent()) {
KotlinCopyMethod copyMethod = kotlinCopyMethod.get();

Optional<KParameter> kParameter = kotlinCopyMethod.stream()
.flatMap(it -> it.getCopyFunction().getParameters().stream()) //
.filter(kf -> kf.getKind() == Kind.VALUE) //
.filter(kf -> StringUtils.hasText(kf.getName())) //
.filter(kf -> kf.getName().equals(property.getName())) //
.findFirst();

ValueBoxing vh = kParameter.map(KotlinValueUtils::getCopyValueHierarchy).orElse(null);
KotlinCopyByProperty kotlinCopyByProperty = copyMethod.forProperty(property).get();
Method copy = copyMethod.getSyntheticCopyMethod();

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

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

return Function.identity();
}

@Override
public Object getProperty(PersistentProperty<?> property) {
return delegate.getProperty(property);
}

@Override
public T getBean() {
return delegate.getBean();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@
*/
package org.springframework.data.mapping.model;

import kotlin.jvm.JvmClassMappingKt;
import kotlin.reflect.KCallable;
import kotlin.reflect.KClass;
import kotlin.reflect.KProperty;

import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Constructor;
Expand All @@ -24,6 +29,7 @@
import java.util.List;

import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.KotlinDetector;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.data.annotation.PersistenceCreator;
Expand Down Expand Up @@ -55,7 +61,8 @@ class InstanceCreatorMetadataDiscoverer {
* @return
*/
@Nullable
public static <T, P extends PersistentProperty<P>> InstanceCreatorMetadata<P> discover(PersistentEntity<T, P> entity) {
public static <T, P extends PersistentProperty<P>> InstanceCreatorMetadata<P> discover(
PersistentEntity<T, P> entity) {

Constructor<?>[] declaredConstructors = entity.getType().getDeclaredConstructors();
Method[] declaredMethods = entity.getType().getDeclaredMethods();
Expand All @@ -78,6 +85,34 @@ public static <T, P extends PersistentProperty<P>> InstanceCreatorMetadata<P> di
}
}

if (KotlinDetector.isKotlinReflectPresent() && KotlinDetector.isKotlinType(entity.getType())) {

KClass<?> kClass = JvmClassMappingKt.getKotlinClass(entity.getType());
// We use box-impl as factory for classes.
if (kClass.isValue()) {

String propertyName = "";
for (KCallable<?> member : kClass.getMembers()) {
if (member instanceof KProperty<?>) {
propertyName = member.getName();
break;
}
}

for (Method declaredMethod : entity.getType().getDeclaredMethods()) {
if (declaredMethod.getName().equals("box-impl") && declaredMethod.isSynthetic()
&& declaredMethod.getParameterCount() == 1) {

Annotation[][] parameterAnnotations = declaredMethod.getParameterAnnotations();
List<TypeInformation<?>> types = entity.getTypeInformation().getParameterTypes(declaredMethod);

return new FactoryMethod<>(declaredMethod,
new Parameter<>(propertyName, (TypeInformation) types.get(0), parameterAnnotations[0], entity));
}
}
}
}

return PreferredConstructorDiscoverer.discover(entity);
}

Expand Down
Loading