Skip to content

Commit 23ebf31

Browse files
committed
Support for association target type detection for jMolecules.
We now detect the component type of jMolecules' Association as association target type in AbstractPersistentProperty. AnnotationBasedPersistentProperty keeps the lookup of the explicit @reference around but falls back to the Association analysis for that. @reference is now only considered by AnnotationBasedPersistentProperty. It was previously also (erroneously) considered by AbstractPersistentProperty. It was also changed to return `null` in case no association target type could be looked up instead of returning the sole property's type in case that is an entity. This was done to satisfy the behavior documented on the interface but also to avoid the call to isEntity() during the calculation which might use association information in turn and thus lead to a stack overflow. Reworded the contract of PersistentProperty.getAssociationTargetType() to emphasize the symmetry with ….isAssociation() in implementation classes. Closes: #2344.
1 parent 85c0ce1 commit 23ebf31

File tree

5 files changed

+82
-33
lines changed

5 files changed

+82
-33
lines changed

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

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -383,11 +383,13 @@ default boolean hasActualTypeAnnotation(Class<? extends Annotation> annotationTy
383383
}
384384

385385
/**
386-
* Return the type the property refers to in case it's an association.
386+
* Return the type the property refers to in case it's an association, i.e. {@link #isAssociation()} returns
387+
* {@literal true}. That means, that implementations <em>must</em> return a non-{@literal null} value from this method
388+
* in that case. We also recommend to return {@literal null} for non-associations right away to establish symmetry
389+
* between this method and {@link #isAssociation()}.
387390
*
388-
* @return the type the property refers to in case it's an association or {@literal null} in case it's not an
389-
* association, the target entity type is not explicitly defined (either explicitly or through the property
390-
* type itself).
391+
* @return the type the property refers to in case it's an association, i.e. {@link #isAssociation()} returns
392+
* {@literal true}.
391393
* @since 2.1
392394
*/
393395
@Nullable

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

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
import java.util.Map;
2323
import java.util.Optional;
2424

25-
import org.springframework.data.annotation.Reference;
2625
import org.springframework.data.mapping.Association;
2726
import org.springframework.data.mapping.PersistentEntity;
2827
import org.springframework.data.mapping.PersistentProperty;
@@ -64,6 +63,9 @@ public abstract class AbstractPersistentProperty<P extends PersistentProperty<P>
6463
private final Lazy<Boolean> usePropertyAccess;
6564
private final Lazy<Optional<? extends TypeInformation<?>>> entityTypeInformation;
6665

66+
private final Lazy<Boolean> isAssociation;
67+
private final Lazy<Class<?>> associationTargetType;
68+
6769
private final Method getter;
6870
private final Method setter;
6971
private final Field field;
@@ -86,6 +88,15 @@ public AbstractPersistentProperty(Property property, PersistentEntity<?, P> owne
8688
this.hashCode = Lazy.of(property::hashCode);
8789
this.usePropertyAccess = Lazy.of(() -> owner.getType().isInterface() || CAUSE_FIELD.equals(getField()));
8890

91+
this.isAssociation = Lazy.of(() -> ASSOCIATION_TYPE != null && ASSOCIATION_TYPE.isAssignableFrom(rawType));
92+
this.associationTargetType = ASSOCIATION_TYPE == null
93+
? Lazy.empty()
94+
: Lazy.of(() -> Optional.of(getTypeInformation())
95+
.map(it -> it.getSuperTypeInformation(ASSOCIATION_TYPE))
96+
.map(TypeInformation::getComponentType)
97+
.map(TypeInformation::getType)
98+
.orElse(null));
99+
89100
this.entityTypeInformation = Lazy.of(() -> Optional.ofNullable(information.getActualType())//
90101
.filter(it -> !simpleTypeHolder.isSimpleType(it.getType()))//
91102
.filter(it -> !it.isCollectionLike())//
@@ -170,6 +181,7 @@ public Iterable<? extends TypeInformation<?>> getPersistentEntityTypes() {
170181
* (non-Javadoc)
171182
* @see org.springframework.data.mapping.PersistentProperty#getGetter()
172183
*/
184+
@Nullable
173185
@Override
174186
public Method getGetter() {
175187
return this.getter;
@@ -179,6 +191,7 @@ public Method getGetter() {
179191
* (non-Javadoc)
180192
* @see org.springframework.data.mapping.PersistentProperty#getSetter()
181193
*/
194+
@Nullable
182195
@Override
183196
public Method getSetter() {
184197
return this.setter;
@@ -188,6 +201,7 @@ public Method getSetter() {
188201
* (non-Javadoc)
189202
* @see org.springframework.data.mapping.PersistentProperty#getWither()
190203
*/
204+
@Nullable
191205
@Override
192206
public Method getWither() {
193207
return this.wither;
@@ -245,8 +259,7 @@ public boolean isImmutable() {
245259
*/
246260
@Override
247261
public boolean isAssociation() {
248-
return isAnnotationPresent(Reference.class) //
249-
|| ASSOCIATION_TYPE != null && ASSOCIATION_TYPE.isAssignableFrom(rawType);
262+
return isAssociation.get();
250263
}
251264

252265
/*
@@ -259,6 +272,16 @@ public Association<P> getAssociation() {
259272
return association.orElse(null);
260273
}
261274

275+
/*
276+
* (non-Javadoc)
277+
* @see org.springframework.data.mapping.PersistentProperty#getAssociationTargetType()
278+
*/
279+
@Nullable
280+
@Override
281+
public Class<?> getAssociationTargetType() {
282+
return associationTargetType.getNullable();
283+
}
284+
262285
/*
263286
* (non-Javadoc)
264287
* @see org.springframework.data.mapping.PersistentProperty#isCollectionLike()

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

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,18 @@ public abstract class AnnotationBasedPersistentProperty<P extends PersistentProp
7474
&& (isAnnotationPresent(Reference.class) || super.isAssociation()));
7575
private final Lazy<Boolean> isId = Lazy.of(() -> isAnnotationPresent(Id.class));
7676
private final Lazy<Boolean> isVersion = Lazy.of(() -> isAnnotationPresent(Version.class));
77+
private final Lazy<Class<?>> associationTargetType = Lazy.of(() -> {
78+
79+
if (!isAssociation()) {
80+
return null;
81+
}
82+
83+
return Optional.of(Reference.class) //
84+
.map(this::findAnnotation) //
85+
.map(Reference::to) //
86+
.map(it -> !Class.class.equals(it) ? it : getActualType()) //
87+
.orElseGet(() -> super.getAssociationTargetType());
88+
});
7789

7890
/**
7991
* Creates a new {@link AnnotationBasedPersistentProperty}.
@@ -285,18 +297,7 @@ public boolean usePropertyAccess() {
285297
@Nullable
286298
@Override
287299
public Class<?> getAssociationTargetType() {
288-
289-
Reference reference = findAnnotation(Reference.class);
290-
291-
if (reference == null) {
292-
return isEntity() ? getActualType() : null;
293-
}
294-
295-
Class<?> targetType = reference.to();
296-
297-
return Class.class.equals(targetType) //
298-
? isEntity() ? getActualType() : null //
299-
: targetType;
300+
return associationTargetType.getNullable();
300301
}
301302

302303
/*

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

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232
import java.util.TreeMap;
3333
import java.util.TreeSet;
3434

35+
import org.jmolecules.ddd.types.AggregateRoot;
36+
import org.jmolecules.ddd.types.Identifier;
3537
import org.junit.jupiter.api.BeforeEach;
3638
import org.junit.jupiter.api.Test;
3739
import org.springframework.data.mapping.Association;
@@ -230,6 +232,7 @@ void detectsJMoleculesAssociation() {
230232
SamplePersistentProperty property = getProperty(JMolecules.class, "association");
231233

232234
assertThat(property.isAssociation()).isTrue();
235+
assertThat(property.getAssociationTargetType()).isEqualTo(JMoleculesAggregate.class);
233236
}
234237

235238
private <T> BasicPersistentEntity<T, SamplePersistentProperty> getEntity(Class<T> type) {
@@ -371,11 +374,6 @@ public boolean isAnnotationPresent(Class<? extends Annotation> annotationType) {
371374
public <A extends Annotation> A findPropertyOrOwnerAnnotation(Class<A> annotationType) {
372375
return null;
373376
}
374-
375-
@Override
376-
public Class<?> getAssociationTargetType() {
377-
return null;
378-
}
379377
}
380378

381379
static class Sample {
@@ -392,6 +390,10 @@ class TreeMapWrapper {
392390
}
393391

394392
class JMolecules {
395-
org.jmolecules.ddd.types.Association association;
393+
org.jmolecules.ddd.types.Association<JMoleculesAggregate, Identifier> association;
394+
}
395+
396+
interface JMoleculesAggregate extends AggregateRoot<JMoleculesAggregate, Identifier> {
397+
396398
}
397399
}

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

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,17 @@
2424
import java.lang.annotation.Target;
2525
import java.util.Map;
2626
import java.util.Optional;
27+
import java.util.function.Function;
2728
import java.util.stream.Stream;
2829

30+
import org.assertj.core.api.ClassAssert;
31+
import org.jmolecules.ddd.types.AggregateRoot;
2932
import org.jmolecules.ddd.types.Association;
33+
import org.jmolecules.ddd.types.Identifier;
3034
import org.junit.jupiter.api.BeforeEach;
35+
import org.junit.jupiter.api.DynamicTest;
3136
import org.junit.jupiter.api.Test;
37+
import org.junit.jupiter.api.TestFactory;
3238
import org.springframework.core.annotation.AliasFor;
3339
import org.springframework.core.annotation.AnnotationUtils;
3440
import org.springframework.data.annotation.AccessType;
@@ -242,12 +248,21 @@ public void getRequiredAnnotationThrowsException() {
242248
assertThatThrownBy(() -> property.getRequiredAnnotation(Transient.class)).isInstanceOf(IllegalStateException.class);
243249
}
244250

245-
@Test // DATACMNS-1318
246-
public void detectsUltimateAssociationTargetClass() {
251+
@TestFactory // DATACMNS-1318
252+
public Stream<DynamicTest> detectsUltimateAssociationTargetClass() {
247253

248-
Stream.of("toSample", "toSample2", "sample", "withoutAnnotation").forEach(it -> {
249-
assertThat(getProperty(WithReferences.class, it).getAssociationTargetType()).isEqualTo(Sample.class);
250-
});
254+
Function<String, ClassAssert> verifier = it -> assertThat(
255+
getProperty(WithReferences.class, it).getAssociationTargetType());
256+
257+
Stream<DynamicTest> positives = DynamicTest.stream(Stream.of("toSample", "toSample2", "sample"), //
258+
it -> String.format("Property %s resolves to %s.", it, Sample.class.getSimpleName()), //
259+
it -> verifier.apply(it).isEqualTo(Sample.class));
260+
261+
Stream<DynamicTest> negatives = DynamicTest.stream(Stream.of("withoutAnnotation"), //
262+
it -> String.format("Property %s resolves to null.", it), //
263+
it -> verifier.apply(it).isNull());
264+
265+
return Stream.concat(positives, negatives);
251266
}
252267

253268
@Test // DATACMNS-1359
@@ -295,8 +310,12 @@ public void missingRequiredFieldThrowsException() {
295310
}
296311

297312
@Test // GH-2315
298-
void detectesJMoleculesAssociation() {
299-
assertThat(getProperty(JMolecules.class, "association").isAssociation()).isTrue();
313+
void detectsJMoleculesAssociation() {
314+
315+
SamplePersistentProperty property = getProperty(JMolecules.class, "association");
316+
317+
assertThat(property.isAssociation()).isTrue();
318+
assertThat(property.getAssociationTargetType()).isEqualTo(JMoleculesAggregate.class);
300319
}
301320

302321
@SuppressWarnings("unchecked")
@@ -484,6 +503,8 @@ interface NoField {
484503
}
485504

486505
static class JMolecules {
487-
Association association;
506+
Association<JMoleculesAggregate, Identifier> association;
488507
}
508+
509+
interface JMoleculesAggregate extends AggregateRoot<JMoleculesAggregate, Identifier> {}
489510
}

0 commit comments

Comments
 (0)