From 8664a4cd5a069214ee3bca9ce0b524ed7655490f Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 28 Aug 2023 11:34:17 +0200 Subject: [PATCH 1/3] Prepare issue branch. --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 565738b5bb..25f6ef8b37 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-commons - 3.2.0-SNAPSHOT + 3.2.0-GH-2915-SNAPSHOT Spring Data Core Core Spring concepts underpinning every Spring Data module. From 2e4ad19748635ff1ec75bf5ca94ea46510450b98 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 28 Aug 2023 11:38:05 +0200 Subject: [PATCH 2/3] Introduce `PersistentProperty.isReadable`. isReadable reports whether a property can be read through PersistentPropertyAccessor, by either using property access through setters, a wither, Kotlin Copy method or by accessing the field directly. Closes #2915 --- .../data/mapping/PersistentProperty.java | 10 +++++ .../model/AbstractPersistentProperty.java | 40 +++++++++++++---- ...lassGeneratingPropertyAccessorFactory.java | 35 +-------------- .../InstantiationAwarePropertyAccessor.java | 3 +- .../data/mapping/model/KotlinCopyMethod.java | 14 ++++++ ...ationBasedPersistentPropertyUnitTests.java | 43 ++++++++++++++++++- 6 files changed, 100 insertions(+), 45 deletions(-) diff --git a/src/main/java/org/springframework/data/mapping/PersistentProperty.java b/src/main/java/org/springframework/data/mapping/PersistentProperty.java index 3c950db3a4..fc1b2d3704 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 @@ default Association

getRequiredAssociation() { */ 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 46290436ea..6cfd42db58 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 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 AbstractPersistentProperty(Property property, PersistentEntity owne 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 Method getWither() { } @Nullable + @Override public Field getField() { return this.field; } @@ -190,6 +209,11 @@ public boolean isWritable() { return !isTransient(); } + @Override + public boolean isReadable() { + return !isTransient() && readable.get(); + } + @Override public boolean isImmutable() { return immutable; @@ -313,10 +337,8 @@ private Set> detectEntityTypes(SimpleTypeHolder simpleTypes) 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 58b790a383..302a93f2d9 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 @@ private static void visitSetProperty0(PersistentEntity entity, PersistentP 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 int compareTo(PropertyStackAddress o) { * @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 dba291c608..07b35c2ad2 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 @@ 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 void setProperty(PersistentProperty property, @Nullable Object value) 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 404fc2cd5d..246548fa09 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 @@ public static Optional findCopyMethod(Class type) { }); } + /** + * 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 4f2ee2cef3..c465f59340 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 void considersPropertyWithReadOnlyMetaAnnotationReadOnly() { .satisfies(it -> 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 String getProperty() { @Retention(RetentionPolicy.RUNTIME) @Target(value = { FIELD, METHOD, ANNOTATION_TYPE }) @Id - public @interface MyId {} + public @interface MyId { + } static class FieldAccess { String name; @@ -493,6 +519,21 @@ static class ClassWithReadOnlyProperties { @CustomReadOnly String customReadOnlyProperty; } + static class ClassWithWither { + + private final String nonReadable = ""; + + private final String immutable; + + public ClassWithWither(String immutable) { + this.immutable = immutable; + } + + public ClassWithWither withImmutable(String v) { + return new ClassWithWither(v); + } + } + static class TypeWithCustomAnnotationsOnBothFieldAndAccessor { @Nullable String field; From 48f7843eb2076540294e78dea8fd550f03606a19 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 28 Aug 2023 11:38:48 +0200 Subject: [PATCH 3/3] Polishing. Fix Javadoc references. See #2915 --- .../org/springframework/data/mapping/model/BytecodeUtil.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/springframework/data/mapping/model/BytecodeUtil.java b/src/main/java/org/springframework/data/mapping/model/BytecodeUtil.java index a0575a2334..4da59c3ccd 100644 --- a/src/main/java/org/springframework/data/mapping/model/BytecodeUtil.java +++ b/src/main/java/org/springframework/data/mapping/model/BytecodeUtil.java @@ -165,7 +165,7 @@ static boolean isAccessible(Class type) { /** * Checks whether the class is accessible by inspecting modifiers (i.e. whether the class is {@code private}). * - * @param type must not be {@literal null}. + * @param modifiers modifiers to check. * @return {@literal true} if the {@code modifiers} do not indicate the private flag. * @see Modifier#isPrivate(int) */ @@ -177,7 +177,7 @@ static boolean isAccessible(int modifiers) { * Checks whether the modifiers express {@literal default} (not * {@literal private}/{@literal protected}/{@literal public}). * - * @param type must not be {@literal null}. + * @param modifiers modifiers to check. * @return {@literal true} if the {@code modifiers} indicate {@literal default}. * @see Modifier#isPrivate(int) */