diff --git a/src/main/java/org/springframework/data/mapping/PersistentProperty.java b/src/main/java/org/springframework/data/mapping/PersistentProperty.java index 3c950db3a..fc1b2d370 100644 --- a/src/main/java/org/springframework/data/mapping/PersistentProperty.java +++ b/src/main/java/org/springframework/data/mapping/PersistentProperty.java @@ -262,6 +262,16 @@ public interface PersistentProperty

> { */ boolean isWritable(); + /** + * Returns whether the current property is readable through {@link PersistentPropertyAccessor}, i.e. if it is not + * {@link #isTransient()}, if the value can be set on the current instance or read to create a new instance as per + * {@link #getWither()} or via Kotlin Copy methods. + * + * @return + * @since 3.2 + */ + boolean isReadable(); + /** * Returns whether the current property is immutable, i.e. if there is no setter or the backing {@link Field} is * {@code final}. 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 46290436e..6cfd42db5 100644 --- a/src/main/java/org/springframework/data/mapping/model/AbstractPersistentProperty.java +++ b/src/main/java/org/springframework/data/mapping/model/AbstractPersistentProperty.java @@ -27,6 +27,7 @@ import java.util.stream.Collectors; import org.springframework.data.mapping.Association; import org.springframework.data.mapping.PersistentEntity; import org.springframework.data.mapping.PersistentProperty; +import org.springframework.data.util.KotlinReflectionUtils; import org.springframework.data.util.Lazy; import org.springframework.data.util.ReflectionUtils; import org.springframework.data.util.TypeInformation; @@ -70,6 +71,7 @@ public abstract class AbstractPersistentProperty

private final Method setter; private final Field field; private final Method wither; + private final Lazy readable; private final boolean immutable; public AbstractPersistentProperty(Property property, PersistentEntity owner, @@ -103,11 +105,27 @@ public abstract class AbstractPersistentProperty

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; - } + this.immutable = setter == null && (field == null || Modifier.isFinal(field.getModifiers())); + this.readable = Lazy.of(() -> { + + if (setter != null) { + return true; + } + + if (wither != null) { + return true; + } + + if (KotlinReflectionUtils.isDataClass(owner.getType()) && KotlinCopyMethod.hasKotlinCopyMethodFor(this)) { + return true; + } + + if (field != null && !Modifier.isFinal(field.getModifiers())) { + return true; + } + + return false; + }); } protected abstract Association

createAssociation(); @@ -170,6 +188,7 @@ public abstract class AbstractPersistentProperty

} @Nullable + @Override public Field getField() { return this.field; } @@ -190,6 +209,11 @@ public abstract class AbstractPersistentProperty

return !isTransient(); } + @Override + public boolean isReadable() { + return !isTransient() && readable.get(); + } + @Override public boolean isImmutable() { return immutable; @@ -313,10 +337,8 @@ public abstract class AbstractPersistentProperty

Set> result = detectEntityTypes(typeToStartWith); - return result.stream() - .filter(it -> !simpleTypes.isSimpleType(it.getType())) - .filter(it -> !it.getType().equals(ASSOCIATION_TYPE)) - .collect(Collectors.toSet()); + return result.stream().filter(it -> !simpleTypes.isSimpleType(it.getType())) + .filter(it -> !it.getType().equals(ASSOCIATION_TYPE)).collect(Collectors.toSet()); } private Set> detectEntityTypes(@Nullable TypeInformation source) { 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 58b790a38..302a93f2d 100644 --- a/src/main/java/org/springframework/data/mapping/model/ClassGeneratingPropertyAccessorFactory.java +++ b/src/main/java/org/springframework/data/mapping/model/ClassGeneratingPropertyAccessorFactory.java @@ -1029,7 +1029,7 @@ public class ClassGeneratingPropertyAccessorFactory implements PersistentPropert visitWithProperty(entity, property, mv, internalClassName, wither); } - if (hasKotlinCopyMethod(property)) { + if (KotlinDetector.isKotlinType(entity.getType()) && KotlinCopyMethod.hasKotlinCopyMethodFor(property)) { visitKotlinCopy(entity, property, mv, internalClassName); } @@ -1429,38 +1429,7 @@ public class ClassGeneratingPropertyAccessorFactory implements PersistentPropert * @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)) { - return true; - } - } - - return (property.usePropertyAccess() && property.getSetter() != null) - || (property.getField() != null && !Modifier.isFinal(property.getField().getModifiers())); - } - - /** - * Check whether the owning type of {@link PersistentProperty} declares a {@literal copy} method or {@literal copy} - * method with parameter defaulting. - * - * @param property must not be {@literal null}. - * @return - */ - private static boolean hasKotlinCopyMethod(PersistentProperty property) { - - Class type = property.getOwner().getType(); - - if (isAccessible(type) && KotlinDetector.isKotlinType(type)) { - return KotlinCopyMethod.findCopyMethod(type).filter(it -> it.supportsProperty(property)).isPresent(); - } - - return false; + return (property.usePropertyAccess() && property.getSetter() != null) || property.isReadable(); } /** diff --git a/src/main/java/org/springframework/data/mapping/model/InstantiationAwarePropertyAccessor.java b/src/main/java/org/springframework/data/mapping/model/InstantiationAwarePropertyAccessor.java index dba291c60..07b35c2ad 100644 --- a/src/main/java/org/springframework/data/mapping/model/InstantiationAwarePropertyAccessor.java +++ b/src/main/java/org/springframework/data/mapping/model/InstantiationAwarePropertyAccessor.java @@ -17,7 +17,6 @@ package org.springframework.data.mapping.model; import java.util.function.Function; -import org.springframework.core.KotlinDetector; import org.springframework.data.mapping.InstanceCreatorMetadata; import org.springframework.data.mapping.Parameter; import org.springframework.data.mapping.PersistentEntity; @@ -76,7 +75,7 @@ public class InstantiationAwarePropertyAccessor implements PersistentProperty PersistentEntity> owner = property.getOwner(); PersistentPropertyAccessor delegate = delegateFunction.apply(this.bean); - if (!property.isImmutable() || property.getWither() != null || KotlinDetector.isKotlinType(owner.getType())) { + if (property.isReadable()) { delegate.setProperty(property, value); this.bean = delegate.getBean(); diff --git a/src/main/java/org/springframework/data/mapping/model/KotlinCopyMethod.java b/src/main/java/org/springframework/data/mapping/model/KotlinCopyMethod.java index 404fc2cd5..246548fa0 100644 --- a/src/main/java/org/springframework/data/mapping/model/KotlinCopyMethod.java +++ b/src/main/java/org/springframework/data/mapping/model/KotlinCopyMethod.java @@ -97,6 +97,20 @@ class KotlinCopyMethod { }); } + /** + * Check whether the owning type of {@link PersistentProperty} declares a {@literal copy} method or {@literal copy} + * method with parameter defaulting. + * + * @param property must not be {@literal null}. + * @return + */ + public static boolean hasKotlinCopyMethodFor(PersistentProperty property) { + + Class type = property.getOwner().getType(); + + return KotlinCopyMethod.findCopyMethod(type).filter(it -> it.supportsProperty(property)).isPresent(); + } + public Method getPublicCopyMethod() { return this.publicCopyMethod; } diff --git a/src/test/java/org/springframework/data/mapping/model/AnnotationBasedPersistentPropertyUnitTests.java b/src/test/java/org/springframework/data/mapping/model/AnnotationBasedPersistentPropertyUnitTests.java index 4f2ee2cef..c465f5934 100755 --- a/src/test/java/org/springframework/data/mapping/model/AnnotationBasedPersistentPropertyUnitTests.java +++ b/src/test/java/org/springframework/data/mapping/model/AnnotationBasedPersistentPropertyUnitTests.java @@ -178,6 +178,31 @@ public class AnnotationBasedPersistentPropertyUnitTests

assertThat(it.isWritable()).isFalse()); } + @Test // GH-2915 + public void treatsReadOnlyAsReadable() { + + assertThat(getProperty(ClassWithReadOnlyProperties.class, "readOnlyProperty")) + .satisfies(it -> assertThat(it.isReadable()).isTrue()); + } + + @Test // GH-2915 + public void considersReadableForWither() { + + assertThat(getProperty(ClassWithWither.class, "nonReadable")) + .satisfies(it -> assertThat(it.isReadable()).isFalse()); + + assertThat(getProperty(ClassWithWither.class, "immutable")).satisfies(it -> assertThat(it.isReadable()).isTrue()); + } + + @Test // GH-2915 + public void considersReadableForKotlinDataClass() { + + assertThat(getProperty(SingleSettableProperty.class, "version")) + .satisfies(it -> assertThat(it.isReadable()).isFalse()); + + assertThat(getProperty(SingleSettableProperty.class, "id")).satisfies(it -> assertThat(it.isReadable()).isTrue()); + } + @Test // DATACMNS-556 public void doesNotRejectNonSpringDataAnnotationsUsedOnBothFieldAndAccessor() { getProperty(TypeWithCustomAnnotationsOnBothFieldAndAccessor.class, "field"); @@ -460,7 +485,8 @@ public class AnnotationBasedPersistentPropertyUnitTests