Skip to content

Commit df543cd

Browse files
committed
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
1 parent 6f98114 commit df543cd

File tree

6 files changed

+100
-45
lines changed

6 files changed

+100
-45
lines changed

src/main/java/org/springframework/data/mapping/PersistentProperty.java

+10
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,16 @@ default Association<P> getRequiredAssociation() {
262262
*/
263263
boolean isWritable();
264264

265+
/**
266+
* Returns whether the current property is readable through {@link PersistentPropertyAccessor}, i.e. if it is not
267+
* {@link #isTransient()}, if the value can be set on the current instance or read to create a new instance as per
268+
* {@link #getWither()} or via Kotlin Copy methods.
269+
*
270+
* @return
271+
* @since 3.2
272+
*/
273+
boolean isReadable();
274+
265275
/**
266276
* Returns whether the current property is immutable, i.e. if there is no setter or the backing {@link Field} is
267277
* {@code final}.

src/main/java/org/springframework/data/mapping/model/AbstractPersistentProperty.java

+31-9
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import org.springframework.data.mapping.Association;
2828
import org.springframework.data.mapping.PersistentEntity;
2929
import org.springframework.data.mapping.PersistentProperty;
30+
import org.springframework.data.util.KotlinReflectionUtils;
3031
import org.springframework.data.util.Lazy;
3132
import org.springframework.data.util.ReflectionUtils;
3233
import org.springframework.data.util.TypeInformation;
@@ -70,6 +71,7 @@ public abstract class AbstractPersistentProperty<P extends PersistentProperty<P>
7071
private final Method setter;
7172
private final Field field;
7273
private final Method wither;
74+
private final Lazy<Boolean> readable;
7375
private final boolean immutable;
7476

7577
public AbstractPersistentProperty(Property property, PersistentEntity<?, P> owner,
@@ -103,11 +105,27 @@ public AbstractPersistentProperty(Property property, PersistentEntity<?, P> owne
103105
this.field = property.getField().orElse(null);
104106
this.wither = property.getWither().orElse(null);
105107

106-
if (setter == null && (field == null || Modifier.isFinal(field.getModifiers()))) {
107-
this.immutable = true;
108-
} else {
109-
this.immutable = false;
110-
}
108+
this.immutable = setter == null && (field == null || Modifier.isFinal(field.getModifiers()));
109+
this.readable = Lazy.of(() -> {
110+
111+
if (setter != null) {
112+
return true;
113+
}
114+
115+
if (wither != null) {
116+
return true;
117+
}
118+
119+
if (KotlinReflectionUtils.isDataClass(owner.getType()) && KotlinCopyMethod.hasKotlinCopyMethodFor(this)) {
120+
return true;
121+
}
122+
123+
if (field != null && !Modifier.isFinal(field.getModifiers())) {
124+
return true;
125+
}
126+
127+
return false;
128+
});
111129
}
112130

113131
protected abstract Association<P> createAssociation();
@@ -170,6 +188,7 @@ public Method getWither() {
170188
}
171189

172190
@Nullable
191+
@Override
173192
public Field getField() {
174193
return this.field;
175194
}
@@ -190,6 +209,11 @@ public boolean isWritable() {
190209
return !isTransient();
191210
}
192211

212+
@Override
213+
public boolean isReadable() {
214+
return !isTransient() && readable.get();
215+
}
216+
193217
@Override
194218
public boolean isImmutable() {
195219
return immutable;
@@ -313,10 +337,8 @@ private Set<TypeInformation<?>> detectEntityTypes(SimpleTypeHolder simpleTypes)
313337

314338
Set<TypeInformation<?>> result = detectEntityTypes(typeToStartWith);
315339

316-
return result.stream()
317-
.filter(it -> !simpleTypes.isSimpleType(it.getType()))
318-
.filter(it -> !it.getType().equals(ASSOCIATION_TYPE))
319-
.collect(Collectors.toSet());
340+
return result.stream().filter(it -> !simpleTypes.isSimpleType(it.getType()))
341+
.filter(it -> !it.getType().equals(ASSOCIATION_TYPE)).collect(Collectors.toSet());
320342
}
321343

322344
private Set<TypeInformation<?>> detectEntityTypes(@Nullable TypeInformation<?> source) {

src/main/java/org/springframework/data/mapping/model/ClassGeneratingPropertyAccessorFactory.java

+2-33
Original file line numberDiff line numberDiff line change
@@ -1029,7 +1029,7 @@ private static void visitSetProperty0(PersistentEntity<?, ?> entity, PersistentP
10291029
visitWithProperty(entity, property, mv, internalClassName, wither);
10301030
}
10311031

1032-
if (hasKotlinCopyMethod(property)) {
1032+
if (KotlinDetector.isKotlinType(entity.getType()) && KotlinCopyMethod.hasKotlinCopyMethodFor(property)) {
10331033
visitKotlinCopy(entity, property, mv, internalClassName);
10341034
}
10351035

@@ -1429,38 +1429,7 @@ public int compareTo(PropertyStackAddress o) {
14291429
* @return {@literal true} if object mutation is supported.
14301430
*/
14311431
static boolean supportsMutation(PersistentProperty<?> property) {
1432-
1433-
if (property.isImmutable()) {
1434-
1435-
if (property.getWither() != null) {
1436-
return true;
1437-
}
1438-
1439-
if (hasKotlinCopyMethod(property)) {
1440-
return true;
1441-
}
1442-
}
1443-
1444-
return (property.usePropertyAccess() && property.getSetter() != null)
1445-
|| (property.getField() != null && !Modifier.isFinal(property.getField().getModifiers()));
1446-
}
1447-
1448-
/**
1449-
* Check whether the owning type of {@link PersistentProperty} declares a {@literal copy} method or {@literal copy}
1450-
* method with parameter defaulting.
1451-
*
1452-
* @param property must not be {@literal null}.
1453-
* @return
1454-
*/
1455-
private static boolean hasKotlinCopyMethod(PersistentProperty<?> property) {
1456-
1457-
Class<?> type = property.getOwner().getType();
1458-
1459-
if (isAccessible(type) && KotlinDetector.isKotlinType(type)) {
1460-
return KotlinCopyMethod.findCopyMethod(type).filter(it -> it.supportsProperty(property)).isPresent();
1461-
}
1462-
1463-
return false;
1432+
return (property.usePropertyAccess() && property.getSetter() != null) || property.isReadable();
14641433
}
14651434

14661435
/**

src/main/java/org/springframework/data/mapping/model/InstantiationAwarePropertyAccessor.java

+1-2
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717

1818
import java.util.function.Function;
1919

20-
import org.springframework.core.KotlinDetector;
2120
import org.springframework.data.mapping.InstanceCreatorMetadata;
2221
import org.springframework.data.mapping.Parameter;
2322
import org.springframework.data.mapping.PersistentEntity;
@@ -76,7 +75,7 @@ public void setProperty(PersistentProperty<?> property, @Nullable Object value)
7675
PersistentEntity<?, ? extends PersistentProperty<?>> owner = property.getOwner();
7776
PersistentPropertyAccessor<T> delegate = delegateFunction.apply(this.bean);
7877

79-
if (!property.isImmutable() || property.getWither() != null || KotlinDetector.isKotlinType(owner.getType())) {
78+
if (property.isReadable()) {
8079

8180
delegate.setProperty(property, value);
8281
this.bean = delegate.getBean();

src/main/java/org/springframework/data/mapping/model/KotlinCopyMethod.java

+14
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,20 @@ public static Optional<KotlinCopyMethod> findCopyMethod(Class<?> type) {
9797
});
9898
}
9999

100+
/**
101+
* Check whether the owning type of {@link PersistentProperty} declares a {@literal copy} method or {@literal copy}
102+
* method with parameter defaulting.
103+
*
104+
* @param property must not be {@literal null}.
105+
* @return
106+
*/
107+
public static boolean hasKotlinCopyMethodFor(PersistentProperty<?> property) {
108+
109+
Class<?> type = property.getOwner().getType();
110+
111+
return KotlinCopyMethod.findCopyMethod(type).filter(it -> it.supportsProperty(property)).isPresent();
112+
}
113+
100114
public Method getPublicCopyMethod() {
101115
return this.publicCopyMethod;
102116
}

src/test/java/org/springframework/data/mapping/model/AnnotationBasedPersistentPropertyUnitTests.java

+42-1
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,31 @@ public void considersPropertyWithReadOnlyMetaAnnotationReadOnly() {
178178
.satisfies(it -> assertThat(it.isWritable()).isFalse());
179179
}
180180

181+
@Test // GH-2915
182+
public void treatsReadOnlyAsReadable() {
183+
184+
assertThat(getProperty(ClassWithReadOnlyProperties.class, "readOnlyProperty"))
185+
.satisfies(it -> assertThat(it.isReadable()).isTrue());
186+
}
187+
188+
@Test // GH-2915
189+
public void considersReadableForWither() {
190+
191+
assertThat(getProperty(ClassWithWither.class, "nonReadable"))
192+
.satisfies(it -> assertThat(it.isReadable()).isFalse());
193+
194+
assertThat(getProperty(ClassWithWither.class, "immutable")).satisfies(it -> assertThat(it.isReadable()).isTrue());
195+
}
196+
197+
@Test // GH-2915
198+
public void considersReadableForKotlinDataClass() {
199+
200+
assertThat(getProperty(SingleSettableProperty.class, "version"))
201+
.satisfies(it -> assertThat(it.isReadable()).isFalse());
202+
203+
assertThat(getProperty(SingleSettableProperty.class, "id")).satisfies(it -> assertThat(it.isReadable()).isTrue());
204+
}
205+
181206
@Test // DATACMNS-556
182207
public void doesNotRejectNonSpringDataAnnotationsUsedOnBothFieldAndAccessor() {
183208
getProperty(TypeWithCustomAnnotationsOnBothFieldAndAccessor.class, "field");
@@ -460,7 +485,8 @@ public String getProperty() {
460485
@Retention(RetentionPolicy.RUNTIME)
461486
@Target(value = { FIELD, METHOD, ANNOTATION_TYPE })
462487
@Id
463-
public @interface MyId {}
488+
public @interface MyId {
489+
}
464490

465491
static class FieldAccess {
466492
String name;
@@ -493,6 +519,21 @@ static class ClassWithReadOnlyProperties {
493519
@CustomReadOnly String customReadOnlyProperty;
494520
}
495521

522+
static class ClassWithWither {
523+
524+
private final String nonReadable = "";
525+
526+
private final String immutable;
527+
528+
public ClassWithWither(String immutable) {
529+
this.immutable = immutable;
530+
}
531+
532+
public ClassWithWither withImmutable(String v) {
533+
return new ClassWithWither(v);
534+
}
535+
}
536+
496537
static class TypeWithCustomAnnotationsOnBothFieldAndAccessor {
497538

498539
@Nullable String field;

0 commit comments

Comments
 (0)