diff --git a/src/main/java/org/springframework/data/mapping/PersistentProperty.java b/src/main/java/org/springframework/data/mapping/PersistentProperty.java
index 2814bf36b..b975228ac 100644
--- a/src/main/java/org/springframework/data/mapping/PersistentProperty.java
+++ b/src/main/java/org/springframework/data/mapping/PersistentProperty.java
@@ -113,6 +113,27 @@ public interface PersistentProperty
> {
return setter;
}
+ /**
+ * Returns the wither method to set a property value on a new object instance. Might return {@literal null} in case
+ * there is no wither available.
+ *
+ * @return the wither method to set a property value on a new object instance if available, otherwise {@literal null}.
+ * @since 2.1
+ */
+ @Nullable
+ Method getWither();
+
+ default Method getRequiredWither() {
+
+ Method wither = getWither();
+
+ if (wither == null) {
+ throw new IllegalArgumentException("No wither available for this persistent property!");
+ }
+
+ return wither;
+ }
+
@Nullable
Field getField();
@@ -221,6 +242,15 @@ public interface PersistentProperty
> {
*/
boolean isWritable();
+ /**
+ * Returns whether the current property is immutable, i.e. if there is no setter or the backing {@link Field} is
+ * {@code final}.
+ *
+ * @return
+ * @since 2.1
+ */
+ boolean isImmutable();
+
/**
* Returns whether the property is an {@link Association}.
*
@@ -321,7 +351,7 @@ public interface PersistentProperty
> {
/**
* Returns whether the actual type of the property carries the given annotation.
- *
+ *
* @param annotationType must not be {@literal null}.
* @return
* @since 2.1
diff --git a/src/main/java/org/springframework/data/mapping/PersistentPropertyAccessor.java b/src/main/java/org/springframework/data/mapping/PersistentPropertyAccessor.java
index 85ed388c9..42e5e88e7 100644
--- a/src/main/java/org/springframework/data/mapping/PersistentPropertyAccessor.java
+++ b/src/main/java/org/springframework/data/mapping/PersistentPropertyAccessor.java
@@ -23,8 +23,12 @@ import org.springframework.util.Assert;
* Domain service to allow accessing and setting {@link PersistentProperty}s of an entity. Usually obtained through
* {@link PersistentEntity#getPropertyAccessor(Object)}. In case type conversion shall be applied on property access,
* use a {@link ConvertingPropertyAccessor}.
+ *
+ * This service supports mutation for immutable classes by creating new object instances. These are managed as state of
+ * {@link PersistentPropertyAccessor} and must be obtained from {@link #getBean()} after processing all updates.
*
* @author Oliver Gierke
+ * @author Mark Paluch
* @since 1.10
* @see PersistentEntity#getPropertyAccessor(Object)
* @see ConvertingPropertyAccessor
@@ -44,7 +48,7 @@ public interface PersistentPropertyAccessor {
/**
* Sets the given value for the {@link PersistentProperty} pointed to by the given {@link PersistentPropertyPath}. The
* lookup of intermediate values must not yield {@literal null}.
- *
+ *
* @param path must not be {@literal null} or empty.
* @param value can be {@literal null}.
* @since 2.1
@@ -84,7 +88,7 @@ public interface PersistentPropertyAccessor {
/**
* Return the value pointed to by the given {@link PersistentPropertyPath}. If the given path is empty, the wrapped
* bean is returned.
- *
+ *
* @param path must not be {@literal null}.
* @return
* @since 2.1
@@ -119,7 +123,7 @@ public interface PersistentPropertyAccessor {
}
/**
- * Returns the underlying bean.
+ * Returns the underlying bean. May change between {@link #setProperty(PersistentProperty, Object)} calls.
*
* @return will never be {@literal null}.
*/
diff --git a/src/main/java/org/springframework/data/mapping/model/AbstractPersistentProperty.java b/src/main/java/org/springframework/data/mapping/model/AbstractPersistentProperty.java
index aa6610f9c..c2fdf4814 100644
--- a/src/main/java/org/springframework/data/mapping/model/AbstractPersistentProperty.java
+++ b/src/main/java/org/springframework/data/mapping/model/AbstractPersistentProperty.java
@@ -20,6 +20,7 @@ import lombok.Getter;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
import java.util.Collections;
import java.util.Map;
import java.util.Optional;
@@ -65,6 +66,8 @@ public abstract class AbstractPersistentProperty
private final @Getter(onMethod = @__(@Nullable)) Method getter;
private final @Getter(onMethod = @__(@Nullable)) Method setter;
private final @Getter(onMethod = @__(@Nullable)) Field field;
+ private final @Getter(onMethod = @__(@Nullable)) Method wither;
+ private final boolean immutable;
public AbstractPersistentProperty(Property property, PersistentEntity, P> owner,
SimpleTypeHolder simpleTypeHolder) {
@@ -90,6 +93,13 @@ public abstract class AbstractPersistentProperty
this.getter = property.getGetter().orElse(null);
this.setter = property.getSetter().orElse(null);
this.field = property.getField().orElse(null);
+ this.wither = property.getWither().orElse(null);
+
+ if (setter == null && (field == null || Modifier.isFinal(field.getModifiers()))) {
+ this.immutable = true;
+ } else {
+ this.immutable = false;
+ }
}
protected abstract Association
createAssociation();
@@ -130,7 +140,7 @@ public abstract class AbstractPersistentProperty
return information;
}
- /*
+ /*
* (non-Javadoc)
* @see org.springframework.data.mapping.PersistentProperty#getPersistentEntityTypes()
*/
@@ -174,6 +184,15 @@ public abstract class AbstractPersistentProperty
return !isTransient();
}
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.mapping.PersistentProperty#isImmutable()
+ */
+ @Override
+ public boolean isImmutable() {
+ return immutable;
+ }
+
/*
* (non-Javadoc)
* @see org.springframework.data.mapping.PersistentProperty#isAssociation()
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 1e74a5d75..3a0aad5bd 100644
--- a/src/main/java/org/springframework/data/mapping/model/BeanWrapper.java
+++ b/src/main/java/org/springframework/data/mapping/model/BeanWrapper.java
@@ -33,7 +33,7 @@ import org.springframework.util.ReflectionUtils;
*/
class BeanWrapper implements PersistentPropertyAccessor {
- private final T bean;
+ private T bean;
/**
* Creates a new {@link BeanWrapper} for the given bean.
@@ -50,12 +50,27 @@ class BeanWrapper implements PersistentPropertyAccessor {
* (non-Javadoc)
* @see org.springframework.data.mapping.PersistentPropertyAccessor#setProperty(org.springframework.data.mapping.PersistentProperty, java.util.Optional)
*/
+ @SuppressWarnings("unchecked")
public void setProperty(PersistentProperty> property, @Nullable Object value) {
Assert.notNull(property, "PersistentProperty must not be null!");
try {
+ if (property.isImmutable()) {
+
+ Method wither = property.getWither();
+ if (wither != null) {
+
+ ReflectionUtils.makeAccessible(wither);
+ this.bean = (T) ReflectionUtils.invokeMethod(wither, bean, value);
+ return;
+ }
+
+ throw new UnsupportedOperationException(
+ String.format("Cannot set immutable property %s.%s!", property.getOwner().getName(), property.getName()));
+ }
+
if (!property.usePropertyAccess()) {
Field field = property.getRequiredField();
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 2caae95b2..eb1125583 100644
--- a/src/main/java/org/springframework/data/mapping/model/ClassGeneratingPropertyAccessorFactory.java
+++ b/src/main/java/org/springframework/data/mapping/model/ClassGeneratingPropertyAccessorFactory.java
@@ -35,6 +35,7 @@ import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Function;
import java.util.stream.Collectors;
import org.springframework.asm.ClassWriter;
@@ -246,6 +247,11 @@ public class ClassGeneratingPropertyAccessorFactory implements PersistentPropert
* case 3355:
* $id_fieldSetter.invoke(bean, value);
* return;
+ * case 3357:
+ * this.bean = $id_fieldWither.invoke(bean, value);
+ * case 3358:
+ * this.bean = bean.withId(value);
+ * return;
* // ...
* }
* throw new UnsupportedOperationException(
@@ -364,20 +370,20 @@ public class ClassGeneratingPropertyAccessorFactory implements PersistentPropert
cw.visitInnerClass(JAVA_LANG_INVOKE_METHOD_HANDLES_LOOKUP, JAVA_LANG_INVOKE_METHOD_HANDLES, "Lookup",
ACC_PRIVATE + ACC_FINAL + ACC_STATIC);
- boolean accessibleType = isAccessible(entity);
-
- if (accessibleType) {
- cw.visitField(ACC_PRIVATE + ACC_FINAL, BEAN_FIELD, referenceName(Type.getInternalName(entity.getType())), null,
- null).visitEnd();
- } else {
- cw.visitField(ACC_PRIVATE + ACC_FINAL, BEAN_FIELD, referenceName(JAVA_LANG_OBJECT), null, null).visitEnd();
- }
+ cw.visitField(ACC_PRIVATE, BEAN_FIELD, getAccessibleTypeReferenceName(entity), null, null).visitEnd();
for (PersistentProperty> property : persistentProperties) {
- if (generateMethodHandle(entity, property.getSetter())) {
- cw.visitField(ACC_PRIVATE + ACC_FINAL + ACC_STATIC, setterName(property),
- referenceName(JAVA_LANG_INVOKE_METHOD_HANDLE), null, null).visitEnd();
+ if (property.isImmutable()) {
+ if (generateMethodHandle(entity, property.getWither())) {
+ cw.visitField(ACC_PRIVATE + ACC_FINAL + ACC_STATIC, witherName(property),
+ referenceName(JAVA_LANG_INVOKE_METHOD_HANDLE), null, null).visitEnd();
+ }
+ } else {
+ if (generateMethodHandle(entity, property.getSetter())) {
+ cw.visitField(ACC_PRIVATE + ACC_FINAL + ACC_STATIC, setterName(property),
+ referenceName(JAVA_LANG_INVOKE_METHOD_HANDLE), null, null).visitEnd();
+ }
}
if (generateMethodHandle(entity, property.getGetter())) {
@@ -387,8 +393,11 @@ public class ClassGeneratingPropertyAccessorFactory implements PersistentPropert
if (generateSetterMethodHandle(entity, property.getField())) {
- cw.visitField(ACC_PRIVATE + ACC_FINAL + ACC_STATIC, fieldSetterName(property),
- referenceName(JAVA_LANG_INVOKE_METHOD_HANDLE), null, null).visitEnd();
+ if (!property.isImmutable()) {
+ cw.visitField(ACC_PRIVATE + ACC_FINAL + ACC_STATIC, fieldSetterName(property),
+ referenceName(JAVA_LANG_INVOKE_METHOD_HANDLE), null, null).visitEnd();
+ }
+
cw.visitField(ACC_PRIVATE + ACC_FINAL + ACC_STATIC, fieldGetterName(property),
referenceName(JAVA_LANG_INVOKE_METHOD_HANDLE), null, null).visitEnd();
}
@@ -412,13 +421,8 @@ public class ClassGeneratingPropertyAccessorFactory implements PersistentPropert
// public EntityAccessor(Entity bean) or EntityAccessor(Object bean)
MethodVisitor mv;
- boolean accessibleType = isAccessible(entity);
- if (accessibleType) {
- mv = cw.visitMethod(ACC_PUBLIC, INIT, String.format("(%s)V", referenceName(entity.getType())), null, null);
- } else {
- mv = cw.visitMethod(ACC_PUBLIC, INIT, String.format("(%s)V", referenceName(JAVA_LANG_OBJECT)), null, null);
- }
+ mv = cw.visitMethod(ACC_PUBLIC, INIT, String.format("(%s)V", getAccessibleTypeReferenceName(entity)), null, null);
mv.visitCode();
Label l0 = new Label();
@@ -436,22 +440,13 @@ public class ClassGeneratingPropertyAccessorFactory implements PersistentPropert
mv.visitVarInsn(ALOAD, 0);
mv.visitVarInsn(ALOAD, 1);
- if (accessibleType) {
- mv.visitFieldInsn(PUTFIELD, internalClassName, BEAN_FIELD, referenceName(entity.getType()));
- } else {
- mv.visitFieldInsn(PUTFIELD, internalClassName, BEAN_FIELD, referenceName(JAVA_LANG_OBJECT));
- }
+ mv.visitFieldInsn(PUTFIELD, internalClassName, BEAN_FIELD, getAccessibleTypeReferenceName(entity));
mv.visitInsn(RETURN);
Label l3 = new Label();
mv.visitLabel(l3);
mv.visitLocalVariable(THIS_REF, referenceName(internalClassName), null, l0, l3, 0);
-
- if (accessibleType) {
- mv.visitLocalVariable(BEAN_FIELD, referenceName(Type.getInternalName(entity.getType())), null, l0, l3, 1);
- } else {
- mv.visitLocalVariable(BEAN_FIELD, referenceName(JAVA_LANG_OBJECT), null, l0, l3, 1);
- }
+ mv.visitLocalVariable(BEAN_FIELD, getAccessibleTypeReferenceName(entity), null, l0, l3, 1);
mv.visitMaxs(2, 2);
}
@@ -496,7 +491,7 @@ public class ClassGeneratingPropertyAccessorFactory implements PersistentPropert
mv.visitLdcInsn(entityClass.getName());
mv.visitMethodInsn(INVOKESTATIC, JAVA_LANG_CLASS, "forName",
String.format("(%s)%s", referenceName(JAVA_LANG_STRING), referenceName(JAVA_LANG_CLASS)), false);
- mv.visitVarInsn(ASTORE, classVariableIndex4(entityClasses, entityClass));
+ mv.visitVarInsn(ASTORE, classVariableIndex5(entityClasses, entityClass));
}
for (PersistentProperty> property : persistentProperties) {
@@ -508,7 +503,17 @@ public class ClassGeneratingPropertyAccessorFactory implements PersistentPropert
}
if (generateMethodHandle(entity, property.getSetter())) {
- visitPropertySetterInitializer(property, mv, entityClasses, internalClassName);
+ visitPropertySetterInitializer(property.getSetter(), property, mv, entityClasses, internalClassName,
+ PropertyAccessorClassGenerator::setterName, 2);
+ }
+ }
+
+ if (property.isImmutable()) {
+ if (generateMethodHandle(entity, property.getWither())) {
+ if (generateMethodHandle(entity, property.getWither())) {
+ visitPropertySetterInitializer(property.getWither(), property, mv, entityClasses, internalClassName,
+ PropertyAccessorClassGenerator::witherName, 4);
+ }
}
}
@@ -524,10 +529,11 @@ public class ClassGeneratingPropertyAccessorFactory implements PersistentPropert
mv.visitLocalVariable("field", referenceName(JAVA_LANG_REFLECT_FIELD), null, l0, l1, 1);
mv.visitLocalVariable("setter", referenceName(JAVA_LANG_REFLECT_METHOD), null, l0, l1, 2);
mv.visitLocalVariable("getter", referenceName(JAVA_LANG_REFLECT_METHOD), null, l0, l1, 3);
+ mv.visitLocalVariable("wither", referenceName(JAVA_LANG_REFLECT_METHOD), null, l0, l1, 4);
for (Class> entityClass : entityClasses) {
- int index = classVariableIndex4(entityClasses, entityClass);
+ int index = classVariableIndex5(entityClasses, entityClass);
mv.visitLocalVariable(String.format("class_%d", index), referenceName(JAVA_LANG_CLASS), null, l0, l1, index);
}
@@ -567,7 +573,7 @@ public class ClassGeneratingPropertyAccessorFactory implements PersistentPropert
if (getter != null) {
- mv.visitVarInsn(ALOAD, classVariableIndex4(entityClasses, getter.getDeclaringClass()));
+ mv.visitVarInsn(ALOAD, classVariableIndex5(entityClasses, getter.getDeclaringClass()));
mv.visitLdcInsn(getter.getName());
mv.visitInsn(ICONST_0);
mv.visitTypeInsn(ANEWARRAY, JAVA_LANG_CLASS);
@@ -597,28 +603,28 @@ public class ClassGeneratingPropertyAccessorFactory implements PersistentPropert
}
/**
- * Generate property setter initializer.
+ * Generate property setter/wither initializer.
*/
- private static void visitPropertySetterInitializer(PersistentProperty> property, MethodVisitor mv,
- List> entityClasses, String internalClassName) {
+ private static void visitPropertySetterInitializer(Method method, PersistentProperty> property, MethodVisitor mv,
+ List> entityClasses, String internalClassName,
+ Function, String> setterNameFunction, int localVariableIndex) {
- // setter = .class.getDeclaredMethod()
- Method setter = property.getSetter();
+ // method = .class.getDeclaredMethod()
- if (setter != null) {
+ if (method != null) {
- mv.visitVarInsn(ALOAD, classVariableIndex4(entityClasses, setter.getDeclaringClass()));
- mv.visitLdcInsn(setter.getName());
+ mv.visitVarInsn(ALOAD, classVariableIndex5(entityClasses, method.getDeclaringClass()));
+ mv.visitLdcInsn(method.getName());
mv.visitInsn(ICONST_1);
mv.visitTypeInsn(ANEWARRAY, JAVA_LANG_CLASS);
mv.visitInsn(DUP);
mv.visitInsn(ICONST_0);
- Class> parameterType = setter.getParameterTypes()[0];
+ Class> parameterType = method.getParameterTypes()[0];
if (parameterType.isPrimitive()) {
- mv.visitFieldInsn(GETSTATIC, Type.getInternalName(autoboxType(setter.getParameterTypes()[0])), "TYPE",
+ mv.visitFieldInsn(GETSTATIC, Type.getInternalName(autoboxType(method.getParameterTypes()[0])), "TYPE",
referenceName(JAVA_LANG_CLASS));
} else {
mv.visitLdcInsn(Type.getType(referenceName(parameterType)));
@@ -629,24 +635,24 @@ public class ClassGeneratingPropertyAccessorFactory implements PersistentPropert
mv.visitMethodInsn(INVOKEVIRTUAL, JAVA_LANG_CLASS, "getDeclaredMethod", String.format("(%s[%s)%s",
referenceName(JAVA_LANG_STRING), referenceName(JAVA_LANG_CLASS), referenceName(JAVA_LANG_REFLECT_METHOD)),
false);
- mv.visitVarInsn(ASTORE, 2);
+ mv.visitVarInsn(ASTORE, localVariableIndex);
- // setter.setAccessible(true)
- mv.visitVarInsn(ALOAD, 2);
+ // wither/setter.setAccessible(true)
+ mv.visitVarInsn(ALOAD, localVariableIndex);
mv.visitInsn(ICONST_1);
mv.visitMethodInsn(INVOKEVIRTUAL, JAVA_LANG_REFLECT_METHOD, SET_ACCESSIBLE, "(Z)V", false);
mv.visitVarInsn(ALOAD, 0);
- mv.visitVarInsn(ALOAD, 2);
+ mv.visitVarInsn(ALOAD, localVariableIndex);
mv.visitMethodInsn(INVOKEVIRTUAL, JAVA_LANG_INVOKE_METHOD_HANDLES_LOOKUP, "unreflect", String.format("(%s)%s",
referenceName(JAVA_LANG_REFLECT_METHOD), referenceName(JAVA_LANG_INVOKE_METHOD_HANDLE)), false);
}
- if (setter == null) {
+ if (method == null) {
mv.visitInsn(ACONST_NULL);
}
- mv.visitFieldInsn(PUTSTATIC, internalClassName, setterName(property),
+ mv.visitFieldInsn(PUTSTATIC, internalClassName, setterNameFunction.apply(property),
referenceName(JAVA_LANG_INVOKE_METHOD_HANDLE));
}
@@ -661,7 +667,7 @@ public class ClassGeneratingPropertyAccessorFactory implements PersistentPropert
Field field = property.getField();
if (field != null) {
- mv.visitVarInsn(ALOAD, classVariableIndex4(entityClasses, field.getDeclaringClass()));
+ mv.visitVarInsn(ALOAD, classVariableIndex5(entityClasses, field.getDeclaringClass()));
mv.visitLdcInsn(field.getName());
mv.visitMethodInsn(INVOKEVIRTUAL, JAVA_LANG_CLASS, "getDeclaredField",
String.format("(%s)%s", referenceName(JAVA_LANG_STRING), referenceName(JAVA_LANG_REFLECT_FIELD)), false);
@@ -680,13 +686,16 @@ public class ClassGeneratingPropertyAccessorFactory implements PersistentPropert
mv.visitFieldInsn(PUTSTATIC, internalClassName, fieldGetterName(property),
referenceName(JAVA_LANG_INVOKE_METHOD_HANDLE));
- // $fieldSetter = lookup.unreflectSetter(field)
- mv.visitVarInsn(ALOAD, 0);
- mv.visitVarInsn(ALOAD, 1);
- mv.visitMethodInsn(INVOKEVIRTUAL, JAVA_LANG_INVOKE_METHOD_HANDLES_LOOKUP, "unreflectSetter", String.format(
- "(%s)%s", referenceName(JAVA_LANG_REFLECT_FIELD), referenceName(JAVA_LANG_INVOKE_METHOD_HANDLE)), false);
- mv.visitFieldInsn(PUTSTATIC, internalClassName, fieldSetterName(property),
- referenceName(JAVA_LANG_INVOKE_METHOD_HANDLE));
+ if (!property.isImmutable()) {
+
+ // $fieldSetter = lookup.unreflectSetter(field)
+ mv.visitVarInsn(ALOAD, 0);
+ mv.visitVarInsn(ALOAD, 1);
+ mv.visitMethodInsn(INVOKEVIRTUAL, JAVA_LANG_INVOKE_METHOD_HANDLES_LOOKUP, "unreflectSetter", String.format(
+ "(%s)%s", referenceName(JAVA_LANG_REFLECT_FIELD), referenceName(JAVA_LANG_INVOKE_METHOD_HANDLE)), false);
+ mv.visitFieldInsn(PUTSTATIC, internalClassName, fieldSetterName(property),
+ referenceName(JAVA_LANG_INVOKE_METHOD_HANDLE));
+ }
}
}
@@ -702,11 +711,7 @@ public class ClassGeneratingPropertyAccessorFactory implements PersistentPropert
mv.visitLabel(l0);
mv.visitVarInsn(ALOAD, 0);
- if (isAccessible(entity)) {
- mv.visitFieldInsn(GETFIELD, internalClassName, BEAN_FIELD, referenceName(entity.getType()));
- } else {
- mv.visitFieldInsn(GETFIELD, internalClassName, BEAN_FIELD, referenceName(JAVA_LANG_OBJECT));
- }
+ mv.visitFieldInsn(GETFIELD, internalClassName, BEAN_FIELD, getAccessibleTypeReferenceName(entity));
mv.visitInsn(ARETURN);
@@ -758,11 +763,8 @@ public class ClassGeneratingPropertyAccessorFactory implements PersistentPropert
mv.visitVarInsn(ALOAD, 0);
- if (isAccessible(entity)) {
- mv.visitFieldInsn(GETFIELD, internalClassName, BEAN_FIELD, referenceName(entity.getType()));
- } else {
- mv.visitFieldInsn(GETFIELD, internalClassName, BEAN_FIELD, referenceName(JAVA_LANG_OBJECT));
- }
+ mv.visitFieldInsn(GETFIELD, internalClassName, BEAN_FIELD, getAccessibleTypeReferenceName(entity));
+
mv.visitVarInsn(ASTORE, 2);
visitGetPropertySwitch(entity, persistentProperties, internalClassName, mv);
@@ -774,11 +776,7 @@ public class ClassGeneratingPropertyAccessorFactory implements PersistentPropert
mv.visitLocalVariable("property", referenceName(PERSISTENT_PROPERTY),
"Lorg/springframework/data/mapping/PersistentProperty<*>;", l0, l1, 1);
- if (isAccessible(entity)) {
- mv.visitLocalVariable(BEAN_FIELD, referenceName(entity.getType()), null, l0, l1, 2);
- } else {
- mv.visitLocalVariable(BEAN_FIELD, referenceName(JAVA_LANG_OBJECT), null, l0, l1, 2);
- }
+ mv.visitLocalVariable(BEAN_FIELD, getAccessibleTypeReferenceName(entity), null, l0, l1, 2);
mv.visitMaxs(0, 0);
mv.visitEnd();
@@ -897,6 +895,10 @@ public class ClassGeneratingPropertyAccessorFactory implements PersistentPropert
* case 3355:
* $id_fieldSetter.invoke(bean, value);
* return;
+ * case 3357:
+ * this.bean = $id_fieldWither.invoke(bean, value);
+ * case 3358:
+ * this.bean = bean.withId(value);
* // ...
* }
* throw new UnsupportedOperationException(
@@ -920,11 +922,7 @@ public class ClassGeneratingPropertyAccessorFactory implements PersistentPropert
mv.visitVarInsn(ALOAD, 0);
- if (isAccessible(entity)) {
- mv.visitFieldInsn(GETFIELD, internalClassName, BEAN_FIELD, referenceName(entity.getType()));
- } else {
- mv.visitFieldInsn(GETFIELD, internalClassName, BEAN_FIELD, referenceName(JAVA_LANG_OBJECT));
- }
+ mv.visitFieldInsn(GETFIELD, internalClassName, BEAN_FIELD, getAccessibleTypeReferenceName(entity));
mv.visitVarInsn(ASTORE, 3);
@@ -940,11 +938,7 @@ public class ClassGeneratingPropertyAccessorFactory implements PersistentPropert
"Lorg/springframework/data/mapping/PersistentProperty<*>;", l0, l1, 1);
mv.visitLocalVariable("value", referenceName(JAVA_LANG_OBJECT), null, l0, l1, 2);
- if (isAccessible(entity)) {
- mv.visitLocalVariable(BEAN_FIELD, referenceName(entity.getType()), null, l0, l1, 3);
- } else {
- mv.visitLocalVariable(BEAN_FIELD, referenceName(JAVA_LANG_OBJECT), null, l0, l1, 3);
- }
+ mv.visitLocalVariable(BEAN_FIELD, getAccessibleTypeReferenceName(entity), null, l0, l1, 3);
mv.visitMaxs(0, 0);
mv.visitEnd();
@@ -981,7 +975,8 @@ public class ClassGeneratingPropertyAccessorFactory implements PersistentPropert
mv.visitLabel(propertyStackMap.get(property.getName()).label);
mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
- if (property.getSetter() != null || property.getField() != null) {
+ if ((property.isImmutable() && property.getWither() != null)
+ || (!property.isImmutable() && (property.getSetter() != null || property.getField() != null))) {
visitSetProperty0(entity, property, mv, internalClassName);
} else {
mv.visitJumpInsn(GOTO, dfltLabel);
@@ -1000,10 +995,59 @@ public class ClassGeneratingPropertyAccessorFactory implements PersistentPropert
private static void visitSetProperty0(PersistentEntity, ?> entity, PersistentProperty> property,
MethodVisitor mv, String internalClassName) {
+
Method setter = property.getSetter();
- if (property.usePropertyAccess() && setter != null) {
+ Method wither = property.getWither();
+
+ if (property.isImmutable() && wither != null) {
+
+ if (generateMethodHandle(entity, wither)) {
+
+ // this. <- for later PUTFIELD
+ mv.visitVarInsn(ALOAD, 0);
+
+ // $wither.invoke(bean)
+ mv.visitFieldInsn(GETSTATIC, internalClassName, witherName(property),
+ 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",
+ referenceName(JAVA_LANG_OBJECT), referenceName(JAVA_LANG_OBJECT), getAccessibleTypeReferenceName(entity)),
+ false);
+
+ mv.visitTypeInsn(CHECKCAST, getAccessibleTypeReferenceName(entity));
+
+ } else {
+
+ // this. <- for later PUTFIELD
+ mv.visitVarInsn(ALOAD, 0);
+
+ // bean.set...(object)
+ mv.visitVarInsn(ALOAD, 3);
+ mv.visitVarInsn(ALOAD, 2);
+
+ Class> parameterType = wither.getParameterTypes()[0];
+ mv.visitTypeInsn(CHECKCAST, Type.getInternalName(autoboxType(parameterType)));
+ autoboxIfNeeded(autoboxType(parameterType), parameterType, mv);
+
+ int invokeOpCode = INVOKEVIRTUAL;
+ Class> declaringClass = wither.getDeclaringClass();
+ boolean interfaceDefinition = declaringClass.isInterface();
+
+ if (interfaceDefinition) {
+ invokeOpCode = INVOKEINTERFACE;
+ }
+
+ mv.visitMethodInsn(invokeOpCode, Type.getInternalName(wither.getDeclaringClass()), wither.getName(),
+ String.format("(%s)%s", signatureTypeName(parameterType), referenceName(wither.getReturnType())),
+ interfaceDefinition);
+ }
+
+ mv.visitFieldInsn(PUTFIELD, internalClassName, BEAN_FIELD, getAccessibleTypeReferenceName(entity));
+ } else if (property.usePropertyAccess() && setter != null) {
if (generateMethodHandle(entity, setter)) {
+
// $setter.invoke(bean)
mv.visitFieldInsn(GETSTATIC, internalClassName, setterName(property),
referenceName(JAVA_LANG_INVOKE_METHOD_HANDLE));
@@ -1012,6 +1056,7 @@ public class ClassGeneratingPropertyAccessorFactory implements PersistentPropert
mv.visitMethodInsn(INVOKEVIRTUAL, JAVA_LANG_INVOKE_METHOD_HANDLE, "invoke",
String.format("(%s%s)V", referenceName(JAVA_LANG_OBJECT), referenceName(JAVA_LANG_OBJECT)), false);
} else {
+
// bean.set...(object)
mv.visitVarInsn(ALOAD, 3);
mv.visitVarInsn(ALOAD, 2);
@@ -1101,6 +1146,10 @@ public class ClassGeneratingPropertyAccessorFactory implements PersistentPropert
return String.format("$%s_setter", property.getName());
}
+ private static String witherName(PersistentProperty> property) {
+ return String.format("$%s_wither", property.getName());
+ }
+
private static String getterName(PersistentProperty> property) {
return String.format("$%s_getter", property.getName());
}
@@ -1122,13 +1171,21 @@ public class ClassGeneratingPropertyAccessorFactory implements PersistentPropert
return !(Modifier.isPrivate(modifiers) || Modifier.isProtected(modifiers) || Modifier.isPublic(modifiers));
}
+ private static String getAccessibleTypeReferenceName(PersistentEntity, ?> entity) {
+ if (isAccessible(entity)) {
+ return referenceName(entity.getType());
+ }
+
+ return referenceName(JAVA_LANG_OBJECT);
+ }
+
private static boolean generateSetterMethodHandle(PersistentEntity, ?> entity, @Nullable Field field) {
if (field == null) {
return false;
}
- return generateMethodHandle(entity, field) || Modifier.isFinal(field.getModifiers());
+ return generateMethodHandle(entity, field);
}
/**
@@ -1161,8 +1218,8 @@ public class ClassGeneratingPropertyAccessorFactory implements PersistentPropert
/**
* Retrieves the class variable index with an offset of {@code 4}.
*/
- private static int classVariableIndex4(List> list, Class> item) {
- return 4 + list.indexOf(item);
+ private static int classVariableIndex5(List> list, Class> item) {
+ return 5 + list.indexOf(item);
}
private static String generateClassName(PersistentEntity, ?> entity) {
diff --git a/src/main/java/org/springframework/data/mapping/model/Property.java b/src/main/java/org/springframework/data/mapping/model/Property.java
index ee8adc25c..926f429f4 100644
--- a/src/main/java/org/springframework/data/mapping/model/Property.java
+++ b/src/main/java/org/springframework/data/mapping/model/Property.java
@@ -29,6 +29,8 @@ import org.springframework.data.util.Optionals;
import org.springframework.data.util.TypeInformation;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
+import org.springframework.util.ReflectionUtils;
+import org.springframework.util.StringUtils;
/**
* Value object to abstract the concept of a property backed by a {@link Field} and / or a {@link PropertyDescriptor}.
@@ -49,6 +51,7 @@ public class Property {
private final Lazy name;
private final Lazy toString;
+ private final Lazy> wither;
private Property(TypeInformation> type, Optional field, Optional descriptor) {
@@ -73,6 +76,20 @@ public class Property {
this.setter = descriptor.map(PropertyDescriptor::getWriteMethod)//
.filter(it -> getType() != null)//
.filter(it -> type.getParameterTypes(it).get(0).getType().isAssignableFrom(getType()));
+
+ this.wither = Lazy.of(() -> findWither(type, getName(), getType()));
+
+ }
+
+ private static Optional findWither(TypeInformation> owner, String propertyName, Class> rawType) {
+
+ Method method = ReflectionUtils.findMethod(owner.getType(), "with" + StringUtils.capitalize(propertyName), rawType);
+
+ if (method != null && owner.isAssignableFrom(owner.getReturnType(method))) {
+ return Optional.of(method);
+ }
+
+ return Optional.empty();
}
/**
@@ -162,6 +179,15 @@ public class Property {
return setter;
}
+ /**
+ * Returns the wither of the property if available and if its first (only) parameter matches the type of the property.
+ *
+ * @return will never be {@literal null}.
+ */
+ public Optional getWither() {
+ return wither.get();
+ }
+
/**
* Returns whether the property exposes a getter or a setter.
*
diff --git a/src/test/java/org/springframework/data/mapping/PersistentPropertyAccessorUnitTests.java b/src/test/java/org/springframework/data/mapping/PersistentPropertyAccessorUnitTests.java
index 271f47cd1..18a2aff49 100644
--- a/src/test/java/org/springframework/data/mapping/PersistentPropertyAccessorUnitTests.java
+++ b/src/test/java/org/springframework/data/mapping/PersistentPropertyAccessorUnitTests.java
@@ -17,6 +17,8 @@ package org.springframework.data.mapping;
import static org.assertj.core.api.Assertions.*;
+import lombok.AllArgsConstructor;
+import lombok.Data;
import lombok.Value;
import org.junit.Test;
@@ -93,7 +95,8 @@ public class PersistentPropertyAccessorUnitTests {
Customer customer;
}
- @Value
+ @Data
+ @AllArgsConstructor
static class Customer {
String firstname;
}
diff --git a/src/test/java/org/springframework/data/mapping/model/ClassGeneratingPropertyAccessorFactoryTests.java b/src/test/java/org/springframework/data/mapping/model/ClassGeneratingPropertyAccessorFactoryTests.java
index bf745aa69..220928ff4 100755
--- a/src/test/java/org/springframework/data/mapping/model/ClassGeneratingPropertyAccessorFactoryTests.java
+++ b/src/test/java/org/springframework/data/mapping/model/ClassGeneratingPropertyAccessorFactoryTests.java
@@ -46,8 +46,8 @@ import org.springframework.util.StringUtils;
@RunWith(Parameterized.class)
public class ClassGeneratingPropertyAccessorFactoryTests {
- private final ClassGeneratingPropertyAccessorFactory factory = new ClassGeneratingPropertyAccessorFactory();
- private final SampleMappingContext mappingContext = new SampleMappingContext();
+ private final static ClassGeneratingPropertyAccessorFactory factory = new ClassGeneratingPropertyAccessorFactory();
+ private final static SampleMappingContext mappingContext = new SampleMappingContext();
private final Object bean;
private final String propertyName;
@@ -67,7 +67,8 @@ public class ClassGeneratingPropertyAccessorFactoryTests {
List parameters = new ArrayList<>();
List propertyNames = Arrays.asList("privateField", "packageDefaultField", "protectedField", "publicField",
- "privateProperty", "packageDefaultProperty", "protectedProperty", "publicProperty", "syntheticProperty");
+ "privateProperty", "packageDefaultProperty", "protectedProperty", "publicProperty", "syntheticProperty",
+ "immutable", "wither");
parameters.addAll(parameters(new InnerPrivateType(), propertyNames, Object.class));
parameters
@@ -106,7 +107,7 @@ public class ClassGeneratingPropertyAccessorFactoryTests {
assertThat(factory.isSupported(mappingContext.getRequiredPersistentEntity(bean.getClass()))).isTrue();
}
- @Test // DATACMNS-809
+ @Test // DATACMNS-809, // DATACMNS-1322
public void shouldSetAndGetProperty() throws Exception {
assumeTrue(StringUtils.hasText(propertyName));
@@ -114,9 +115,15 @@ public class ClassGeneratingPropertyAccessorFactoryTests {
assertThat(getProperty(bean, propertyName)).satisfies(property -> {
PersistentPropertyAccessor persistentPropertyAccessor = getPersistentPropertyAccessor(bean);
+ if (property.isImmutable() && property.getWither() == null) {
- persistentPropertyAccessor.setProperty(property, "value");
- assertThat(persistentPropertyAccessor.getProperty(property)).isEqualTo("value");
+ assertThatThrownBy(() -> persistentPropertyAccessor.setProperty(property, "value"))
+ .isInstanceOf(UnsupportedOperationException.class);
+ } else {
+
+ persistentPropertyAccessor.setProperty(property, "value");
+ assertThat(persistentPropertyAccessor.getProperty(property)).isEqualTo("value");
+ }
});
}
@@ -183,6 +190,8 @@ public class ClassGeneratingPropertyAccessorFactoryTests {
protected String protectedField;
public String publicField;
private String backing;
+ private final String immutable = "";
+ private final String wither;
@AccessType(Type.PROPERTY) private String privateProperty;
@@ -192,6 +201,14 @@ public class ClassGeneratingPropertyAccessorFactoryTests {
@AccessType(Type.PROPERTY) private String publicProperty;
+ private InnerPrivateType() {
+ this.wither = "";
+ }
+
+ private InnerPrivateType(String wither) {
+ this.wither = wither;
+ }
+
private String getPrivateProperty() {
return privateProperty;
}
@@ -232,6 +249,14 @@ public class ClassGeneratingPropertyAccessorFactoryTests {
public void setSyntheticProperty(String syntheticProperty) {
backing = syntheticProperty;
}
+
+ public String getWither() {
+ return wither;
+ }
+
+ public InnerPrivateType withWither(String wither) {
+ return new InnerPrivateType(wither);
+ }
}
// DATACMNS-809
@@ -248,6 +273,8 @@ public class ClassGeneratingPropertyAccessorFactoryTests {
protected String protectedField;
public String publicField;
private String backing;
+ private final String immutable = "";
+ private final String wither;
@AccessType(Type.PROPERTY) private String privateProperty;
@@ -257,6 +284,14 @@ public class ClassGeneratingPropertyAccessorFactoryTests {
@AccessType(Type.PROPERTY) private String publicProperty;
+ InnerPackageDefaultType() {
+ this.wither = "";
+ }
+
+ private InnerPackageDefaultType(String wither) {
+ this.wither = wither;
+ }
+
private String getPrivateProperty() {
return privateProperty;
}
@@ -297,6 +332,14 @@ public class ClassGeneratingPropertyAccessorFactoryTests {
public void setSyntheticProperty(String syntheticProperty) {
backing = syntheticProperty;
}
+
+ public String getWither() {
+ return wither;
+ }
+
+ public InnerPrivateType withWither(String wither) {
+ return new InnerPrivateType(wither);
+ }
}
// DATACMNS-809
@@ -308,6 +351,8 @@ public class ClassGeneratingPropertyAccessorFactoryTests {
protected String protectedField;
public String publicField;
private String backing;
+ private final String immutable = "";
+ private final String wither;
@AccessType(Type.PROPERTY) private String privateProperty;
@@ -317,6 +362,14 @@ public class ClassGeneratingPropertyAccessorFactoryTests {
@AccessType(Type.PROPERTY) private String publicProperty;
+ InnerProtectedType() {
+ this.wither = "";
+ }
+
+ private InnerProtectedType(String wither) {
+ this.wither = wither;
+ }
+
private String getPrivateProperty() {
return privateProperty;
}
@@ -357,6 +410,14 @@ public class ClassGeneratingPropertyAccessorFactoryTests {
public void setSyntheticProperty(String syntheticProperty) {
backing = syntheticProperty;
}
+
+ public String getWither() {
+ return wither;
+ }
+
+ public InnerPrivateType withWither(String wither) {
+ return new InnerPrivateType(wither);
+ }
}
// DATACMNS-809
@@ -368,6 +429,8 @@ public class ClassGeneratingPropertyAccessorFactoryTests {
protected String protectedField;
public String publicField;
private String backing;
+ private final String immutable = "";
+ private final String wither;
@AccessType(Type.PROPERTY) private String privateProperty;
@@ -377,6 +440,14 @@ public class ClassGeneratingPropertyAccessorFactoryTests {
@AccessType(Type.PROPERTY) private String publicProperty;
+ InnerPublicType() {
+ this.wither = "";
+ }
+
+ private InnerPublicType(String wither) {
+ this.wither = wither;
+ }
+
private String getPrivateProperty() {
return privateProperty;
}
@@ -417,6 +488,14 @@ public class ClassGeneratingPropertyAccessorFactoryTests {
public void setSyntheticProperty(String syntheticProperty) {
backing = syntheticProperty;
}
+
+ public String getWither() {
+ return wither;
+ }
+
+ public InnerPrivateType withWither(String wither) {
+ return new InnerPrivateType(wither);
+ }
}
public static class SubtypeOfTypeInOtherPackage extends TypeInOtherPackage {}
diff --git a/src/test/java/org/springframework/data/mapping/model/ClassGeneratingPropertyAccessorPackageDefaultType.java b/src/test/java/org/springframework/data/mapping/model/ClassGeneratingPropertyAccessorPackageDefaultType.java
index 349b9bacd..d4578d623 100644
--- a/src/test/java/org/springframework/data/mapping/model/ClassGeneratingPropertyAccessorPackageDefaultType.java
+++ b/src/test/java/org/springframework/data/mapping/model/ClassGeneratingPropertyAccessorPackageDefaultType.java
@@ -30,6 +30,8 @@ class ClassGeneratingPropertyAccessorPackageDefaultType {
protected String protectedField;
public String publicField;
private String backing;
+ private final String immutable = "";
+ private final String wither;
@AccessType(Type.PROPERTY) private String privateProperty;
@@ -39,6 +41,14 @@ class ClassGeneratingPropertyAccessorPackageDefaultType {
@AccessType(Type.PROPERTY) private String publicProperty;
+ public ClassGeneratingPropertyAccessorPackageDefaultType() {
+ this.wither = "";
+ }
+
+ private ClassGeneratingPropertyAccessorPackageDefaultType(String wither) {
+ this.wither = wither;
+ }
+
private String getPrivateProperty() {
return privateProperty;
}
@@ -79,4 +89,12 @@ class ClassGeneratingPropertyAccessorPackageDefaultType {
public void setSyntheticProperty(String syntheticProperty) {
backing = syntheticProperty;
}
+
+ public String getWither() {
+ return wither;
+ }
+
+ public ClassGeneratingPropertyAccessorPackageDefaultType withWither(String wither) {
+ return new ClassGeneratingPropertyAccessorPackageDefaultType(wither);
+ }
}
diff --git a/src/test/java/org/springframework/data/mapping/model/ClassGeneratingPropertyAccessorPublicType.java b/src/test/java/org/springframework/data/mapping/model/ClassGeneratingPropertyAccessorPublicType.java
index 5dd739d49..3a7ecb7ca 100644
--- a/src/test/java/org/springframework/data/mapping/model/ClassGeneratingPropertyAccessorPublicType.java
+++ b/src/test/java/org/springframework/data/mapping/model/ClassGeneratingPropertyAccessorPublicType.java
@@ -30,6 +30,8 @@ public class ClassGeneratingPropertyAccessorPublicType {
protected String protectedField;
public String publicField;
private String backing;
+ private final String immutable = "";
+ private final String wither;
private Integer aa;
private int bb;
@@ -41,6 +43,14 @@ public class ClassGeneratingPropertyAccessorPublicType {
@AccessType(Type.PROPERTY) private String publicProperty;
+ public ClassGeneratingPropertyAccessorPublicType() {
+ this.wither = "";
+ }
+
+ private ClassGeneratingPropertyAccessorPublicType(String wither) {
+ this.wither = wither;
+ }
+
private String getPrivateProperty() {
return privateProperty;
}
@@ -82,6 +92,14 @@ public class ClassGeneratingPropertyAccessorPublicType {
backing = syntheticProperty;
}
+ public String getWither() {
+ return wither;
+ }
+
+ public ClassGeneratingPropertyAccessorPublicType withWither(String wither) {
+ return new ClassGeneratingPropertyAccessorPublicType(wither);
+ }
+
public Object set(Object e) {
aa = (Integer) e;
diff --git a/src/test/java/org/springframework/data/mapping/model/PersistentPropertyAccessorTests.java b/src/test/java/org/springframework/data/mapping/model/PersistentPropertyAccessorTests.java
new file mode 100644
index 000000000..3dd5c4591
--- /dev/null
+++ b/src/test/java/org/springframework/data/mapping/model/PersistentPropertyAccessorTests.java
@@ -0,0 +1,128 @@
+/*
+ * 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 static org.assertj.core.api.Assertions.*;
+
+import lombok.Data;
+import lombok.Value;
+import lombok.experimental.Wither;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Function;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+import org.springframework.data.mapping.PersistentPropertyAccessor;
+import org.springframework.data.mapping.context.SampleMappingContext;
+import org.springframework.data.mapping.context.SamplePersistentProperty;
+
+/**
+ * Unit tests for {@link PersistentPropertyAccessor} through {@link BeanWrapper} and
+ * {@link ClassGeneratingPropertyAccessorFactory}.
+ *
+ * @author Mark Paluch
+ */
+@RunWith(Parameterized.class)
+public class PersistentPropertyAccessorTests {
+
+ private final static SampleMappingContext mappingContext = new SampleMappingContext();
+
+ private final Function propertyAccessorFunction;
+
+ public PersistentPropertyAccessorTests(Function propertyAccessor,
+ String displayName) {
+
+ this.propertyAccessorFunction = propertyAccessor;
+ }
+
+ @Parameters(name = "{1}")
+ @SuppressWarnings("unchecked")
+ public static List parameters() {
+
+ List parameters = new ArrayList<>();
+
+ ClassGeneratingPropertyAccessorFactory factory = new ClassGeneratingPropertyAccessorFactory();
+
+ Function beanWrapper = BeanWrapper::new;
+ Function classGenerating = it -> factory
+ .getPropertyAccessor(mappingContext.getRequiredPersistentEntity(it.getClass()), it);
+
+ parameters.add(new Object[] { beanWrapper, "BeanWrapper" });
+ parameters.add(new Object[] { classGenerating, "ClassGeneratingPropertyAccessorFactory" });
+
+ return parameters;
+ }
+
+ @Test // DATACMNS-1322
+ public void shouldSetProperty() {
+
+ DataClass bean = new DataClass();
+ PersistentPropertyAccessor accessor = propertyAccessorFunction.apply(bean);
+ SamplePersistentProperty property = getProperty(bean, "id");
+
+ accessor.setProperty(property, "value");
+
+ assertThat(accessor.getBean()).hasFieldOrPropertyWithValue("id", "value");
+ assertThat(accessor.getBean()).isSameAs(bean);
+ assertThat(accessor.getProperty(property)).isEqualTo("value");
+ }
+
+ @Test // DATACMNS-1322
+ public void shouldWitherProperty() {
+
+ ValueClass bean = new ValueClass("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 shouldRejectImmutablePropertyUpdate() {
+
+ ValueClass bean = new ValueClass("foo", "bar");
+ 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);
+ }
+
+ @Data
+ static class DataClass {
+ String id;
+ }
+
+ @Value
+
+ static class ValueClass {
+ @Wither String id;
+ String immutable;
+ }
+
+}
diff --git a/src/test/java/org/springframework/data/mapping/model/PropertyUnitTests.java b/src/test/java/org/springframework/data/mapping/model/PropertyUnitTests.java
new file mode 100644
index 000000000..00c3723cb
--- /dev/null
+++ b/src/test/java/org/springframework/data/mapping/model/PropertyUnitTests.java
@@ -0,0 +1,81 @@
+/*
+ * 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 static org.assertj.core.api.Assertions.*;
+
+import lombok.Value;
+import lombok.experimental.Wither;
+
+import org.junit.Test;
+import org.springframework.data.util.ClassTypeInformation;
+import org.springframework.util.ReflectionUtils;
+
+/**
+ * Unit tests for {@link Property}.
+ *
+ * @author Mark Paluch
+ */
+public class PropertyUnitTests {
+
+ @Test // DATACMNS-1322
+ public void shouldNotFindWitherMethod() {
+
+ assertThat(Property
+ .of(ClassTypeInformation.from(ImmutableType.class), ReflectionUtils.findField(ImmutableType.class, "id"))
+ .getWither()).isEmpty();
+ assertThat(Property
+ .of(ClassTypeInformation.from(ImmutableType.class), ReflectionUtils.findField(ImmutableType.class, "name"))
+ .getWither()).isEmpty();
+ }
+
+ @Test // DATACMNS-1322
+ public void shouldDiscoverWitherMethod() {
+
+ Property property = Property.of(ClassTypeInformation.from(WitherType.class),
+ ReflectionUtils.findField(WitherType.class, "id"));
+
+ assertThat(property.getWither()).isPresent().hasValueSatisfying(actual -> {
+ assertThat(actual.getName()).isEqualTo("withId");
+ assertThat(actual.getReturnType()).isEqualTo(WitherType.class);
+ });
+ }
+
+ @Value
+ static class ImmutableType {
+ String id;
+ String name;
+
+ ImmutableType withId(Long id) {
+ return null;
+ }
+
+ ImmutableType withName(Object id) {
+ return null;
+ }
+
+ Object withName(String id) {
+ return null;
+ }
+ }
+
+ @Value
+ @Wither
+ static class WitherType {
+ String id;
+ String name;
+ }
+}
diff --git a/src/test/java/org/springframework/data/mapping/model/subpackage/TypeInOtherPackage.java b/src/test/java/org/springframework/data/mapping/model/subpackage/TypeInOtherPackage.java
index 29480c08a..57ae50430 100644
--- a/src/test/java/org/springframework/data/mapping/model/subpackage/TypeInOtherPackage.java
+++ b/src/test/java/org/springframework/data/mapping/model/subpackage/TypeInOtherPackage.java
@@ -29,6 +29,8 @@ public class TypeInOtherPackage {
protected String protectedField;
public String publicField;
private String backing;
+ private final String immutable = "";
+ private final String wither;
@AccessType(Type.PROPERTY) private String privateProperty;
@@ -38,6 +40,14 @@ public class TypeInOtherPackage {
@AccessType(Type.PROPERTY) private String publicProperty;
+ public TypeInOtherPackage() {
+ this.wither = "";
+ }
+
+ private TypeInOtherPackage(String wither) {
+ this.wither = wither;
+ }
+
@SuppressWarnings("unused")
private String getPrivateProperty() {
return privateProperty;
@@ -80,4 +90,12 @@ public class TypeInOtherPackage {
public void setSyntheticProperty(String syntheticProperty) {
backing = syntheticProperty;
}
+
+ public String getWither() {
+ return wither;
+ }
+
+ public TypeInOtherPackage withWither(String wither) {
+ return new TypeInOtherPackage(wither);
+ }
}