Skip to content

Commit 61e489b

Browse files
committed
DATACMNS-1318 - Allow inspection of a reference's ultimate target entity.
We now expose what type or PersistentEntity an association points to by trying to match the association's type to identifier types to entities. In case multiple matches are found, we require the user to explicitly declare the target type via @reference. Introduced PersistentEntities.of(…) for convenience.
1 parent 7d8539d commit 61e489b

File tree

7 files changed

+260
-0
lines changed

7 files changed

+260
-0
lines changed

src/main/java/org/springframework/data/annotation/Reference.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
import java.lang.annotation.RetentionPolicy;
2222
import java.lang.annotation.Target;
2323

24+
import org.springframework.core.annotation.AliasFor;
25+
2426
/**
2527
* Meta-annotation to be used to annotate annotations that mark references to other objects.
2628
*
@@ -30,4 +32,22 @@
3032
@Retention(RetentionPolicy.RUNTIME)
3133
@Target(value = { FIELD, METHOD, ANNOTATION_TYPE })
3234
public @interface Reference {
35+
36+
/**
37+
* Explicitly define the target type of the reference. Used in case the annotated property is not the target type but
38+
* rather an identifier and/or if that identifier type is not uniquely identifying the target entity.
39+
*
40+
* @return
41+
*/
42+
@AliasFor(attribute = "to")
43+
Class<?> value() default Class.class;
44+
45+
/**
46+
* Explicitly define the target type of the reference. Used in case the annotated property is not the target type but
47+
* rather an identifier and/or if that identifier type is not uniquely identifying the target entity.
48+
*
49+
* @return
50+
*/
51+
@AliasFor(attribute = "value")
52+
Class<?> to() default Class.class;
3353
}

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,4 +333,15 @@ default boolean hasActualTypeAnnotation(Class<? extends Annotation> annotationTy
333333

334334
return AnnotatedElementUtils.hasAnnotation(getActualType(), annotationType);
335335
}
336+
337+
/**
338+
* Return the type the property refers to in case it's an association.
339+
*
340+
* @return the type the property refers to in case it's an association or {@literal null} in case it's not an
341+
* association, the target entity type is not explicitly defined (either explicitly or through the property
342+
* type itself).
343+
* @since 2.1
344+
*/
345+
@Nullable
346+
Class<?> getAssociationTargetType();
336347
}

src/main/java/org/springframework/data/mapping/context/PersistentEntities.java

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
*/
1616
package org.springframework.data.mapping.context;
1717

18+
import java.util.Arrays;
19+
import java.util.Collection;
1820
import java.util.Iterator;
1921
import java.util.Optional;
2022
import java.util.function.BiFunction;
@@ -24,6 +26,7 @@
2426
import org.springframework.data.mapping.PersistentProperty;
2527
import org.springframework.data.util.Streamable;
2628
import org.springframework.data.util.TypeInformation;
29+
import org.springframework.lang.Nullable;
2730
import org.springframework.util.Assert;
2831

2932
/**
@@ -44,9 +47,23 @@ public class PersistentEntities implements Streamable<PersistentEntity<?, ? exte
4447
public PersistentEntities(Iterable<? extends MappingContext<?, ?>> contexts) {
4548

4649
Assert.notNull(contexts, "MappingContexts must not be null!");
50+
4751
this.contexts = Streamable.of(contexts);
4852
}
4953

54+
/**
55+
* Creates a new {@link PersistentEntities} for the given {@link MappingContext}s.
56+
*
57+
* @param contexts must not be {@literal null}.
58+
* @return
59+
*/
60+
public static PersistentEntities of(MappingContext<?, ?>... contexts) {
61+
62+
Assert.notNull(contexts, "MappingContexts must not be null!");
63+
64+
return new PersistentEntities(Arrays.asList(contexts));
65+
}
66+
5067
/**
5168
* Returns the {@link PersistentEntity} for the given type. Will consider all {@link MappingContext}s registered but
5269
* return {@literal Optional#empty()} in case none of the registered ones already have a {@link PersistentEntity}
@@ -122,4 +139,79 @@ public Streamable<TypeInformation<?>> getManagedTypes() {
122139
.<PersistentEntity<?, ? extends PersistentProperty<?>>> flatMap(it -> it.getPersistentEntities().stream())
123140
.collect(Collectors.toList()).iterator();
124141
}
142+
143+
/**
144+
* Returns the {@link PersistentEntity} the given {@link PersistentProperty} refers to in case it's an association.
145+
* For direct aggregate references, that's simply the entity for the {@link PersistentProperty}'s actual type. If the
146+
* property type is not an entity - as it might rather refer to the identifier type - we either use the reference's
147+
* defined target type and fall back to trying to find a {@link PersistentEntity} identified by the
148+
* {@link PersistentProperty}'s actual type.
149+
*
150+
* @param property must not be {@literal null}.
151+
* @return
152+
* @since 2.1
153+
*/
154+
@Nullable
155+
public PersistentEntity<?, ?> getEntityUltimatelyReferredToBy(PersistentProperty<?> property) {
156+
157+
TypeInformation<?> propertyType = property.getTypeInformation().getActualType();
158+
159+
if (propertyType == null || !property.isAssociation()) {
160+
return null;
161+
}
162+
163+
Class<?> associationTargetType = property.getAssociationTargetType();
164+
165+
return associationTargetType == null //
166+
? getEntityIdentifiedBy(propertyType) //
167+
: getPersistentEntity(associationTargetType).orElseGet(() -> getEntityIdentifiedBy(propertyType));
168+
}
169+
170+
/**
171+
* Returns the type the given {@link PersistentProperty} ultimately refers to. In case it's of a unique identifier
172+
* type of an entity known it'll return the entity type.
173+
*
174+
* @param property must not be {@literal null}.
175+
* @return
176+
*/
177+
public TypeInformation<?> getTypeUltimatelyReferredToBy(PersistentProperty<?> property) {
178+
179+
Assert.notNull(property, "PersistentProperty must not be null!");
180+
181+
PersistentEntity<?, ?> entity = getEntityUltimatelyReferredToBy(property);
182+
183+
return entity == null //
184+
? property.getTypeInformation().getRequiredActualType() //
185+
: entity.getTypeInformation();
186+
}
187+
188+
/**
189+
* Returns the {@link PersistentEntity} identified by the given type.
190+
*
191+
* @param type
192+
* @return
193+
* @throws IllegalStateException if the entity cannot be detected uniquely as multiple ones might share the same
194+
* identifier.
195+
*/
196+
@Nullable
197+
private PersistentEntity<?, ?> getEntityIdentifiedBy(TypeInformation<?> type) {
198+
199+
Collection<PersistentEntity<?, ?>> entities = contexts.stream() //
200+
.flatMap(it -> it.getPersistentEntities().stream()) //
201+
.map(it -> it.getIdProperty()) //
202+
.filter(it -> it != null && type.equals(it.getTypeInformation().getActualType())) //
203+
.map(it -> it.getOwner()) //
204+
.collect(Collectors.toList());
205+
206+
if (entities.size() > 1) {
207+
208+
String message = "Found multiple entities identified by " + type.getType() + ": ";
209+
message += entities.stream().map(it -> it.getType().getName()).collect(Collectors.joining(", "));
210+
message += "! Introduce dedciated unique identifier types or explicitly define the target type in @Reference!";
211+
212+
throw new IllegalStateException(message);
213+
}
214+
215+
return entities.isEmpty() ? null : entities.iterator().next();
216+
}
125217
}

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,27 @@ public boolean usePropertyAccess() {
277277
return usePropertyAccess.get();
278278
}
279279

280+
/*
281+
* (non-Javadoc)
282+
* @see org.springframework.data.mapping.PersistentProperty#getAssociationTargetType()
283+
*/
284+
@Nullable
285+
@Override
286+
public Class<?> getAssociationTargetType() {
287+
288+
Reference reference = findAnnotation(Reference.class);
289+
290+
if (reference == null) {
291+
return isEntity() ? getActualType() : null;
292+
}
293+
294+
Class<?> targetType = reference.to();
295+
296+
return Class.class.equals(targetType) //
297+
? isEntity() ? getActualType() : null //
298+
: targetType;
299+
}
300+
280301
/*
281302
* (non-Javadoc)
282303
* @see org.springframework.data.mapping.model.AbstractPersistentProperty#toString()

src/test/java/org/springframework/data/mapping/context/PersistentEntitiesUnitTests.java

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@
2525
import org.junit.runner.RunWith;
2626
import org.mockito.Mock;
2727
import org.mockito.junit.MockitoJUnitRunner;
28+
import org.springframework.data.annotation.Id;
29+
import org.springframework.data.annotation.Reference;
30+
import org.springframework.data.mapping.PersistentEntity;
2831
import org.springframework.data.util.ClassTypeInformation;
2932

3033
/**
@@ -72,10 +75,100 @@ public void indicatesManagedType() {
7275
assertThat(entities.getManagedTypes()).contains(ClassTypeInformation.from(Sample.class));
7376

7477
assertThat(entities.getPersistentEntity(Sample.class)).hasValueSatisfying(it -> assertThat(entities).contains(it));
78+
}
79+
80+
@Test // DATACMNS-1318
81+
public void detectsReferredToEntity() {
82+
83+
SampleMappingContext context = new SampleMappingContext();
84+
context.getPersistentEntity(Sample.class);
85+
86+
SamplePersistentProperty property = context.getRequiredPersistentEntity(WithReference.class)//
87+
.getPersistentProperty("sampleId");
7588

89+
PersistentEntity<?, ?> referredToEntity = PersistentEntities.of(context).getEntityUltimatelyReferredToBy(property);
90+
91+
assertThat(referredToEntity).isNotNull();
92+
assertThat(referredToEntity.getType()).isEqualTo(Sample.class);
93+
}
94+
95+
@Test // DATACMNS-1318
96+
public void rejectsAmbiguousIdentifierType() {
97+
98+
SampleMappingContext context = new SampleMappingContext();
99+
context.getPersistentEntity(FirstWithLongId.class);
100+
context.getPersistentEntity(SecondWithLongId.class);
101+
102+
SamplePersistentProperty property = context.getRequiredPersistentEntity(WithReference.class) //
103+
.getPersistentProperty("longId");
104+
105+
PersistentEntities entities = PersistentEntities.of(context);
106+
107+
assertThatExceptionOfType(IllegalStateException.class)//
108+
.isThrownBy(() -> entities.getEntityUltimatelyReferredToBy(property)) //
109+
.withMessageContaining(FirstWithLongId.class.getName()) //
110+
.withMessageContaining(SecondWithLongId.class.getName()) //
111+
.withMessageContaining(Reference.class.getSimpleName());
112+
}
113+
114+
@Test // DATACMNS-1318
115+
public void allowsExplicitlyQualifiedReference() {
116+
117+
SampleMappingContext context = new SampleMappingContext();
118+
context.getPersistentEntity(FirstWithLongId.class);
119+
context.getPersistentEntity(SecondWithLongId.class);
120+
121+
SamplePersistentProperty property = context.getRequiredPersistentEntity(WithReference.class) //
122+
.getPersistentProperty("qualifiedLongId");
123+
124+
PersistentEntity<?, ?> entity = PersistentEntities.of(context).getEntityUltimatelyReferredToBy(property);
125+
126+
assertThat(entity).isNotNull();
127+
assertThat(entity.getType()).isEqualTo(FirstWithLongId.class);
128+
}
129+
130+
@Test // DATACMNS-1318
131+
public void allowsGenericReference() {
132+
133+
SampleMappingContext context = new SampleMappingContext();
134+
context.getPersistentEntity(FirstWithGenericId.class);
135+
context.getPersistentEntity(SecondWithGenericId.class);
136+
137+
SamplePersistentProperty property = context.getRequiredPersistentEntity(WithReference.class) //
138+
.getPersistentProperty("generic");
139+
140+
PersistentEntity<?, ?> entity = PersistentEntities.of(context).getEntityUltimatelyReferredToBy(property);
141+
142+
assertThat(entity).isNotNull();
143+
assertThat(entity.getType()).isEqualTo(SecondWithGenericId.class);
76144
}
77145

78146
static class Sample {
147+
@Id String id;
148+
}
79149

150+
static class WithReference {
151+
@Reference String sampleId;
152+
@Reference Long longId;
153+
@Reference(FirstWithLongId.class) Long qualifiedLongId;
154+
@Reference Identifier<SecondWithGenericId> generic;
80155
}
156+
157+
static class FirstWithLongId {
158+
@Id Long id;
159+
}
160+
161+
static class SecondWithLongId {
162+
@Id Long id;
163+
}
164+
165+
static class FirstWithGenericId {
166+
@Id Identifier<FirstWithGenericId> id;
167+
}
168+
169+
static class SecondWithGenericId {
170+
@Id Identifier<SecondWithGenericId> id;
171+
}
172+
173+
interface Identifier<T> {}
81174
}

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,11 @@ public boolean isAnnotationPresent(Class<? extends Annotation> annotationType) {
368368
public <A extends Annotation> A findPropertyOrOwnerAnnotation(Class<A> annotationType) {
369369
return null;
370370
}
371+
372+
@Override
373+
public Class<?> getAssociationTargetType() {
374+
return null;
375+
}
371376
}
372377

373378
static class Sample {

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import java.lang.annotation.Target;
2525
import java.util.Map;
2626
import java.util.Optional;
27+
import java.util.stream.Stream;
2728

2829
import org.junit.Before;
2930
import org.junit.Test;
@@ -33,6 +34,7 @@
3334
import org.springframework.data.annotation.AccessType.Type;
3435
import org.springframework.data.annotation.Id;
3536
import org.springframework.data.annotation.ReadOnlyProperty;
37+
import org.springframework.data.annotation.Reference;
3638
import org.springframework.data.annotation.Transient;
3739
import org.springframework.data.mapping.MappingException;
3840
import org.springframework.data.mapping.PersistentProperty;
@@ -239,6 +241,14 @@ public void getRequiredAnnotationThrowsException() {
239241
assertThatThrownBy(() -> property.getRequiredAnnotation(Transient.class)).isInstanceOf(IllegalStateException.class);
240242
}
241243

244+
@Test // DATACMNS-1318
245+
public void detectsUltimateAssociationTargetClass() {
246+
247+
Stream.of("toSample", "toSample2", "sample", "withoutAnnotation").forEach(it -> {
248+
assertThat(getProperty(WithReferences.class, it).getAssociationTargetType()).isEqualTo(Sample.class);
249+
});
250+
}
251+
242252
@SuppressWarnings("unchecked")
243253
private Map<Class<? extends Annotation>, Annotation> getAnnotationCache(SamplePersistentProperty property) {
244254
return (Map<Class<? extends Annotation>, Annotation>) ReflectionTestUtils.getField(property, "annotationCache");
@@ -410,4 +420,12 @@ public String getField() {
410420
@interface CustomReadOnly {
411421

412422
}
423+
424+
static class WithReferences {
425+
426+
@Reference(to = Sample.class) String toSample;
427+
@Reference(Sample.class) String toSample2;
428+
@Reference Sample sample;
429+
Sample withoutAnnotation;
430+
}
413431
}

0 commit comments

Comments
 (0)