Support repeatable annotations as composed annotations

Prior to this commit, AnnotationUtils supported searching for
repeatable annotations even if the repeatable annotation was declared
on a custom stereotype annotation. However, there was no support for
merging of attributes in composed repeatable annotations. In other
words, it was not possible for a custom annotation to override
attributes in a repeatable annotation.

This commit addresses this by introducing
findMergedRepeatableAnnotations() methods in AnnotatedElementUtils.
These new methods provide full support for explicit annotation
attribute overrides configured via @AliasFor (as well as
convention-based overrides) with "find semantics".

Issue: SPR-13973
This commit is contained in:
Sam Brannen
2016-03-23 20:34:43 +01:00
parent 63115ed6eb
commit 2535469099
3 changed files with 487 additions and 57 deletions

View File

@@ -0,0 +1,230 @@
/*
* Copyright 2002-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.core.annotation;
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.lang.reflect.AnnotatedElement;
import java.util.Iterator;
import java.util.Set;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import static org.hamcrest.CoreMatchers.isA;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.startsWith;
import static org.junit.Assert.*;
import static org.springframework.core.annotation.AnnotatedElementUtils.*;
/**
* Unit tests that verify support for finding all composed, repeatable
* annotations on a single annotated element.
*
* <p>See <a href="https://jira.spring.io/browse/SPR-13973">SPR-13973</a>.
*
* @author Sam Brannen
* @since 4.3
* @see AnnotatedElementUtils
* @see AnnotatedElementUtilsTests
*/
public class ComposedRepeatableAnnotationsTests {
@Rule
public final ExpectedException exception = ExpectedException.none();
@Test
public void nonRepeatableAnnotation() {
exception.expect(IllegalArgumentException.class);
exception.expectMessage(startsWith("annotationType must be a repeatable annotation"));
exception.expectMessage(containsString("failed to resolve container type for"));
exception.expectMessage(containsString(NonRepeatable.class.getName()));
findMergedRepeatableAnnotations(getClass(), NonRepeatable.class);
}
@Test
public void invalidRepeatableAnnotationContainerMissingValueAttribute() {
exception.expect(AnnotationConfigurationException.class);
exception.expectMessage(startsWith("Invalid declaration of container type"));
exception.expectMessage(containsString(ContainerMissingValueAttribute.class.getName()));
exception.expectMessage(containsString("for repeatable annotation"));
exception.expectMessage(containsString(InvalidRepeatable.class.getName()));
exception.expectCause(isA(NoSuchMethodException.class));
findMergedRepeatableAnnotations(getClass(), InvalidRepeatable.class, ContainerMissingValueAttribute.class);
}
@Test
public void invalidRepeatableAnnotationContainerWithNonArrayValueAttribute() {
exception.expect(AnnotationConfigurationException.class);
exception.expectMessage(startsWith("Container type"));
exception.expectMessage(containsString(ContainerWithNonArrayValueAttribute.class.getName()));
exception.expectMessage(containsString("must declare a 'value' attribute for an array of type"));
exception.expectMessage(containsString(InvalidRepeatable.class.getName()));
findMergedRepeatableAnnotations(getClass(), InvalidRepeatable.class, ContainerWithNonArrayValueAttribute.class);
}
@Test
public void invalidRepeatableAnnotationContainerWithArrayValueAttributeButWrongComponentType() {
exception.expect(AnnotationConfigurationException.class);
exception.expectMessage(startsWith("Container type"));
exception.expectMessage(containsString(ContainerWithArrayValueAttributeButWrongComponentType.class.getName()));
exception.expectMessage(containsString("must declare a 'value' attribute for an array of type"));
exception.expectMessage(containsString(InvalidRepeatable.class.getName()));
findMergedRepeatableAnnotations(getClass(), InvalidRepeatable.class,
ContainerWithArrayValueAttributeButWrongComponentType.class);
}
@Test
public void repeatableAnnotationsOnClass() {
assertRepeatableAnnotations(RepeatableClass.class);
}
@Test
public void repeatableAnnotationsOnSuperclass() {
assertRepeatableAnnotations(SubRepeatableClass.class);
}
@Test
public void composedRepeatableAnnotationsOnClass() {
assertRepeatableAnnotations(ComposedRepeatableClass.class);
}
@Test
public void composedRepeatableAnnotationsMixedWithContainerOnClass() {
assertRepeatableAnnotations(ComposedRepeatableMixedWithContainerClass.class);
}
@Test
public void composedContainerForRepeatableAnnotationsOnClass() {
assertRepeatableAnnotations(ComposedContainerClass.class);
}
private void assertRepeatableAnnotations(AnnotatedElement element) {
assertNotNull(element);
Set<PeteRepeat> peteRepeats = findMergedRepeatableAnnotations(element, PeteRepeat.class);
assertNotNull(peteRepeats);
assertEquals(3, peteRepeats.size());
Iterator<PeteRepeat> iterator = peteRepeats.iterator();
assertEquals("A", iterator.next().value());
assertEquals("B", iterator.next().value());
assertEquals("C", iterator.next().value());
}
// -------------------------------------------------------------------------
@Retention(RetentionPolicy.RUNTIME)
@interface NonRepeatable {
}
@Retention(RetentionPolicy.RUNTIME)
@interface ContainerMissingValueAttribute {
// InvalidRepeatable[] value();
}
@Retention(RetentionPolicy.RUNTIME)
@interface ContainerWithNonArrayValueAttribute {
InvalidRepeatable value();
}
@Retention(RetentionPolicy.RUNTIME)
@interface ContainerWithArrayValueAttributeButWrongComponentType {
String[] value();
}
@Retention(RetentionPolicy.RUNTIME)
@interface InvalidRepeatable {
}
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@interface PeteRepeats {
PeteRepeat[] value();
}
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Repeatable(PeteRepeats.class)
@interface PeteRepeat {
String value();
}
@PeteRepeat("shadowed")
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@interface ForPetesSake {
@AliasFor(annotation = PeteRepeat.class)
String value();
}
@PeteRepeat("shadowed")
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@interface ForTheLoveOfFoo {
@AliasFor(annotation = PeteRepeat.class)
String value();
}
@PeteRepeats({ @PeteRepeat("B"), @PeteRepeat("C") })
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@interface ComposedContainer {
}
@PeteRepeat("A")
@PeteRepeats({ @PeteRepeat("B"), @PeteRepeat("C") })
static class RepeatableClass {
}
static class SubRepeatableClass extends RepeatableClass {
}
@ForPetesSake("B")
@ForTheLoveOfFoo("C")
@PeteRepeat("A")
static class ComposedRepeatableClass {
}
@ForPetesSake("C")
@PeteRepeats(@PeteRepeat("A"))
@PeteRepeat("B")
static class ComposedRepeatableMixedWithContainerClass {
}
@PeteRepeat("A")
@ComposedContainer
static class ComposedContainerClass {
}
}