diff --git a/src/main/java/org/springframework/data/mapping/model/BeanWrapper.java b/src/main/java/org/springframework/data/mapping/model/BeanWrapper.java index edc0bf429..55c45f8bd 100644 --- a/src/main/java/org/springframework/data/mapping/model/BeanWrapper.java +++ b/src/main/java/org/springframework/data/mapping/model/BeanWrapper.java @@ -15,9 +15,7 @@ */ package org.springframework.data.mapping.model; -import kotlin.jvm.JvmClassMappingKt; import kotlin.reflect.KCallable; -import kotlin.reflect.KClass; import kotlin.reflect.KParameter; import kotlin.reflect.KParameter.Kind; @@ -32,6 +30,7 @@ import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.lang.Nullable; import org.springframework.util.Assert; +import org.springframework.util.ConcurrentReferenceHashMap; import org.springframework.util.ReflectionUtils; /** @@ -165,6 +164,8 @@ class BeanWrapper implements PersistentPropertyAccessor { */ static class KotlinCopyUtil { + private static final Map, KCallable> COPY_METHOD_CACHE = new ConcurrentReferenceHashMap<>(); + /** * Set a single property by calling {@code copy(…)} on a Kotlin data class. Copying creates a new instance that * holds all values of the original instance and the newly set {@link PersistentProperty} value. @@ -174,12 +175,11 @@ class BeanWrapper implements PersistentPropertyAccessor { static Object setProperty(PersistentProperty property, T bean, @Nullable Object value) { Class type = property.getOwner().getType(); - KClass kotlinClass = JvmClassMappingKt.getKotlinClass(type); - KCallable copy = getCopyMethod(kotlinClass); + KCallable copy = COPY_METHOD_CACHE.computeIfAbsent(type, it -> getCopyMethod(it, property)); if (copy == null) { throw new UnsupportedOperationException(String.format( - "Kotlin class %s has no .copy(…) method. Cannot set property %s!", type.getName(), property.getName())); + "Kotlin class %s has no .copy(…) method for property %s!", type.getName(), property.getName())); } return copy.callBy(getCallArgs(copy, property, bean, value)); @@ -198,7 +198,8 @@ class BeanWrapper implements PersistentPropertyAccessor { args.put(parameter, bean); } - if (parameter.getKind() == Kind.VALUE && parameter.getName().equals(property.getName())) { + if (parameter.getKind() == Kind.VALUE && parameter.getName() != null + && parameter.getName().equals(property.getName())) { args.put(parameter, value); } } @@ -206,16 +207,9 @@ class BeanWrapper implements PersistentPropertyAccessor { } @Nullable - private static KCallable getCopyMethod(KClass kotlinClass) { - - for (KCallable kCallable : kotlinClass.getMembers()) { - - if (kCallable.getName().equals("copy")) { - return kCallable; - } - } - - return null; + private static KCallable getCopyMethod(Class type, PersistentProperty property) { + return KotlinCopyMethod.findCopyMethod(type).filter(it -> it.supportsProperty(property)) + .map(KotlinCopyMethod::getCopyFunction).orElse(null); } } } diff --git a/src/main/java/org/springframework/data/mapping/model/ClassGeneratingPropertyAccessorFactory.java b/src/main/java/org/springframework/data/mapping/model/ClassGeneratingPropertyAccessorFactory.java index d0a7acccc..9bf2c28ea 100644 --- a/src/main/java/org/springframework/data/mapping/model/ClassGeneratingPropertyAccessorFactory.java +++ b/src/main/java/org/springframework/data/mapping/model/ClassGeneratingPropertyAccessorFactory.java @@ -18,12 +18,6 @@ package org.springframework.data.mapping.model; import static org.springframework.asm.Opcodes.*; import static org.springframework.data.mapping.model.BytecodeUtil.*; -import kotlin.reflect.KFunction; -import kotlin.reflect.KParameter; -import kotlin.reflect.KParameter.Kind; -import kotlin.reflect.jvm.ReflectJvmMapping; -import lombok.AccessLevel; -import lombok.Getter; import lombok.NonNull; import lombok.RequiredArgsConstructor; @@ -34,7 +28,6 @@ import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.security.ProtectionDomain; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; @@ -44,10 +37,7 @@ import java.util.Optional; import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Function; -import java.util.function.IntConsumer; -import java.util.function.Predicate; import java.util.stream.Collectors; -import java.util.stream.Stream; import org.springframework.asm.ClassWriter; import org.springframework.asm.Label; @@ -60,6 +50,7 @@ import org.springframework.data.mapping.PersistentProperty; 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.util.Optionals; import org.springframework.data.util.TypeInformation; import org.springframework.lang.Nullable; @@ -1010,7 +1001,7 @@ public class ClassGeneratingPropertyAccessorFactory implements PersistentPropert visitWithProperty(entity, property, mv, internalClassName, wither); } - if (hasKotlinCopyMethod(entity.getType())) { + if (hasKotlinCopyMethod(property)) { visitKotlinCopy(entity, property, mv, internalClassName); } @@ -1079,31 +1070,36 @@ public class ClassGeneratingPropertyAccessorFactory implements PersistentPropert private static void visitKotlinCopy(PersistentEntity entity, PersistentProperty property, MethodVisitor mv, String internalClassName) { + KotlinCopyMethod kotlinCopyMethod = KotlinCopyMethod.findCopyMethod(entity.getType()) + .orElseThrow(() -> new IllegalStateException( + String.format("No usable .copy(…) method found in entity %s", entity.getType().getName()))); + // this. <- for later PUTFIELD mv.visitVarInsn(ALOAD, 0); - Optional publicCopy = findPublicCopyMethod(entity.getType()); - Optional defaultedCopy = findDefaultCopyMethod(entity.getType()); + if (kotlinCopyMethod.shouldUsePublicCopyMethod(entity)) { - if (publicCopy.filter(it -> it.getParameterCount() == 1 && !Modifier.isStatic(it.getModifiers())).isPresent()) { - // PersonWithId.copy$default..(bean, object, MASK, null) + // PersonWithId.copy$(value) mv.visitVarInsn(ALOAD, 3); mv.visitVarInsn(ALOAD, 2); - visitInvokeMethodSingleArg(mv, publicCopy.get()); + visitInvokeMethodSingleArg(mv, kotlinCopyMethod.getPublicCopyMethod()); } else { - Method copy = defaultedCopy.get(); + Method copy = kotlinCopyMethod.getSyntheticCopyMethod(); Class[] parameterTypes = copy.getParameterTypes(); - // PersonWithId.copy$default...(object) + + // PersonWithId.copy$default..(bean, object, MASK, null) mv.visitVarInsn(ALOAD, 3); - KotlinDefaultCopyMethod defaultMethod = new KotlinDefaultCopyMethod(entity.getType()); - KotlinCopyByProperty kotlinCopyByProperty = defaultMethod.forProperty(property); - for (int i = 1; i < defaultMethod.getParameterCount(); i++) { + KotlinCopyByProperty copyByProperty = kotlinCopyMethod.forProperty(property) + .orElseThrow(() -> new IllegalStateException( + String.format("No usable .copy(…) method found for property %s", property))); - if (kotlinCopyByProperty.getParameterPosition() == i) { + for (int i = 1; i < kotlinCopyMethod.getParameterCount(); i++) { + + if (copyByProperty.getParameterPosition() == i) { mv.visitVarInsn(ALOAD, 2); @@ -1116,7 +1112,7 @@ public class ClassGeneratingPropertyAccessorFactory implements PersistentPropert visitDefaultValue(parameterTypes[i], mv); } - kotlinCopyByProperty.getDefaultMask().forEach(i -> { + copyByProperty.getDefaultMask().forEach(i -> { mv.visitIntInsn(Opcodes.SIPUSH, i); }); @@ -1414,7 +1410,7 @@ public class ClassGeneratingPropertyAccessorFactory implements PersistentPropert return true; } - if (hasKotlinCopyMethod(property.getOwner().getType())) { + if (hasKotlinCopyMethod(property)) { return true; } } @@ -1424,175 +1420,20 @@ public class ClassGeneratingPropertyAccessorFactory implements PersistentPropert } /** - * Find the Kotlin {@literal copy} method or {@literal copy} method with parameter defaulting. + * 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}. * @return */ - @Nullable - static Method findCopyMethod(Class type) { + private static boolean hasKotlinCopyMethod(PersistentProperty property) { - Optional singleCopy = findPublicCopyMethod(type); - - return singleCopy.orElseGet(() -> findDefaultCopyMethod(type).orElse(null)); - } - - private static Optional findPublicCopyMethod(Class type) { - - return Stream.concat(Arrays.stream(type.getDeclaredMethods()), Arrays.stream(type.getMethods())) - .filter(it -> it.getName().equals("copy") // - && !it.isSynthetic() // - && !it.isBridge() // - && !Modifier.isStatic(it.getModifiers()) // - && it.getReturnType().equals(type)) - .findFirst(); - } - - private static Optional findDefaultCopyMethod(Class type) { - - return Stream.concat(Arrays.stream(type.getDeclaredMethods()), Arrays.stream(type.getMethods())) - .filter(it -> it.getName().equals("copy$default") // - && it.isSynthetic() // - && it.isBridge() // - && Modifier.isStatic(it.getModifiers()) // - && it.getReturnType().equals(type)) - .findFirst(); - } - - /** - * Check whether the {@link Class} declares a {@literal copy} method or {@literal copy} method with parameter - * defaulting. - * - * @param type must not be {@literal null}. - * @return - */ - private static boolean hasKotlinCopyMethod(Class type) { + Class type = property.getOwner().getType(); if (isAccessible(type) && org.springframework.data.util.ReflectionUtils.isKotlinClass(type)) { - return findCopyMethod(type) != null; + return KotlinCopyMethod.findCopyMethod(type).filter(it -> it.supportsProperty(property)).isPresent(); } return false; } - - /** - * Value object to represent a Kotlin default method. - * - * @author Mark Paluch - */ - @Getter - static class KotlinDefaultCopyMethod { - - private final int parameterCount; - private final KFunction copyFunction; - - public KotlinDefaultCopyMethod(Class type) { - - Method copyMethod = findPublicCopyMethod(type) - .orElseThrow(() -> new IllegalArgumentException("Cannot resolve public Kotlin copy() method!")); - - this.copyFunction = ReflectJvmMapping.getKotlinFunction(copyMethod); - this.parameterCount = copyFunction.getParameters().size(); - } - - /** - * Create metadata for {@literal copy$default} invocation. - * - * @param property - * @return - */ - public KotlinCopyByProperty forProperty(PersistentProperty property) { - return new KotlinCopyByProperty(copyFunction, property); - } - } - - /** - * Value object to represent Kotlin {@literal copy$default} invocation metadata. - * - * @author Mark Paluch - */ - @Getter - static class KotlinCopyByProperty { - - private final int parameterPosition; - private final int parameterCount; - private final KotlinDefaultMask defaultMask; - - KotlinCopyByProperty(KFunction copyFunction, PersistentProperty property) { - - this.parameterPosition = findIndex(copyFunction, property.getName()); - this.parameterCount = copyFunction.getParameters().size(); - this.defaultMask = KotlinDefaultMask.from(copyFunction, it -> property.getName().equals(it.getName())); - } - - private static int findIndex(KFunction function, String parameterName) { - - for (KParameter parameter : function.getParameters()) { - if (parameterName.equals(parameter.getName())) { - return parameter.getIndex(); - } - } - - throw new IllegalArgumentException(String.format("Cannot resolve parameter name %s to a index in method %s!", - parameterName, function.getName())); - } - } - - /** - * Value object representing defaulting masks used for Kotlin methods applying parameter defaulting. - */ - @RequiredArgsConstructor(access = AccessLevel.PRIVATE) - static class KotlinDefaultMask { - - private final int[] defaulting; - - /** - * Callback method to notify {@link IntConsumer} for each defaulting mask. - * - * @param maskCallback must not be {@literal null}. - */ - public void forEach(IntConsumer maskCallback) { - - for (int i : defaulting) { - maskCallback.accept(i); - } - } - - /** - * Creates defaulting mask(s) used to invoke Kotlin {@literal default} methods that conditionally apply parameter - * values. - * - * @param function the {@link KFunction} that should be invoked. - * @param isPresent {@link Predicate} for the presence/absence of parameters. - * @return {@link KotlinDefaultMask}. - */ - public static KotlinDefaultMask from(KFunction function, Predicate isPresent) { - - List masks = new ArrayList<>(); - int index = 0; - int mask = 0; - - List parameters = function.getParameters(); - - for (KParameter parameter : parameters) { - - if (index != 0 && index % Integer.SIZE == 0) { - masks.add(mask); - mask = 0; - } - - if (parameter.isOptional() && !isPresent.test(parameter)) { - mask = mask | (1 << (index % Integer.SIZE)); - } - - if (parameter.getKind() == Kind.VALUE) { - index++; - } - } - - masks.add(mask); - - return new KotlinDefaultMask(masks.stream().mapToInt(i -> i).toArray()); - } - } } diff --git a/src/main/java/org/springframework/data/mapping/model/KotlinCopyMethod.java b/src/main/java/org/springframework/data/mapping/model/KotlinCopyMethod.java new file mode 100644 index 000000000..a414c1685 --- /dev/null +++ b/src/main/java/org/springframework/data/mapping/model/KotlinCopyMethod.java @@ -0,0 +1,249 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mapping.model; + +import kotlin.jvm.JvmClassMappingKt; +import kotlin.reflect.KClass; +import kotlin.reflect.KFunction; +import kotlin.reflect.KParameter; +import kotlin.reflect.KParameter.Kind; +import kotlin.reflect.full.KClasses; +import kotlin.reflect.jvm.ReflectJvmMapping; +import lombok.Getter; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import org.springframework.data.mapping.PersistentEntity; +import org.springframework.data.mapping.PersistentProperty; +import org.springframework.data.mapping.SimplePropertyHandler; +import org.springframework.util.Assert; + +/** + * Value object to represent a Kotlin {@code copy} method. + * + * @author Mark Paluch + * @since 2.1 + */ +@Getter +class KotlinCopyMethod { + + private final Method publicCopyMethod; + private final Method syntheticCopyMethod; + private final int parameterCount; + private final KFunction copyFunction; + + /** + * @param publicCopyMethod Compiler-generated public {@code copy} method accepting all properties. + * @param syntheticCopyMethod Compiler-generated synthetic {@code copy$default} variant of the copy method accepting + * the original instance and defaulting masks. + */ + private KotlinCopyMethod(Method publicCopyMethod, Method syntheticCopyMethod) { + + this.publicCopyMethod = publicCopyMethod; + this.syntheticCopyMethod = syntheticCopyMethod; + this.copyFunction = ReflectJvmMapping.getKotlinFunction(publicCopyMethod); + this.parameterCount = copyFunction.getParameters().size(); + } + + /** + * Attempt to lookup the Kotlin {@code copy} method. Lookup happens in two stages: Find the synthetic copy method and + * then attempt to resolve its public variant. + * + * @param type the class. + * @return {@link Optional} {@link KotlinCopyMethod}. + */ + public static Optional findCopyMethod(Class type) { + + Assert.notNull(type, "Type must not be null!"); + + Optional syntheticCopyMethod = findSyntheticCopyMethod(type); + + if (!syntheticCopyMethod.isPresent()) { + return Optional.empty(); + } + + Optional publicCopyMethod = syntheticCopyMethod.flatMap(KotlinCopyMethod::findPublicCopyMethod); + + return publicCopyMethod.map(method -> new KotlinCopyMethod(method, syntheticCopyMethod.get())); + } + + /** + * Check whether the {@link PersistentProperty} is accepted as part of the copy method. + * + * @param property + * @return + */ + boolean supportsProperty(PersistentProperty property) { + return forProperty(property).isPresent(); + } + + /** + * Create metadata for {@literal copy$default} invocation. + * + * @param property + * @return + */ + Optional forProperty(PersistentProperty property) { + + int index = KotlinCopyByProperty.findIndex(copyFunction, property.getName()); + + if (index == -1) { + return Optional.empty(); + } + + return Optional.of(new KotlinCopyByProperty(copyFunction, property)); + } + + boolean shouldUsePublicCopyMethod(PersistentEntity entity) { + + List> persistentProperties = new ArrayList<>(); + entity.doWithProperties((SimplePropertyHandler) persistentProperties::add); + + if (persistentProperties.size() > 1) { + return false; + } + + if (publicCopyMethod.getParameterCount() != 1) { + return false; + } + + if (Modifier.isStatic(publicCopyMethod.getModifiers())) { + return false; + } + + Class[] parameterTypes = publicCopyMethod.getParameterTypes(); + + for (int i = 0; i < parameterTypes.length; i++) { + if (!parameterTypes[i].equals(persistentProperties.get(i).getType())) { + return false; + } + } + + return true; + } + + @SuppressWarnings("unchecked") + private static Optional findPublicCopyMethod(Method defaultKotlinMethod) { + + Class type = defaultKotlinMethod.getDeclaringClass(); + KClass kotlinClass = JvmClassMappingKt.getKotlinClass(type); + + KFunction primaryConstructor = KClasses.getPrimaryConstructor(kotlinClass); + + if (primaryConstructor == null) { + return Optional.empty(); + } + + List constructorArguments = primaryConstructor.getParameters() // + .stream() // + .filter(it -> it.getKind() == Kind.VALUE) // + .collect(Collectors.toList()); + + return Arrays.stream(type.getDeclaredMethods()).filter(it -> it.getName().equals("copy") // + && !it.isSynthetic() // + && !it.isBridge() // + && !Modifier.isStatic(it.getModifiers()) // + && it.getReturnType().equals(type) // + && it.getParameterCount() == constructorArguments.size()) // + .filter(it -> { + + KFunction kotlinFunction = ReflectJvmMapping.getKotlinFunction(it); + + if (kotlinFunction == null) { + return false; + } + + return parameterMatches(constructorArguments, kotlinFunction); + }).findFirst(); + } + + private static boolean parameterMatches(List constructorArguments, KFunction kotlinFunction) { + + boolean foundInstance = false; + int constructorArgIndex = 0; + + for (KParameter parameter : kotlinFunction.getParameters()) { + + if (parameter.getKind() == Kind.INSTANCE) { + foundInstance = true; + continue; + } + + if (constructorArguments.size() <= constructorArgIndex) { + return false; + } + + KParameter constructorParameter = constructorArguments.get(constructorArgIndex); + + if (!constructorParameter.getName().equals(parameter.getName()) + || !constructorParameter.getType().equals(parameter.getType())) { + return false; + } + + constructorArgIndex++; + } + + return foundInstance; + } + + private static Optional findSyntheticCopyMethod(Class type) { + + return Arrays.stream(type.getDeclaredMethods()) // + .filter(it -> it.getName().equals("copy$default") // + && Modifier.isStatic(it.getModifiers()) // + && it.getReturnType().equals(type)) + .filter(Method::isSynthetic) // + .filter(Method::isBridge) // + .findFirst(); + } + + /** + * Value object to represent Kotlin {@literal copy$default} invocation metadata. + * + * @author Mark Paluch + */ + @Getter + static class KotlinCopyByProperty { + + private final int parameterPosition; + private final int parameterCount; + private final KotlinDefaultMask defaultMask; + + KotlinCopyByProperty(KFunction copyFunction, PersistentProperty property) { + + this.parameterPosition = findIndex(copyFunction, property.getName()); + this.parameterCount = copyFunction.getParameters().size(); + this.defaultMask = KotlinDefaultMask.from(copyFunction, it -> property.getName().equals(it.getName())); + } + + static int findIndex(KFunction function, String parameterName) { + + for (KParameter parameter : function.getParameters()) { + if (parameterName.equals(parameter.getName())) { + return parameter.getIndex(); + } + } + + return -1; + } + } +} diff --git a/src/main/java/org/springframework/data/mapping/model/KotlinDefaultMask.java b/src/main/java/org/springframework/data/mapping/model/KotlinDefaultMask.java new file mode 100644 index 000000000..fd458bda3 --- /dev/null +++ b/src/main/java/org/springframework/data/mapping/model/KotlinDefaultMask.java @@ -0,0 +1,85 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mapping.model; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.IntConsumer; +import java.util.function.Predicate; + +import kotlin.reflect.KFunction; +import kotlin.reflect.KParameter; +import kotlin.reflect.KParameter.Kind; +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; + +/** + * Value object representing defaulting masks used for Kotlin methods applying parameter defaulting. + */ +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +class KotlinDefaultMask { + + private final int[] defaulting; + + /** + * Callback method to notify {@link IntConsumer} for each defaulting mask. + * + * @param maskCallback must not be {@literal null}. + */ + public void forEach(IntConsumer maskCallback) { + + for (int i : defaulting) { + maskCallback.accept(i); + } + } + + /** + * Creates defaulting mask(s) used to invoke Kotlin {@literal default} methods that conditionally apply parameter + * values. + * + * @param function the {@link KFunction} that should be invoked. + * @param isPresent {@link Predicate} for the presence/absence of parameters. + * @return {@link KotlinDefaultMask}. + */ + public static KotlinDefaultMask from(KFunction function, Predicate isPresent) { + + List masks = new ArrayList<>(); + int index = 0; + int mask = 0; + + List parameters = function.getParameters(); + + for (KParameter parameter : parameters) { + + if (index != 0 && index % Integer.SIZE == 0) { + masks.add(mask); + mask = 0; + } + + if (parameter.isOptional() && !isPresent.test(parameter)) { + mask = mask | (1 << (index % Integer.SIZE)); + } + + if (parameter.getKind() == Kind.VALUE) { + index++; + } + } + + masks.add(mask); + + return new KotlinDefaultMask(masks.stream().mapToInt(i -> i).toArray()); + } +} diff --git a/src/test/java/org/springframework/data/mapping/model/PersistentPropertyAccessorTests.java b/src/test/java/org/springframework/data/mapping/model/PersistentPropertyAccessorTests.java index 47e831b32..a98cea6a3 100644 --- a/src/test/java/org/springframework/data/mapping/model/PersistentPropertyAccessorTests.java +++ b/src/test/java/org/springframework/data/mapping/model/PersistentPropertyAccessorTests.java @@ -21,8 +21,10 @@ import lombok.Data; import lombok.Value; import lombok.experimental.Wither; +import java.sql.Timestamp; import java.util.ArrayList; import java.util.List; +import java.util.UUID; import java.util.function.Function; import org.junit.Test; @@ -98,18 +100,42 @@ public class PersistentPropertyAccessorTests { assertThat(accessor.getProperty(property)).isEqualTo("value"); } - @Test // DATACMNS-1322 + @Test // DATACMNS-1322, DATACMNS-1391 public void shouldSetExtendedKotlinDataClassProperty() { - ExtendedDataClassKt bean = new ExtendedDataClassKt("foo", "bar"); + ExtendedDataClassKt bean = new ExtendedDataClassKt(0, "bar"); PersistentPropertyAccessor accessor = propertyAccessorFunction.apply(bean); SamplePersistentProperty property = getProperty(bean, "id"); - accessor.setProperty(property, "value"); + accessor.setProperty(property, 1L); - assertThat(accessor.getBean()).hasFieldOrPropertyWithValue("id", "value"); + assertThat(accessor.getBean()).hasFieldOrPropertyWithValue("id", 1L); assertThat(accessor.getBean()).isNotSameAs(bean); - assertThat(accessor.getProperty(property)).isEqualTo("value"); + assertThat(accessor.getProperty(property)).isEqualTo(1L); + } + + @Test // DATACMNS-1391 + public void shouldUseKotlinGeneratedCopyMethod() { + + UnusedCustomCopy bean = new UnusedCustomCopy(new Timestamp(100)); + PersistentPropertyAccessor accessor = propertyAccessorFunction.apply(bean); + SamplePersistentProperty property = getProperty(bean, "date"); + + accessor.setProperty(property, new Timestamp(200)); + + assertThat(accessor.getBean()).hasFieldOrPropertyWithValue("date", new Timestamp(200)); + assertThat(accessor.getBean()).isNotSameAs(bean); + assertThat(accessor.getProperty(property)).isEqualTo(new Timestamp(200)); + } + + @Test // DATACMNS-1391 + public void kotlinCopyMethodShouldNotSetUnsettableProperty() { + + SingleSettableProperty bean = new SingleSettableProperty(UUID.randomUUID()); + PersistentPropertyAccessor accessor = propertyAccessorFunction.apply(bean); + SamplePersistentProperty property = getProperty(bean, "version"); + + assertThatThrownBy(() -> accessor.setProperty(property, 1)).isInstanceOf(UnsupportedOperationException.class); } @Test // DATACMNS-1322 diff --git a/src/test/kotlin/org/springframework/data/mapping/model/DataClasses.kt b/src/test/kotlin/org/springframework/data/mapping/model/DataClasses.kt new file mode 100644 index 000000000..1df197b33 --- /dev/null +++ b/src/test/kotlin/org/springframework/data/mapping/model/DataClasses.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mapping.model + +import java.util.* + +/** + * @author Mark Paluch + */ +data class DataClassKt(val id: String) { +} + +data class ExtendedDataClassKt(val id: Long, val name: String) { + fun copy(name: String, id: Long): ExtendedDataClassKt { + throw UnsupportedOperationException("Wrong copy method") + } +} + +data class SingleSettableProperty constructor(val id: UUID = UUID.randomUUID()) { + val version: Int? = null +} diff --git a/src/test/kotlin/org/springframework/data/mapping/model/DataClassKt.kt b/src/test/kotlin/org/springframework/data/mapping/model/UnusedCustomCopy.kt similarity index 73% rename from src/test/kotlin/org/springframework/data/mapping/model/DataClassKt.kt rename to src/test/kotlin/org/springframework/data/mapping/model/UnusedCustomCopy.kt index 6a6c99e85..1ff1ea10a 100644 --- a/src/test/kotlin/org/springframework/data/mapping/model/DataClassKt.kt +++ b/src/test/kotlin/org/springframework/data/mapping/model/UnusedCustomCopy.kt @@ -13,13 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.mapping.model +package org.springframework.data.mapping.model; + +import java.sql.Timestamp /** * @author Mark Paluch */ -data class DataClassKt(val id: String) { -} +data class UnusedCustomCopy internal constructor(val date: Timestamp) { -data class ExtendedDataClassKt(val id: String, val name: String) { + fun copy(date: Long): UnusedCustomCopy { + return UnusedCustomCopy(Timestamp(date)) + } }