From 7eacab6a34c94d5a5c5b0345f00532d79fb5c113 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 12 Sep 2018 15:46:33 +0200 Subject: [PATCH] DATACMNS-1391 - Use prefered constructors to discover Kotlin's copy method. We now use the primary constructor of Kotlin classes to discover the copy method. We use the primary constructor args to compare signatures and only use the copy method that takes all parameters in the constructor args order. This allows to find the appropriate copy method in case the class declares multiple copy methods. We now also cache the copy method (KCallable) to reduce lookups in BeanWrapper. Original pull request: #312. --- .../data/mapping/model/BeanWrapper.java | 26 +- ...lassGeneratingPropertyAccessorFactory.java | 209 ++------------- .../data/mapping/model/KotlinCopyMethod.java | 249 ++++++++++++++++++ .../data/mapping/model/KotlinDefaultMask.java | 85 ++++++ .../PersistentPropertyAccessorTests.java | 36 ++- .../data/mapping/model/DataClasses.kt | 34 +++ .../{DataClassKt.kt => UnusedCustomCopy.kt} | 11 +- 7 files changed, 441 insertions(+), 209 deletions(-) create mode 100644 src/main/java/org/springframework/data/mapping/model/KotlinCopyMethod.java create mode 100644 src/main/java/org/springframework/data/mapping/model/KotlinDefaultMask.java create mode 100644 src/test/kotlin/org/springframework/data/mapping/model/DataClasses.kt rename src/test/kotlin/org/springframework/data/mapping/model/{DataClassKt.kt => UnusedCustomCopy.kt} (73%) 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)) + } }