From 61e489b67e9e3d5af7a30605f68a674faf894223 Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Thu, 17 May 2018 11:43:00 +0200 Subject: [PATCH] DATACMNS-1318 - Allow inspection of a reference's ultimate target entity. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../data/annotation/Reference.java | 20 ++++ .../data/mapping/PersistentProperty.java | 11 +++ .../mapping/context/PersistentEntities.java | 92 ++++++++++++++++++ .../AnnotationBasedPersistentProperty.java | 21 ++++ .../context/PersistentEntitiesUnitTests.java | 95 ++++++++++++++++++- .../AbstractPersistentPropertyUnitTests.java | 5 + ...ationBasedPersistentPropertyUnitTests.java | 18 ++++ 7 files changed, 261 insertions(+), 1 deletion(-) 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