From 79119000bb48fca2d2ae8413825ed56527d4dfc3 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 22 May 2018 16:08:49 +0200 Subject: [PATCH] =?UTF-8?q?DATACMNS-1322=20-=20Add=20support=20for=20Kotli?= =?UTF-8?q?n's=20copy(=E2=80=A6)=20method.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We now support updating of immutable Kotlin data objects by creating new copies through Kotlin's copy method that is generated along with data classes and immutable properties. data class DataClassKt(val id: String) { } data class ExtendedDataClassKt(val id: String, val name: String) { } --- ...tlinClassGeneratingEntityInstantiator.java | 45 +- .../data/mapping/model/BeanWrapper.java | 52 +++ ...lassGeneratingPropertyAccessorFactory.java | 427 +++++++++++++++--- .../data/util/ReflectionUtils.java | 43 ++ .../PersistentPropertyAccessorTests.java | 38 ++ .../data/mapping/context/SimpleDataClass.kt | 4 +- .../data/mapping/model/DataClassKt.kt | 25 + .../data/mapping/model/ValueClassKt.kt | 22 + 8 files changed, 559 insertions(+), 97 deletions(-) create mode 100644 src/test/kotlin/org/springframework/data/mapping/model/DataClassKt.kt create mode 100644 src/test/kotlin/org/springframework/data/mapping/model/ValueClassKt.kt diff --git a/src/main/java/org/springframework/data/convert/KotlinClassGeneratingEntityInstantiator.java b/src/main/java/org/springframework/data/convert/KotlinClassGeneratingEntityInstantiator.java index 05ecf0253..b82d5ea47 100644 --- a/src/main/java/org/springframework/data/convert/KotlinClassGeneratingEntityInstantiator.java +++ b/src/main/java/org/springframework/data/convert/KotlinClassGeneratingEntityInstantiator.java @@ -214,7 +214,7 @@ public class KotlinClassGeneratingEntityInstantiator extends ClassGeneratingEnti throw new IllegalArgumentException("PreferredConstructor must not be null!"); } - int[] defaulting = new int[(synthetic.getParameterCount() / 32) + 1]; + int[] defaulting = new int[(synthetic.getParameterCount() / Integer.SIZE) + 1]; Object[] params = allocateArguments( synthetic.getParameterCount() + defaulting.length + /* DefaultConstructorMarker */1); @@ -225,8 +225,8 @@ public class KotlinClassGeneratingEntityInstantiator extends ClassGeneratingEnti // Prepare user-space arguments for (int i = 0; i < userParameterCount; i++) { - int slot = i / 32; - int offset = slot * 32; + int slot = i / Integer.SIZE; + int offset = slot * Integer.SIZE; Parameter parameter = parameters.get(i); Class type = parameter.getType().getType(); @@ -240,7 +240,7 @@ public class KotlinClassGeneratingEntityInstantiator extends ClassGeneratingEnti defaulting[slot] = defaulting[slot] | (1 << (i - offset)); if (type.isPrimitive()) { - param = getPrimitiveDefault(type); + param = ReflectionUtils.getPrimitiveDefault(type); } } @@ -254,42 +254,5 @@ public class KotlinClassGeneratingEntityInstantiator extends ClassGeneratingEnti return params; } - - private static Object getPrimitiveDefault(Class type) { - - if (type == Byte.TYPE || type == Byte.class) { - return (byte) 0; - } - - if (type == Short.TYPE || type == Short.class) { - return (short) 0; - } - - if (type == Integer.TYPE || type == Integer.class) { - return 0; - } - - if (type == Long.TYPE || type == Long.class) { - return 0L; - } - - if (type == Float.TYPE || type == Float.class) { - return 0F; - } - - if (type == Double.TYPE || type == Double.class) { - return 0D; - } - - if (type == Character.TYPE || type == Character.class) { - return '\u0000'; - } - - if (type == Boolean.TYPE) { - return Boolean.FALSE; - } - - throw new IllegalArgumentException(String.format("Primitive type %s not supported!", type)); - } } } 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 3a0aad5bd..f0ec9f20b 100644 --- a/src/main/java/org/springframework/data/mapping/model/BeanWrapper.java +++ b/src/main/java/org/springframework/data/mapping/model/BeanWrapper.java @@ -15,8 +15,17 @@ */ 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; + import java.lang.reflect.Field; import java.lang.reflect.Method; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; import org.springframework.data.mapping.MappingException; import org.springframework.data.mapping.PersistentProperty; @@ -60,6 +69,7 @@ class BeanWrapper implements PersistentPropertyAccessor { if (property.isImmutable()) { Method wither = property.getWither(); + if (wither != null) { ReflectionUtils.makeAccessible(wither); @@ -67,6 +77,12 @@ class BeanWrapper implements PersistentPropertyAccessor { return; } + if (org.springframework.data.util.ReflectionUtils.isKotlinClass(property.getOwner().getType())) { + + this.bean = (T) KotlinCopyUtil.setProperty(property, bean, value); + return; + } + throw new UnsupportedOperationException( String.format("Cannot set immutable property %s.%s!", property.getOwner().getName(), property.getName())); } @@ -141,4 +157,40 @@ class BeanWrapper implements PersistentPropertyAccessor { public T getBean() { return bean; } + + /** + * Utility class to leverage Kotlin's copy method for immutable data classes. + */ + static class KotlinCopyUtil { + + static Object setProperty(PersistentProperty property, T bean, @Nullable Object value) { + + Class type = property.getOwner().getType(); + KClass kotlinClass = JvmClassMappingKt.getKotlinClass(type); + + Map args = new LinkedHashMap<>(2, 1); + for (KCallable kCallable : kotlinClass.getMembers()) { + + List parameters = kCallable.getParameters(); + + for (KParameter parameter : parameters) { + + if (parameter.getKind() == Kind.INSTANCE) { + args.put(parameter, bean); + } + + if (parameter.getKind() == Kind.VALUE && parameter.getName().equals(property.getName())) { + args.put(parameter, value); + } + } + + if (kCallable.getName().equals("copy")) { + return kCallable.callBy(args); + } + } + + throw new UnsupportedOperationException(String.format( + "Kotlin class %s has no .copy(…) method. Cannot set property %s!", type.getName(), property.getName())); + } + } } 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 3120c2b6d..e18b0b573 100644 --- a/src/main/java/org/springframework/data/mapping/model/ClassGeneratingPropertyAccessorFactory.java +++ b/src/main/java/org/springframework/data/mapping/model/ClassGeneratingPropertyAccessorFactory.java @@ -17,6 +17,12 @@ package org.springframework.data.mapping.model; import static org.springframework.asm.Opcodes.*; +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; @@ -27,6 +33,7 @@ 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; @@ -36,7 +43,10 @@ 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; @@ -252,6 +262,10 @@ public class ClassGeneratingPropertyAccessorFactory implements PersistentPropert * case 3358: * this.bean = bean.withId(value); * return; + * case 3359: + * this.bean = bean.copy(value); // Kotlin + * case 3360: + * this.bean = PersonWithId.copy$default(bean, value, 0, null); // Kotlin * // ... * } * throw new UnsupportedOperationException( @@ -611,8 +625,7 @@ public class ClassGeneratingPropertyAccessorFactory implements PersistentPropert * Generate property setter/wither initializer. */ private static void visitPropertySetterInitializer(@Nullable Method method, PersistentProperty property, - MethodVisitor mv, - List> entityClasses, String internalClassName, + MethodVisitor mv, List> entityClasses, String internalClassName, Function, String> setterNameFunction, int localVariableIndex) { // method = .class.getDeclaredMethod() @@ -914,6 +927,10 @@ public class ClassGeneratingPropertyAccessorFactory implements PersistentPropert * this.bean = $id_fieldWither.invoke(bean, value); * case 3358: * this.bean = bean.withId(value); + * case 3359: + * this.bean = bean.copy(value); // Kotlin + * case 3360: + * this.bean = PersonWithId.copy$default(bean, value, 0, null); // Kotlin * // ... * } * throw new UnsupportedOperationException( @@ -990,8 +1007,7 @@ public class ClassGeneratingPropertyAccessorFactory implements PersistentPropert mv.visitLabel(propertyStackMap.get(property.getName()).label); mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null); - if ((property.isImmutable() && property.getWither() != null) - || (!property.isImmutable() && (property.getSetter() != null || property.getField() != null))) { + if (supportsMutation(property)) { visitSetProperty0(entity, property, mv, internalClassName); } else { mv.visitJumpInsn(GOTO, dfltLabel); @@ -1013,53 +1029,95 @@ public class ClassGeneratingPropertyAccessorFactory implements PersistentPropert Method setter = property.getSetter(); Method wither = property.getWither(); - if (property.isImmutable() && wither != null) { + if (property.isImmutable()) { - if (generateMethodHandle(entity, wither)) { + if (wither != null) { + if (generateMethodHandle(entity, wither)) { - // this. <- for later PUTFIELD - mv.visitVarInsn(ALOAD, 0); + // this. <- for later PUTFIELD + mv.visitVarInsn(ALOAD, 0); - // $wither.invoke(bean) - mv.visitFieldInsn(GETSTATIC, internalClassName, witherName(property), - BytecodeUtil.referenceName(JAVA_LANG_INVOKE_METHOD_HANDLE)); - mv.visitVarInsn(ALOAD, 3); - mv.visitVarInsn(ALOAD, 2); - mv.visitMethodInsn(INVOKEVIRTUAL, JAVA_LANG_INVOKE_METHOD_HANDLE, "invoke", - String.format("(%s%s)%s", BytecodeUtil.referenceName(JAVA_LANG_OBJECT), - BytecodeUtil.referenceName(JAVA_LANG_OBJECT), getAccessibleTypeReferenceName(entity)), - false); + // $wither.invoke(bean) + mv.visitFieldInsn(GETSTATIC, internalClassName, witherName(property), + BytecodeUtil.referenceName(JAVA_LANG_INVOKE_METHOD_HANDLE)); + mv.visitVarInsn(ALOAD, 3); + mv.visitVarInsn(ALOAD, 2); + mv.visitMethodInsn(INVOKEVIRTUAL, JAVA_LANG_INVOKE_METHOD_HANDLE, "invoke", + String.format("(%s%s)%s", BytecodeUtil.referenceName(JAVA_LANG_OBJECT), + BytecodeUtil.referenceName(JAVA_LANG_OBJECT), getAccessibleTypeReferenceName(entity)), + false); - mv.visitTypeInsn(CHECKCAST, getAccessibleTypeReferenceName(entity)); + mv.visitTypeInsn(CHECKCAST, getAccessibleTypeReferenceName(entity)); + } else { - } else { + // this. <- for later PUTFIELD + mv.visitVarInsn(ALOAD, 0); - // this. <- for later PUTFIELD - mv.visitVarInsn(ALOAD, 0); + // bean.set...(object) + mv.visitVarInsn(ALOAD, 3); + mv.visitVarInsn(ALOAD, 2); - // bean.set...(object) - mv.visitVarInsn(ALOAD, 3); - mv.visitVarInsn(ALOAD, 2); - - Class parameterType = wither.getParameterTypes()[0]; - mv.visitTypeInsn(CHECKCAST, Type.getInternalName(BytecodeUtil.autoboxType(parameterType))); - BytecodeUtil.autoboxIfNeeded(BytecodeUtil.autoboxType(parameterType), parameterType, mv); - - int invokeOpCode = INVOKEVIRTUAL; - Class declaringClass = wither.getDeclaringClass(); - boolean interfaceDefinition = declaringClass.isInterface(); - - if (interfaceDefinition) { - invokeOpCode = INVOKEINTERFACE; + visitInvokeMethodSingleArg(mv, wither); } - mv.visitMethodInsn( - invokeOpCode, Type.getInternalName(wither.getDeclaringClass()), wither.getName(), String.format("(%s)%s", - BytecodeUtil.signatureTypeName(parameterType), BytecodeUtil.referenceName(wither.getReturnType())), - interfaceDefinition); + mv.visitFieldInsn(PUTFIELD, internalClassName, BEAN_FIELD, getAccessibleTypeReferenceName(entity)); + } + + if (hasKotlinCopyMethod(entity.getType())) { + + // this. <- for later PUTFIELD + mv.visitVarInsn(ALOAD, 0); + + Optional publicCopy = findPublicCopyMethod(entity.getType()); + Optional defaultedCopy = findDefaultCopyMethod(entity.getType()); + + if (publicCopy.filter(it -> it.getParameterCount() == 1 && !Modifier.isStatic(it.getModifiers())) + .isPresent()) { + + // PersonWithId.copy$default..(bean, object, MASK, null) + mv.visitVarInsn(ALOAD, 3); + mv.visitVarInsn(ALOAD, 2); + + visitInvokeMethodSingleArg(mv, publicCopy.get()); + } else { + + Method copy = defaultedCopy.get(); + Class[] parameterTypes = copy.getParameterTypes(); + // PersonWithId.copy$default...(object) + mv.visitVarInsn(ALOAD, 3); + + KotlinDefaultCopyMethod defaultMethod = new KotlinDefaultCopyMethod(entity.getType()); + KotlinCopyByProperty kotlinCopyByProperty = defaultMethod.forProperty(property); + for (int i = 1; i < defaultMethod.getParameterCount(); i++) { + + if (kotlinCopyByProperty.getParameterPosition() == i) { + + mv.visitVarInsn(ALOAD, 2); + + mv.visitTypeInsn(CHECKCAST, Type.getInternalName(BytecodeUtil.autoboxType(parameterTypes[i]))); + BytecodeUtil.autoboxIfNeeded(BytecodeUtil.autoboxType(parameterTypes[i]), parameterTypes[i], mv); + + continue; + } + + visitDefaultValue(mv, parameterTypes[i]); + } + + kotlinCopyByProperty.getDefaultMask().forEach(i -> { + mv.visitIntInsn(Opcodes.SIPUSH, i); + }); + + mv.visitInsn(Opcodes.ACONST_NULL); + + int invokeOpCode = getInvokeOp(copy, false); + + mv.visitMethodInsn(invokeOpCode, Type.getInternalName(copy.getDeclaringClass()), copy.getName(), + getArgumentSignature(copy), false); + } + + mv.visitFieldInsn(PUTFIELD, internalClassName, BEAN_FIELD, getAccessibleTypeReferenceName(entity)); } - mv.visitFieldInsn(PUTFIELD, internalClassName, BEAN_FIELD, getAccessibleTypeReferenceName(entity)); } else if (property.usePropertyAccess() && setter != null) { if (generateMethodHandle(entity, setter)) { @@ -1077,20 +1135,7 @@ public class ClassGeneratingPropertyAccessorFactory implements PersistentPropert mv.visitVarInsn(ALOAD, 3); mv.visitVarInsn(ALOAD, 2); - Class parameterType = setter.getParameterTypes()[0]; - mv.visitTypeInsn(CHECKCAST, Type.getInternalName(BytecodeUtil.autoboxType(parameterType))); - BytecodeUtil.autoboxIfNeeded(BytecodeUtil.autoboxType(parameterType), parameterType, mv); - - int invokeOpCode = INVOKEVIRTUAL; - Class declaringClass = setter.getDeclaringClass(); - boolean interfaceDefinition = declaringClass.isInterface(); - - if (interfaceDefinition) { - invokeOpCode = INVOKEINTERFACE; - } - - mv.visitMethodInsn(invokeOpCode, Type.getInternalName(setter.getDeclaringClass()), setter.getName(), - String.format("(%s)V", BytecodeUtil.signatureTypeName(parameterType)), interfaceDefinition); + visitInvokeMethodSingleArg(mv, setter); } } else { @@ -1122,6 +1167,57 @@ public class ClassGeneratingPropertyAccessorFactory implements PersistentPropert mv.visitInsn(RETURN); } + /** + * Creates the method signature containing parameter types (e.g. (Ljava/lang/Object)I for a method accepting + * {@link Object} and returning a primitive {@code int}). + * + * @param method + * @return + */ + private static String getArgumentSignature(Method method) { + + StringBuilder result = new StringBuilder("("); + List argumentTypes = new ArrayList<>(); + + for (Class parameterType : method.getParameterTypes()) { + + result.append("%s"); + argumentTypes.add(BytecodeUtil.signatureTypeName(parameterType)); + } + + result.append(")%s"); + argumentTypes.add(BytecodeUtil.signatureTypeName(method.getReturnType())); + + return String.format(result.toString(), argumentTypes.toArray()); + } + + private static void visitDefaultValue(MethodVisitor mv, Class parameterType) { + if (parameterType.isPrimitive()) { + + if (parameterType == Integer.TYPE || parameterType == Short.TYPE || parameterType == Boolean.TYPE) { + mv.visitInsn(Opcodes.ICONST_0); + } + + if (parameterType == Long.TYPE) { + mv.visitInsn(Opcodes.LCONST_0); + } + + if (parameterType == Double.TYPE) { + mv.visitInsn(Opcodes.DCONST_0); + } + + if (parameterType == Float.TYPE) { + mv.visitInsn(Opcodes.FCONST_0); + } + + if (parameterType == Character.TYPE || parameterType == Byte.TYPE) { + mv.visitIntInsn(Opcodes.BIPUSH, 0); + } + } else { + mv.visitInsn(Opcodes.ACONST_NULL); + } + } + private static void visitAssertNotNull(MethodVisitor mv) { // Assert.notNull(property) @@ -1175,6 +1271,7 @@ public class ClassGeneratingPropertyAccessorFactory implements PersistentPropert } private static String getAccessibleTypeReferenceName(PersistentEntity entity) { + if (isAccessible(entity)) { return BytecodeUtil.referenceName(entity.getType()); } @@ -1230,6 +1327,34 @@ public class ClassGeneratingPropertyAccessorFactory implements PersistentPropert } } + private static void visitInvokeMethodSingleArg(MethodVisitor mv, Method method) { + + Class[] parameterTypes = method.getParameterTypes(); + Class parameterType = parameterTypes[0]; + Class declaringClass = method.getDeclaringClass(); + boolean interfaceDefinition = declaringClass.isInterface(); + + mv.visitTypeInsn(CHECKCAST, Type.getInternalName(BytecodeUtil.autoboxType(parameterType))); + BytecodeUtil.autoboxIfNeeded(BytecodeUtil.autoboxType(parameterType), parameterType, mv); + + int invokeOpCode = getInvokeOp(method, interfaceDefinition); + + mv.visitMethodInsn( + invokeOpCode, Type.getInternalName(method.getDeclaringClass()), method.getName(), String.format("(%s)%s", + BytecodeUtil.signatureTypeName(parameterType), BytecodeUtil.signatureTypeName(method.getReturnType())), + interfaceDefinition); + } + + private static int getInvokeOp(Method method, boolean interfaceDefinition) { + + int invokeOpCode = Modifier.isStatic(method.getModifiers()) ? INVOKESTATIC : INVOKEVIRTUAL; + + if (interfaceDefinition) { + invokeOpCode = INVOKEINTERFACE; + } + return invokeOpCode; + } + private static Map createPropertyStackMap( List> persistentProperties) { @@ -1261,4 +1386,198 @@ public class ClassGeneratingPropertyAccessorFactory implements PersistentPropert return Integer.compare(hash, o.hash); } } + + /** + * @param property + * @return {@literal true} if object mutation is supported. + */ + static boolean supportsMutation(PersistentProperty property) { + + if (property.isImmutable()) { + + if (property.getWither() != null) { + return true; + } + + if (hasKotlinCopyMethod(property.getOwner().getType())) { + return true; + } + } + + return (property.usePropertyAccess() && property.getSetter() != null) + || (property.getField() != null && !Modifier.isFinal(property.getField().getModifiers())); + } + + /** + * Find the Kotlin {@literal copy} method or {@literal copy} method with parameter defaulting. + * + * @param type must not be {@literal null}. + * @return + */ + @Nullable + static Method findCopyMethod(Class type) { + + 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) { + + if (BytecodeUtil.isAccessible(type) && org.springframework.data.util.ReflectionUtils.isKotlinClass(type)) { + return findCopyMethod(type) != null; + } + + 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/util/ReflectionUtils.java b/src/main/java/org/springframework/data/util/ReflectionUtils.java index e07bf9f8c..e8468e64e 100644 --- a/src/main/java/org/springframework/data/util/ReflectionUtils.java +++ b/src/main/java/org/springframework/data/util/ReflectionUtils.java @@ -405,6 +405,49 @@ public class ReflectionUtils { return !parameter.getParameterType().isPrimitive(); } + /** + * Get default value for a primitive type. + * + * @param type must not be {@literal null}. + * @return boxed primitive default value. + */ + public static Object getPrimitiveDefault(Class type) { + + if (type == Byte.TYPE || type == Byte.class) { + return (byte) 0; + } + + if (type == Short.TYPE || type == Short.class) { + return (short) 0; + } + + if (type == Integer.TYPE || type == Integer.class) { + return 0; + } + + if (type == Long.TYPE || type == Long.class) { + return 0L; + } + + if (type == Float.TYPE || type == Float.class) { + return 0F; + } + + if (type == Double.TYPE || type == Double.class) { + return 0D; + } + + if (type == Character.TYPE || type == Character.class) { + return '\u0000'; + } + + if (type == Boolean.TYPE) { + return Boolean.FALSE; + } + + throw new IllegalArgumentException(String.format("Primitive type %s not supported!", type)); + } + /** * Reflection utility methods specific to Kotlin reflection. */ 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 3dd5c4591..11eaf624a 100644 --- a/src/test/java/org/springframework/data/mapping/model/PersistentPropertyAccessorTests.java +++ b/src/test/java/org/springframework/data/mapping/model/PersistentPropertyAccessorTests.java @@ -84,6 +84,34 @@ public class PersistentPropertyAccessorTests { assertThat(accessor.getProperty(property)).isEqualTo("value"); } + @Test // DATACMNS-1322 + public void shouldSetKotlinDataClassProperty() { + + DataClassKt bean = new DataClassKt("foo"); + PersistentPropertyAccessor accessor = propertyAccessorFunction.apply(bean); + SamplePersistentProperty property = getProperty(bean, "id"); + + accessor.setProperty(property, "value"); + + assertThat(accessor.getBean()).hasFieldOrPropertyWithValue("id", "value"); + assertThat(accessor.getBean()).isNotSameAs(bean); + assertThat(accessor.getProperty(property)).isEqualTo("value"); + } + + @Test // DATACMNS-1322 + public void shouldSetExtendedKotlinDataClassProperty() { + + ExtendedDataClassKt bean = new ExtendedDataClassKt("foo", "bar"); + PersistentPropertyAccessor accessor = propertyAccessorFunction.apply(bean); + SamplePersistentProperty property = getProperty(bean, "id"); + + accessor.setProperty(property, "value"); + + assertThat(accessor.getBean()).hasFieldOrPropertyWithValue("id", "value"); + assertThat(accessor.getBean()).isNotSameAs(bean); + assertThat(accessor.getProperty(property)).isEqualTo("value"); + } + @Test // DATACMNS-1322 public void shouldWitherProperty() { @@ -108,6 +136,16 @@ public class PersistentPropertyAccessorTests { assertThatThrownBy(() -> accessor.setProperty(property, "value")).isInstanceOf(UnsupportedOperationException.class); } + @Test // DATACMNS-1322 + public void shouldRejectImmutableKotlinClassPropertyUpdate() { + + ValueClassKt bean = new ValueClassKt("foo"); + PersistentPropertyAccessor accessor = propertyAccessorFunction.apply(bean); + SamplePersistentProperty property = getProperty(bean, "immutable"); + + assertThatThrownBy(() -> accessor.setProperty(property, "value")).isInstanceOf(UnsupportedOperationException.class); + } + private static SamplePersistentProperty getProperty(Object bean, String propertyName) { return mappingContext.getRequiredPersistentEntity(bean.getClass()).getRequiredPersistentProperty(propertyName); diff --git a/src/test/kotlin/org/springframework/data/mapping/context/SimpleDataClass.kt b/src/test/kotlin/org/springframework/data/mapping/context/SimpleDataClass.kt index b8e7ece3d..2c3bb44f2 100644 --- a/src/test/kotlin/org/springframework/data/mapping/context/SimpleDataClass.kt +++ b/src/test/kotlin/org/springframework/data/mapping/context/SimpleDataClass.kt @@ -18,5 +18,5 @@ package org.springframework.data.mapping.context /** * @author Mark Paluch */ -data class SimpleDataClass(val firstname: String) { -} \ No newline at end of file +data class SimpleDataClass(val firstname: String, val lastname: String) { +} diff --git a/src/test/kotlin/org/springframework/data/mapping/model/DataClassKt.kt b/src/test/kotlin/org/springframework/data/mapping/model/DataClassKt.kt new file mode 100644 index 000000000..6a6c99e85 --- /dev/null +++ b/src/test/kotlin/org/springframework/data/mapping/model/DataClassKt.kt @@ -0,0 +1,25 @@ +/* + * 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 + +/** + * @author Mark Paluch + */ +data class DataClassKt(val id: String) { +} + +data class ExtendedDataClassKt(val id: String, val name: String) { +} diff --git a/src/test/kotlin/org/springframework/data/mapping/model/ValueClassKt.kt b/src/test/kotlin/org/springframework/data/mapping/model/ValueClassKt.kt new file mode 100644 index 000000000..cf04097b0 --- /dev/null +++ b/src/test/kotlin/org/springframework/data/mapping/model/ValueClassKt.kt @@ -0,0 +1,22 @@ +/* + * 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 + +/** + * @author Mark Paluch + */ +class ValueClassKt(val immutable: String) { +}