diff --git a/ha-metadata/src/main/java/com/g2forge/habitat/metadata/access/annotation/AnnotationPredicate.java b/ha-metadata/src/main/java/com/g2forge/habitat/metadata/access/annotation/AnnotationPredicate.java index 718af35..f29b08c 100644 --- a/ha-metadata/src/main/java/com/g2forge/habitat/metadata/access/annotation/AnnotationPredicate.java +++ b/ha-metadata/src/main/java/com/g2forge/habitat/metadata/access/annotation/AnnotationPredicate.java @@ -1,7 +1,14 @@ package com.g2forge.habitat.metadata.access.annotation; import java.lang.annotation.Annotation; +import java.lang.annotation.Inherited; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import com.g2forge.alexandria.java.core.helpers.HCollection; +import com.g2forge.alexandria.java.core.helpers.HTree; import com.g2forge.alexandria.java.reflect.annotations.IJavaAnnotations; import com.g2forge.habitat.metadata.annotations.ContainerAnnotationReflection; import com.g2forge.habitat.metadata.type.predicate.IAnnotationPredicateType; @@ -30,25 +37,67 @@ public class AnnotationPredicate implements IPredicate @Override public T get0() { final Class annotationType = getType().getAnnotationType(); - final IJavaAnnotations annotations = getSubject().getAnnotations(); - final T retVal = annotations.getAnnotation(annotationType); - if (retVal != null) return retVal; - - final ContainerAnnotationReflection containerAnnotationReflection = getContainerAnnotationReflection(); - if (containerAnnotationReflection == null) return null; - final Annotation repeatable = annotations.getAnnotation(containerAnnotationReflection.getRepeatable()); - if (repeatable == null) return null; - return containerAnnotationReflection.getCollectionStrategy().builder().add(repeatable).get(); + if (annotationType.isAnnotationPresent(Inherited.class)) { + // Inherited annotations + final ContainerAnnotationReflection containerAnnotationReflection = getContainerAnnotationReflection(); + if (containerAnnotationReflection == null) { + final Optional subject = HTree.find(getSubject(), IElementSubject::getParents, s -> s.getAnnotations().isAnnotated(annotationType)); + if (!subject.isPresent()) return null; + final IJavaAnnotations annotations = subject.get().getAnnotations(); + return annotations.getAnnotation(annotationType); + } else { + final Class repeatableAnnotationType = containerAnnotationReflection.getRepeatable(); + final List repeatableAnnotationValues = HTree.dfs(getSubject(), IElementSubject::getParents, false).flatMap(s -> { + final IJavaAnnotations annotations = s.getAnnotations(); + final List retVal = new ArrayList<>(); + + final T annotation = annotations.getAnnotation(annotationType); + if (annotation != null) retVal.addAll(HCollection.asListIterable(containerAnnotationReflection.getCollectionStrategy().iterable(annotation))); + + final Annotation repeatable = annotations.getAnnotation(repeatableAnnotationType); + if (repeatable != null) retVal.add(repeatable); + + return retVal.stream(); + }).collect(Collectors.toList()); + return containerAnnotationReflection.getCollectionStrategy().builder().add(repeatableAnnotationValues).get(); + } + } else { + // Non-inherited annotations present on the subject + final IJavaAnnotations annotations = getSubject().getAnnotations(); + final T retVal = annotations.getAnnotation(annotationType); + if (retVal != null) return retVal; + + // Non-inherited annotations which are repeated, and whose repeatable child is on the subject + final ContainerAnnotationReflection containerAnnotationReflection = getContainerAnnotationReflection(); + if (containerAnnotationReflection == null) return null; + final Annotation repeatable = annotations.getAnnotation(containerAnnotationReflection.getRepeatable()); + if (repeatable == null) return null; + return containerAnnotationReflection.getCollectionStrategy().builder().add(repeatable).get(); + } } @Override public boolean isPresent() { - final IJavaAnnotations annotations = getSubject().getAnnotations(); - final boolean retVal = annotations.isAnnotated(getType().getAnnotationType()); - if (retVal) return true; + final Class annotationType = getType().getAnnotationType(); + if (annotationType.isAnnotationPresent(Inherited.class)) { + // Inherited annotations + final ContainerAnnotationReflection containerAnnotationReflection = getContainerAnnotationReflection(); + if (containerAnnotationReflection == null) return HTree.find(getSubject(), IElementSubject::getParents, s -> s.getAnnotations().isAnnotated(annotationType)).isPresent(); + else { + final Class repeatableAnnotationType = containerAnnotationReflection.getRepeatable(); + return HTree.find(getSubject(), IElementSubject::getParents, s -> { + final IJavaAnnotations annotations = s.getAnnotations(); + return annotations.isAnnotated(annotationType) || annotations.isAnnotated(repeatableAnnotationType); + }).isPresent(); + } + } else { + final IJavaAnnotations annotations = getSubject().getAnnotations(); + final boolean retVal = annotations.isAnnotated(annotationType); + if (retVal) return true; - final ContainerAnnotationReflection containerAnnotationReflection = getContainerAnnotationReflection(); - if (containerAnnotationReflection == null) return false; - return annotations.isAnnotated(containerAnnotationReflection.getRepeatable()); + final ContainerAnnotationReflection containerAnnotationReflection = getContainerAnnotationReflection(); + if (containerAnnotationReflection == null) return false; + return annotations.isAnnotated(containerAnnotationReflection.getRepeatable()); + } } } \ No newline at end of file diff --git a/ha-metadata/src/main/java/com/g2forge/habitat/metadata/value/implementations/ElementSubject.java b/ha-metadata/src/main/java/com/g2forge/habitat/metadata/value/implementations/ElementSubject.java index 44952cd..e18ba86 100644 --- a/ha-metadata/src/main/java/com/g2forge/habitat/metadata/value/implementations/ElementSubject.java +++ b/ha-metadata/src/main/java/com/g2forge/habitat/metadata/value/implementations/ElementSubject.java @@ -1,7 +1,10 @@ package com.g2forge.habitat.metadata.value.implementations; import java.lang.reflect.AnnotatedElement; +import java.util.ArrayList; +import java.util.List; +import com.g2forge.alexandria.java.core.helpers.HCollection; import com.g2forge.alexandria.java.reflect.annotations.ElementJavaAnnotations; import com.g2forge.alexandria.java.reflect.annotations.IJavaAnnotations; import com.g2forge.habitat.metadata.type.subject.ISubjectType; @@ -19,6 +22,21 @@ @Builder(toBuilder = true) @RequiredArgsConstructor class ElementSubject implements IElementSubject { + protected static List getParents(IElementSubject _this, AnnotatedElement element) { + if (element instanceof Class) { + @SuppressWarnings("rawtypes") + final Class cast = (Class) element; + + final List parents = new ArrayList<>(); + if (cast.getSuperclass() != null) parents.add(new ElementSubject(_this.getContext(), cast.getSuperclass())); + for (Class parent : cast.getInterfaces()) { + parents.add(new ElementSubject(_this.getContext(), parent)); + } + return parents; + } + return HCollection.emptyList(); + } + @ToString.Exclude protected final IMetadataValueContext context; @@ -33,4 +51,9 @@ class ElementSubject implements IElementSubject { @EqualsAndHashCode.Exclude @ToString.Exclude private final IJavaAnnotations annotations = new ElementJavaAnnotations(getElement()); + + @Override + public List getParents() { + return getParents(this, getElement()); + } } diff --git a/ha-metadata/src/main/java/com/g2forge/habitat/metadata/value/implementations/ElementValueSubject.java b/ha-metadata/src/main/java/com/g2forge/habitat/metadata/value/implementations/ElementValueSubject.java index 1848788..85303ed 100644 --- a/ha-metadata/src/main/java/com/g2forge/habitat/metadata/value/implementations/ElementValueSubject.java +++ b/ha-metadata/src/main/java/com/g2forge/habitat/metadata/value/implementations/ElementValueSubject.java @@ -2,11 +2,13 @@ import java.lang.annotation.Annotation; import java.lang.reflect.AnnotatedElement; +import java.util.List; import com.g2forge.alexandria.java.reflect.annotations.ElementJavaAnnotations; import com.g2forge.alexandria.java.reflect.annotations.IJavaAnnotations; import com.g2forge.habitat.metadata.type.subject.IValueSubjectType; import com.g2forge.habitat.metadata.value.IMetadataValueContext; +import com.g2forge.habitat.metadata.value.subject.IElementSubject; import com.g2forge.habitat.metadata.value.subject.IValueSubject; import lombok.Builder; @@ -19,7 +21,7 @@ @Data @Builder(toBuilder = true) @RequiredArgsConstructor -public class ElementValueSubject implements IValueSubject { +class ElementValueSubject implements IValueSubject { @ToString.Exclude protected final IMetadataValueContext context; @@ -46,4 +48,9 @@ protected IValueSubjectType computeType() { return (IValueSubjectType) getContext().getTypeContext().subject(elementType, valueType); } + + @Override + public List getParents() { + return ElementSubject.getParents(this, getElement()); + } } diff --git a/ha-metadata/src/main/java/com/g2forge/habitat/metadata/value/subject/IElementSubject.java b/ha-metadata/src/main/java/com/g2forge/habitat/metadata/value/subject/IElementSubject.java index e546b96..9cc6818 100644 --- a/ha-metadata/src/main/java/com/g2forge/habitat/metadata/value/subject/IElementSubject.java +++ b/ha-metadata/src/main/java/com/g2forge/habitat/metadata/value/subject/IElementSubject.java @@ -1,6 +1,7 @@ package com.g2forge.habitat.metadata.value.subject; import java.lang.reflect.AnnotatedElement; +import java.util.List; import com.g2forge.alexandria.java.reflect.annotations.IJavaAnnotated; import com.g2forge.habitat.metadata.type.subject.IElementSubjectType; @@ -8,4 +9,6 @@ @SubjectType(IElementSubjectType.class) public interface IElementSubject extends ISubject, IJavaAnnotated { public AnnotatedElement getElement(); + + public List getParents(); } diff --git a/ha-metadata/src/test/java/com/g2forge/habitat/metadata/TestInheritedAnnotationMetadata.java b/ha-metadata/src/test/java/com/g2forge/habitat/metadata/TestInheritedAnnotationMetadata.java new file mode 100644 index 0000000..480b181 --- /dev/null +++ b/ha-metadata/src/test/java/com/g2forge/habitat/metadata/TestInheritedAnnotationMetadata.java @@ -0,0 +1,116 @@ +package com.g2forge.habitat.metadata; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.junit.Test; + +import com.g2forge.alexandria.java.core.helpers.HCollection; +import com.g2forge.alexandria.test.HAssert; +import com.g2forge.habitat.metadata.value.predicate.IPredicate; + +/** + * This is similar to {@link TestRepeatableAnnotationMetadata} except that {@link TestInheritedAnnotationMetadata.Contained} is {@link Inherited}. + */ +public class TestInheritedAnnotationMetadata { + @Contained("AC") + @NonRepeatable("ANR") + public interface A {} + + @Contained("BC") + @NonRepeatable("BNR") + public interface B extends A {} + + public interface C extends A {} + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + @Repeatable(Container.class) + @Inherited + public @interface Contained { + public String value(); + } + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + @Inherited + public @interface Container { + public Contained[] value(); + } + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + @Inherited + public @interface NonRepeatable { + public String value(); + } + + @Test + public void aContained() { + final IPredicate contained = Metadata.getStandard().of(A.class).bind(Contained.class); + HAssert.assertTrue(contained.isPresent()); + HAssert.assertEquals("AC", contained.get0().value()); + } + + @Test + public void aContainer() { + final IPredicate container = Metadata.getStandard().of(A.class).bind(Container.class); + HAssert.assertTrue(container.isPresent()); + HAssert.assertEquals(HCollection.asList("AC"), Stream.of(container.get0().value()).map(Contained::value).collect(Collectors.toList())); + } + + @Test + public void aNonRepeatable() { + final IPredicate nonRepeatable = Metadata.getStandard().of(A.class).bind(NonRepeatable.class); + HAssert.assertTrue(nonRepeatable.isPresent()); + HAssert.assertEquals("ANR", nonRepeatable.get0().value()); + } + + @Test + public void bContained() { + final IPredicate contained = Metadata.getStandard().of(B.class).bind(Contained.class); + HAssert.assertTrue(contained.isPresent()); + HAssert.assertEquals("BC", contained.get0().value()); + } + + @Test + public void bContainer() { + final IPredicate container = Metadata.getStandard().of(B.class).bind(Container.class); + HAssert.assertTrue(container.isPresent()); + HAssert.assertEquals(HCollection.asList("BC", "AC"), Stream.of(container.get0().value()).map(Contained::value).collect(Collectors.toList())); + } + + @Test + public void bNonRepeatable() { + final IPredicate nonRepeatable = Metadata.getStandard().of(B.class).bind(NonRepeatable.class); + HAssert.assertTrue(nonRepeatable.isPresent()); + HAssert.assertEquals("BNR", nonRepeatable.get0().value()); + } + + @Test + public void cContained() { + final IPredicate contained = Metadata.getStandard().of(C.class).bind(Contained.class); + HAssert.assertTrue(contained.isPresent()); + HAssert.assertEquals("AC", contained.get0().value()); + } + + @Test + public void cContainer() { + final IPredicate container = Metadata.getStandard().of(C.class).bind(Container.class); + HAssert.assertTrue(container.isPresent()); + HAssert.assertEquals(HCollection.asList("AC"), Stream.of(container.get0().value()).map(Contained::value).collect(Collectors.toList())); + } + + @Test + public void cNonRepeatable() { + final IPredicate nonRepeatable = Metadata.getStandard().of(C.class).bind(NonRepeatable.class); + HAssert.assertTrue(nonRepeatable.isPresent()); + HAssert.assertEquals("ANR", nonRepeatable.get0().value()); + } +}