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) { +}