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 Original pull request: #2916
This commit is contained in:
@@ -262,6 +262,16 @@ public interface PersistentProperty<P extends PersistentProperty<P>> {
|
||||
*/
|
||||
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}.
|
||||
|
||||
@@ -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<P extends PersistentProperty<P>
|
||||
private final Method setter;
|
||||
private final Field field;
|
||||
private final Method wither;
|
||||
private final Lazy<Boolean> readable;
|
||||
private final boolean immutable;
|
||||
|
||||
public AbstractPersistentProperty(Property property, PersistentEntity<?, P> owner,
|
||||
@@ -103,11 +105,27 @@ public abstract class AbstractPersistentProperty<P extends PersistentProperty<P>
|
||||
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<P> createAssociation();
|
||||
@@ -170,6 +188,7 @@ public abstract class AbstractPersistentProperty<P extends PersistentProperty<P>
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Field getField() {
|
||||
return this.field;
|
||||
}
|
||||
@@ -190,6 +209,11 @@ public abstract class AbstractPersistentProperty<P extends PersistentProperty<P>
|
||||
return !isTransient();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isReadable() {
|
||||
return !isTransient() && readable.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isImmutable() {
|
||||
return immutable;
|
||||
@@ -313,10 +337,8 @@ public abstract class AbstractPersistentProperty<P extends PersistentProperty<P>
|
||||
|
||||
Set<TypeInformation<?>> 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<TypeInformation<?>> detectEntityTypes(@Nullable TypeInformation<?> source) {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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<T> implements PersistentProperty
|
||||
PersistentEntity<?, ? extends PersistentProperty<?>> owner = property.getOwner();
|
||||
PersistentPropertyAccessor<T> 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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -178,6 +178,31 @@ public class AnnotationBasedPersistentPropertyUnitTests<P extends AnnotationBase
|
||||
.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 class AnnotationBasedPersistentPropertyUnitTests<P extends AnnotationBase
|
||||
@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 @@ public class AnnotationBasedPersistentPropertyUnitTests<P extends AnnotationBase
|
||||
@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;
|
||||
|
||||
Reference in New Issue
Block a user