diff --git a/src/main/java/org/springframework/data/annotation/Reference.java b/src/main/java/org/springframework/data/annotation/Reference.java index 0b2bfd3be..f87dc97bd 100644 --- a/src/main/java/org/springframework/data/annotation/Reference.java +++ b/src/main/java/org/springframework/data/annotation/Reference.java @@ -21,6 +21,8 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.springframework.core.annotation.AliasFor; + /** * Meta-annotation to be used to annotate annotations that mark references to other objects. * @@ -30,4 +32,22 @@ import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) @Target(value = { FIELD, METHOD, ANNOTATION_TYPE }) public @interface Reference { + + /** + * Explicitly define the target type of the reference. Used in case the annotated property is not the target type but + * rather an identifier and/or if that identifier type is not uniquely identifying the target entity. + * + * @return + */ + @AliasFor(attribute = "to") + Class value() default Class.class; + + /** + * Explicitly define the target type of the reference. Used in case the annotated property is not the target type but + * rather an identifier and/or if that identifier type is not uniquely identifying the target entity. + * + * @return + */ + @AliasFor(attribute = "value") + Class to() default Class.class; } diff --git a/src/main/java/org/springframework/data/mapping/PersistentProperty.java b/src/main/java/org/springframework/data/mapping/PersistentProperty.java index cc5be87f5..2814bf36b 100644 --- a/src/main/java/org/springframework/data/mapping/PersistentProperty.java +++ b/src/main/java/org/springframework/data/mapping/PersistentProperty.java @@ -333,4 +333,15 @@ public interface PersistentProperty

> { return AnnotatedElementUtils.hasAnnotation(getActualType(), annotationType); } + + /** + * Return the type the property refers to in case it's an association. + * + * @return the type the property refers to in case it's an association or {@literal null} in case it's not an + * association, the target entity type is not explicitly defined (either explicitly or through the property + * type itself). + * @since 2.1 + */ + @Nullable + Class getAssociationTargetType(); } diff --git a/src/main/java/org/springframework/data/mapping/context/PersistentEntities.java b/src/main/java/org/springframework/data/mapping/context/PersistentEntities.java index 3a0c864bf..176bcf8b9 100644 --- a/src/main/java/org/springframework/data/mapping/context/PersistentEntities.java +++ b/src/main/java/org/springframework/data/mapping/context/PersistentEntities.java @@ -15,6 +15,8 @@ */ package org.springframework.data.mapping.context; +import java.util.Arrays; +import java.util.Collection; import java.util.Iterator; import java.util.Optional; import java.util.function.BiFunction; @@ -24,6 +26,7 @@ import org.springframework.data.mapping.PersistentEntity; import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.util.Streamable; import org.springframework.data.util.TypeInformation; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -44,9 +47,23 @@ public class PersistentEntities implements Streamable> contexts) { Assert.notNull(contexts, "MappingContexts must not be null!"); + this.contexts = Streamable.of(contexts); } + /** + * Creates a new {@link PersistentEntities} for the given {@link MappingContext}s. + * + * @param contexts must not be {@literal null}. + * @return + */ + public static PersistentEntities of(MappingContext... contexts) { + + Assert.notNull(contexts, "MappingContexts must not be null!"); + + return new PersistentEntities(Arrays.asList(contexts)); + } + /** * Returns the {@link PersistentEntity} for the given type. Will consider all {@link MappingContext}s registered but * return {@literal Optional#empty()} in case none of the registered ones already have a {@link PersistentEntity} @@ -122,4 +139,79 @@ public class PersistentEntities implements Streamable>> flatMap(it -> it.getPersistentEntities().stream()) .collect(Collectors.toList()).iterator(); } + + /** + * Returns the {@link PersistentEntity} the given {@link PersistentProperty} refers to in case it's an association. + * For direct aggregate references, that's simply the entity for the {@link PersistentProperty}'s actual type. If the + * property type is not an entity - as it might rather refer to the identifier type - we either use the reference's + * defined target type and fall back to trying to find a {@link PersistentEntity} identified by the + * {@link PersistentProperty}'s actual type. + * + * @param property must not be {@literal null}. + * @return + * @since 2.1 + */ + @Nullable + public PersistentEntity getEntityUltimatelyReferredToBy(PersistentProperty property) { + + TypeInformation propertyType = property.getTypeInformation().getActualType(); + + if (propertyType == null || !property.isAssociation()) { + return null; + } + + Class associationTargetType = property.getAssociationTargetType(); + + return associationTargetType == null // + ? getEntityIdentifiedBy(propertyType) // + : getPersistentEntity(associationTargetType).orElseGet(() -> getEntityIdentifiedBy(propertyType)); + } + + /** + * Returns the type the given {@link PersistentProperty} ultimately refers to. In case it's of a unique identifier + * type of an entity known it'll return the entity type. + * + * @param property must not be {@literal null}. + * @return + */ + public TypeInformation getTypeUltimatelyReferredToBy(PersistentProperty property) { + + Assert.notNull(property, "PersistentProperty must not be null!"); + + PersistentEntity entity = getEntityUltimatelyReferredToBy(property); + + return entity == null // + ? property.getTypeInformation().getRequiredActualType() // + : entity.getTypeInformation(); + } + + /** + * Returns the {@link PersistentEntity} identified by the given type. + * + * @param type + * @return + * @throws IllegalStateException if the entity cannot be detected uniquely as multiple ones might share the same + * identifier. + */ + @Nullable + private PersistentEntity getEntityIdentifiedBy(TypeInformation type) { + + Collection> entities = contexts.stream() // + .flatMap(it -> it.getPersistentEntities().stream()) // + .map(it -> it.getIdProperty()) // + .filter(it -> it != null && type.equals(it.getTypeInformation().getActualType())) // + .map(it -> it.getOwner()) // + .collect(Collectors.toList()); + + if (entities.size() > 1) { + + String message = "Found multiple entities identified by " + type.getType() + ": "; + message += entities.stream().map(it -> it.getType().getName()).collect(Collectors.joining(", ")); + message += "! Introduce dedciated unique identifier types or explicitly define the target type in @Reference!"; + + throw new IllegalStateException(message); + } + + return entities.isEmpty() ? null : entities.iterator().next(); + } } diff --git a/src/main/java/org/springframework/data/mapping/model/AnnotationBasedPersistentProperty.java b/src/main/java/org/springframework/data/mapping/model/AnnotationBasedPersistentProperty.java index 06f1ebc77..9f94c2e96 100644 --- a/src/main/java/org/springframework/data/mapping/model/AnnotationBasedPersistentProperty.java +++ b/src/main/java/org/springframework/data/mapping/model/AnnotationBasedPersistentProperty.java @@ -277,6 +277,27 @@ public abstract class AnnotationBasedPersistentProperty

getAssociationTargetType() { + + Reference reference = findAnnotation(Reference.class); + + if (reference == null) { + return isEntity() ? getActualType() : null; + } + + Class targetType = reference.to(); + + return Class.class.equals(targetType) // + ? isEntity() ? getActualType() : null // + : targetType; + } + /* * (non-Javadoc) * @see org.springframework.data.mapping.model.AbstractPersistentProperty#toString() diff --git a/src/test/java/org/springframework/data/mapping/context/PersistentEntitiesUnitTests.java b/src/test/java/org/springframework/data/mapping/context/PersistentEntitiesUnitTests.java index c05f6a509..ddf9ae07a 100755 --- a/src/test/java/org/springframework/data/mapping/context/PersistentEntitiesUnitTests.java +++ b/src/test/java/org/springframework/data/mapping/context/PersistentEntitiesUnitTests.java @@ -25,6 +25,9 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.data.annotation.Id; +import org.springframework.data.annotation.Reference; +import org.springframework.data.mapping.PersistentEntity; import org.springframework.data.util.ClassTypeInformation; /** @@ -72,10 +75,100 @@ public class PersistentEntitiesUnitTests { assertThat(entities.getManagedTypes()).contains(ClassTypeInformation.from(Sample.class)); assertThat(entities.getPersistentEntity(Sample.class)).hasValueSatisfying(it -> assertThat(entities).contains(it)); + } + @Test // DATACMNS-1318 + public void detectsReferredToEntity() { + + SampleMappingContext context = new SampleMappingContext(); + context.getPersistentEntity(Sample.class); + + SamplePersistentProperty property = context.getRequiredPersistentEntity(WithReference.class)// + .getPersistentProperty("sampleId"); + + PersistentEntity referredToEntity = PersistentEntities.of(context).getEntityUltimatelyReferredToBy(property); + + assertThat(referredToEntity).isNotNull(); + assertThat(referredToEntity.getType()).isEqualTo(Sample.class); + } + + @Test // DATACMNS-1318 + public void rejectsAmbiguousIdentifierType() { + + SampleMappingContext context = new SampleMappingContext(); + context.getPersistentEntity(FirstWithLongId.class); + context.getPersistentEntity(SecondWithLongId.class); + + SamplePersistentProperty property = context.getRequiredPersistentEntity(WithReference.class) // + .getPersistentProperty("longId"); + + PersistentEntities entities = PersistentEntities.of(context); + + assertThatExceptionOfType(IllegalStateException.class)// + .isThrownBy(() -> entities.getEntityUltimatelyReferredToBy(property)) // + .withMessageContaining(FirstWithLongId.class.getName()) // + .withMessageContaining(SecondWithLongId.class.getName()) // + .withMessageContaining(Reference.class.getSimpleName()); + } + + @Test // DATACMNS-1318 + public void allowsExplicitlyQualifiedReference() { + + SampleMappingContext context = new SampleMappingContext(); + context.getPersistentEntity(FirstWithLongId.class); + context.getPersistentEntity(SecondWithLongId.class); + + SamplePersistentProperty property = context.getRequiredPersistentEntity(WithReference.class) // + .getPersistentProperty("qualifiedLongId"); + + PersistentEntity entity = PersistentEntities.of(context).getEntityUltimatelyReferredToBy(property); + + assertThat(entity).isNotNull(); + assertThat(entity.getType()).isEqualTo(FirstWithLongId.class); + } + + @Test // DATACMNS-1318 + public void allowsGenericReference() { + + SampleMappingContext context = new SampleMappingContext(); + context.getPersistentEntity(FirstWithGenericId.class); + context.getPersistentEntity(SecondWithGenericId.class); + + SamplePersistentProperty property = context.getRequiredPersistentEntity(WithReference.class) // + .getPersistentProperty("generic"); + + PersistentEntity entity = PersistentEntities.of(context).getEntityUltimatelyReferredToBy(property); + + assertThat(entity).isNotNull(); + assertThat(entity.getType()).isEqualTo(SecondWithGenericId.class); } static class Sample { - + @Id String id; } + + static class WithReference { + @Reference String sampleId; + @Reference Long longId; + @Reference(FirstWithLongId.class) Long qualifiedLongId; + @Reference Identifier generic; + } + + static class FirstWithLongId { + @Id Long id; + } + + static class SecondWithLongId { + @Id Long id; + } + + static class FirstWithGenericId { + @Id Identifier id; + } + + static class SecondWithGenericId { + @Id Identifier id; + } + + interface Identifier {} } diff --git a/src/test/java/org/springframework/data/mapping/model/AbstractPersistentPropertyUnitTests.java b/src/test/java/org/springframework/data/mapping/model/AbstractPersistentPropertyUnitTests.java index def90c749..a979a75a8 100755 --- a/src/test/java/org/springframework/data/mapping/model/AbstractPersistentPropertyUnitTests.java +++ b/src/test/java/org/springframework/data/mapping/model/AbstractPersistentPropertyUnitTests.java @@ -368,6 +368,11 @@ public class AbstractPersistentPropertyUnitTests { public A findPropertyOrOwnerAnnotation(Class annotationType) { return null; } + + @Override + public Class getAssociationTargetType() { + return null; + } } static class Sample { 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 faa55499c..2dd950f37 100755 --- a/src/test/java/org/springframework/data/mapping/model/AnnotationBasedPersistentPropertyUnitTests.java +++ b/src/test/java/org/springframework/data/mapping/model/AnnotationBasedPersistentPropertyUnitTests.java @@ -24,6 +24,7 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.util.Map; import java.util.Optional; +import java.util.stream.Stream; import org.junit.Before; import org.junit.Test; @@ -33,6 +34,7 @@ import org.springframework.data.annotation.AccessType; import org.springframework.data.annotation.AccessType.Type; import org.springframework.data.annotation.Id; import org.springframework.data.annotation.ReadOnlyProperty; +import org.springframework.data.annotation.Reference; import org.springframework.data.annotation.Transient; import org.springframework.data.mapping.MappingException; import org.springframework.data.mapping.PersistentProperty; @@ -239,6 +241,14 @@ public class AnnotationBasedPersistentPropertyUnitTests

property.getRequiredAnnotation(Transient.class)).isInstanceOf(IllegalStateException.class); } + @Test // DATACMNS-1318 + public void detectsUltimateAssociationTargetClass() { + + Stream.of("toSample", "toSample2", "sample", "withoutAnnotation").forEach(it -> { + assertThat(getProperty(WithReferences.class, it).getAssociationTargetType()).isEqualTo(Sample.class); + }); + } + @SuppressWarnings("unchecked") private Map, Annotation> getAnnotationCache(SamplePersistentProperty property) { return (Map, Annotation>) ReflectionTestUtils.getField(property, "annotationCache"); @@ -410,4 +420,12 @@ public class AnnotationBasedPersistentPropertyUnitTests