diff --git a/spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigUtils.java b/spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigUtils.java index bf8c0deab63..e36e5c8b117 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigUtils.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigUtils.java @@ -16,7 +16,9 @@ package org.springframework.context.annotation; +import java.util.Collections; import java.util.LinkedHashSet; +import java.util.Map; import java.util.Set; import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; @@ -32,6 +34,7 @@ import org.springframework.context.support.GenericApplicationContext; import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.core.type.AnnotatedTypeMetadata; +import org.springframework.core.type.AnnotationMetadata; import org.springframework.util.ClassUtils; /** @@ -44,6 +47,7 @@ import org.springframework.util.ClassUtils; * @author Mark Fisher * @author Juergen Hoeller * @author Chris Beams + * @author Phillip Webb * @since 2.5 * @see ContextAnnotationAutowireCandidateResolver * @see CommonAnnotationBeanPostProcessor @@ -297,12 +301,40 @@ public class AnnotationConfigUtils { return ScopedProxyCreator.createScopedProxy(definition, registry, proxyTargetClass); } - static AnnotationAttributes attributesFor(AnnotatedTypeMetadata metadata, Class annoClass) { - return attributesFor(metadata, annoClass.getName()); + static AnnotationAttributes attributesFor(AnnotatedTypeMetadata metadata, Class annotationClass) { + return attributesFor(metadata, annotationClass.getName()); } - static AnnotationAttributes attributesFor(AnnotatedTypeMetadata metadata, String annoClassName) { - return AnnotationAttributes.fromMap(metadata.getAnnotationAttributes(annoClassName, false)); + static AnnotationAttributes attributesFor(AnnotatedTypeMetadata metadata, String annotationClassName) { + return AnnotationAttributes.fromMap(metadata.getAnnotationAttributes(annotationClassName, false)); + } + + static Set attributesForRepeatable(AnnotationMetadata metadata, + Class containerClass, Class annotationClass) { + return attributesForRepeatable(metadata, containerClass.getName(), annotationClass.getName()); + } + + @SuppressWarnings("unchecked") + static Set attributesForRepeatable(AnnotationMetadata metadata, + String containerClassName, String annotationClassName) { + Set result = new LinkedHashSet(); + + addAttributesIfNotNull(result, metadata.getAnnotationAttributes(annotationClassName, false)); + + Map container = metadata.getAnnotationAttributes(containerClassName, false); + if (container != null && container.containsKey("value")) { + for (Map containedAttributes : (Map[]) container.get("value")) { + addAttributesIfNotNull(result, containedAttributes); + } + } + return Collections.unmodifiableSet(result); + } + + private static void addAttributesIfNotNull(Set result, + Map attributes) { + if (attributes != null) { + result.add(AnnotationAttributes.fromMap(attributes)); + } } } diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java index 9577d3d551b..59379bf24de 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java @@ -16,6 +16,7 @@ package org.springframework.context.annotation; +import java.io.FileNotFoundException; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; @@ -55,6 +56,7 @@ import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.core.env.CompositePropertySource; import org.springframework.core.env.Environment; import org.springframework.core.env.PropertySource; +import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; import org.springframework.core.io.support.ResourcePropertySource; import org.springframework.core.type.AnnotationMetadata; @@ -63,6 +65,8 @@ import org.springframework.core.type.StandardAnnotationMetadata; import org.springframework.core.type.classreading.MetadataReader; import org.springframework.core.type.classreading.MetadataReaderFactory; import org.springframework.core.type.filter.AssignableTypeFilter; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; import org.springframework.util.StringUtils; /** @@ -112,7 +116,7 @@ class ConfigurationClassParser { private final Map knownSuperclasses = new HashMap(); - private final Stack> propertySources = new Stack>(); + private final MultiValueMap> propertySources = new LinkedMultiValueMap>(); private final ImportStack importStack = new ImportStack(); @@ -218,9 +222,9 @@ class ConfigurationClassParser { processMemberClasses(configClass, sourceClass); // process any @PropertySource annotations - AnnotationAttributes propertySource = AnnotationConfigUtils.attributesFor(sourceClass.getMetadata(), - org.springframework.context.annotation.PropertySource.class); - if (propertySource != null) { + for (AnnotationAttributes propertySource : AnnotationConfigUtils.attributesForRepeatable( + sourceClass.getMetadata(), PropertySources.class, + org.springframework.context.annotation.PropertySource.class)) { processPropertySource(propertySource); } @@ -301,29 +305,29 @@ class ConfigurationClassParser { private void processPropertySource(AnnotationAttributes propertySource) throws IOException { String name = propertySource.getString("name"); String[] locations = propertySource.getStringArray("value"); + boolean ignoreResourceNotFound = propertySource.getBoolean("ignoreResourceNotFound"); int locationCount = locations.length; if (locationCount == 0) { throw new IllegalArgumentException("At least one @PropertySource(value) location is required"); } - for (int i = 0; i < locationCount; i++) { - locations[i] = this.environment.resolveRequiredPlaceholders(locations[i]); - } - ClassLoader classLoader = this.resourceLoader.getClassLoader(); - if (!StringUtils.hasText(name)) { - for (String location : locations) { - this.propertySources.push(new ResourcePropertySource(location, classLoader)); - } - } - else { - if (locationCount == 1) { - this.propertySources.push(new ResourcePropertySource(name, locations[0], classLoader)); + for (String location : locations) { + Resource resource = this.resourceLoader.getResource( + this.environment.resolveRequiredPlaceholders(location)); + try { + if (!StringUtils.hasText(name) || this.propertySources.containsKey(name)) { + // We need to ensure unique names when the property source will + // ultimately end up in a composite + ResourcePropertySource ps = new ResourcePropertySource(resource); + this.propertySources.add((StringUtils.hasText(name) ? name : ps.getName()), ps); + } + else { + this.propertySources.add(name, new ResourcePropertySource(name, resource)); + } } - else { - CompositePropertySource ps = new CompositePropertySource(name); - for (int i = locations.length - 1; i >= 0; i--) { - ps.addPropertySource(new ResourcePropertySource(locations[i], classLoader)); + catch (FileNotFoundException ex) { + if (!ignoreResourceNotFound) { + throw ex; } - this.propertySources.push(ps); } } } @@ -473,10 +477,27 @@ class ConfigurationClassParser { return this.configurationClasses; } - public Stack> getPropertySources() { - return this.propertySources; + public List> getPropertySources() { + List> propertySources = new LinkedList>(); + for (Map.Entry>> entry : this.propertySources.entrySet()) { + propertySources.add(0, collatePropertySources(entry.getKey(), entry.getValue())); + } + return propertySources; } + private PropertySource collatePropertySources(String name, + List> propertySources) { + if (propertySources.size() == 1) { + return propertySources.get(0); + } + CompositePropertySource result = new CompositePropertySource(name); + for (int i = propertySources.size() - 1; i >= 0; i--) { + result.addPropertySource(propertySources.get(i)); + } + return result; + } + + ImportRegistry getImportRegistry() { return this.importStack; } diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassPostProcessor.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassPostProcessor.java index 7e67b3ace27..d4b9eab6d65 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassPostProcessor.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassPostProcessor.java @@ -21,13 +21,12 @@ import java.io.IOException; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedHashSet; +import java.util.List; import java.util.Map; import java.util.Set; -import java.util.Stack; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; - import org.springframework.beans.BeansException; import org.springframework.beans.PropertyValues; import org.springframework.beans.factory.BeanClassLoaderAware; @@ -298,7 +297,7 @@ public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPo parser.validate(); // Handle any @PropertySource annotations - Stack> parsedPropertySources = parser.getPropertySources(); + List> parsedPropertySources = parser.getPropertySources(); if (!parsedPropertySources.isEmpty()) { if (!(this.environment instanceof ConfigurableEnvironment)) { logger.warn("Ignoring @PropertySource annotations. " + @@ -306,8 +305,8 @@ public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPo } else { MutablePropertySources envPropertySources = ((ConfigurableEnvironment)this.environment).getPropertySources(); - while (!parsedPropertySources.isEmpty()) { - envPropertySources.addLast(parsedPropertySources.pop()); + for (PropertySource propertySource : parsedPropertySources) { + envPropertySources.addLast(propertySource); } } } diff --git a/spring-context/src/main/java/org/springframework/context/annotation/PropertySource.java b/spring-context/src/main/java/org/springframework/context/annotation/PropertySource.java index e67f6aab26f..1aea9e9470a 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/PropertySource.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/PropertySource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2011 the original author or authors. + * Copyright 2002-2013 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. @@ -18,6 +18,7 @@ package org.springframework.context.annotation; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @@ -132,7 +133,9 @@ import java.lang.annotation.Target; * Javadoc for details. * * @author Chris Beams + * @author Phillip Webb * @since 3.1 + * @see PropertySources * @see Configuration * @see org.springframework.core.env.PropertySource * @see org.springframework.core.env.ConfigurableEnvironment#getPropertySources() @@ -141,6 +144,7 @@ import java.lang.annotation.Target; @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented +@Repeatable(PropertySources.class) public @interface PropertySource { /** @@ -166,4 +170,13 @@ public @interface PropertySource { */ String[] value(); + /** + * Indicate if failure to find the a {@link #value() property resource} should be + * ignored. + *

{@code true} is appropriate if the properties file is completely optional. + * Default is {@code false}. + * @since 4.0 + */ + boolean ignoreResourceNotFound() default false; + } diff --git a/spring-context/src/main/java/org/springframework/context/annotation/PropertySources.java b/spring-context/src/main/java/org/springframework/context/annotation/PropertySources.java new file mode 100644 index 00000000000..12484b883ab --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/annotation/PropertySources.java @@ -0,0 +1,44 @@ +/* + * Copyright 2002-2013 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.context.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Container annotation that aggregates several {@link PropertySource} annotations. + * + *

Can be used natively, declaring several nested {@link PropertySource} annotations. + * Can also be used in conjunction with Java 8's support for repeatable annotations, + * where {@link PropertySource} can simply be declared several times on the same method, + * implicitly generating this container annotation. + * + * @author Phillip Webb + * @since 4.0 + * @see PropertySource + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface PropertySources { + + PropertySource[] value(); + +} diff --git a/spring-context/src/main/java/org/springframework/scheduling/annotation/Scheduled.java b/spring-context/src/main/java/org/springframework/scheduling/annotation/Scheduled.java index 76eeaafa58f..51f2117b32b 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/annotation/Scheduled.java +++ b/spring-context/src/main/java/org/springframework/scheduling/annotation/Scheduled.java @@ -42,6 +42,7 @@ import java.lang.annotation.Target; * @since 3.0 * @see EnableScheduling * @see ScheduledAnnotationBeanPostProcessor + * @see Schedules */ @Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) @Retention(RetentionPolicy.RUNTIME) diff --git a/spring-context/src/main/java/org/springframework/scheduling/annotation/ScheduledAnnotationBeanPostProcessor.java b/spring-context/src/main/java/org/springframework/scheduling/annotation/ScheduledAnnotationBeanPostProcessor.java index 26d8150b79e..bcde17f44f6 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/annotation/ScheduledAnnotationBeanPostProcessor.java +++ b/spring-context/src/main/java/org/springframework/scheduling/annotation/ScheduledAnnotationBeanPostProcessor.java @@ -117,17 +117,8 @@ public class ScheduledAnnotationBeanPostProcessor ReflectionUtils.doWithMethods(targetClass, new MethodCallback() { @Override public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException { - Schedules schedules = AnnotationUtils.getAnnotation(method, Schedules.class); - if (schedules != null) { - for (Scheduled scheduled : schedules.value()) { - processScheduled(scheduled, method, bean); - } - } - else { - Scheduled scheduled = AnnotationUtils.getAnnotation(method, Scheduled.class); - if (scheduled != null) { - processScheduled(scheduled, method, bean); - } + for (Scheduled scheduled : AnnotationUtils.getRepeatableAnnotation(method, Schedules.class, Scheduled.class)) { + processScheduled(scheduled, method, bean); } } }); diff --git a/spring-context/src/test/java/org/springframework/context/annotation/PropertySourceAnnotationTests.java b/spring-context/src/test/java/org/springframework/context/annotation/PropertySourceAnnotationTests.java index 45673cfd654..ec5bca2aff3 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/PropertySourceAnnotationTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/PropertySourceAnnotationTests.java @@ -16,29 +16,35 @@ package org.springframework.context.annotation; -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.is; -import static org.junit.Assert.assertThat; -import static org.junit.Assert.assertTrue; - +import java.io.FileNotFoundException; import java.util.Iterator; import javax.inject.Inject; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.springframework.beans.factory.BeanDefinitionStoreException; import org.springframework.core.env.Environment; import org.springframework.core.env.MutablePropertySources; - import org.springframework.tests.sample.beans.TestBean; +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; + /** * Tests the processing of @PropertySource annotations on @Configuration classes. * * @author Chris Beams + * @author Phillip Webb * @since 3.1 */ public class PropertySourceAnnotationTests { + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test public void withExplicitName() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); @@ -149,6 +155,29 @@ public class PropertySourceAnnotationTests { ctx.refresh(); } + @Test + public void withPropertySources() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ConfigWithPropertySources.class); + assertThat(ctx.getEnvironment().containsProperty("from.p1"), is(true)); + assertThat(ctx.getEnvironment().containsProperty("from.p2"), is(true)); + // p2 should 'win' as it was registered last + assertThat(ctx.getEnvironment().getProperty("testbean.name"), equalTo("p2TestBean")); + } + + @Test + public void withMissingPropertySource() { + thrown.expect(BeanDefinitionStoreException.class); + thrown.expectCause(isA(FileNotFoundException.class)); + new AnnotationConfigApplicationContext(ConfigWithMissingPropertySource.class); + } + + @Test + public void withIgnoredPropertySource() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ConfigWithIgnoredPropertySource.class); + assertThat(ctx.getEnvironment().containsProperty("from.p1"), is(true)); + assertThat(ctx.getEnvironment().containsProperty("from.p2"), is(true)); + } + @Configuration @PropertySource(value="classpath:${unresolvable}/p1.properties") @@ -231,6 +260,33 @@ public class PropertySourceAnnotationTests { } + @Configuration + @PropertySources({ + @PropertySource(name = "psName", value="classpath:org/springframework/context/annotation/p1.properties"), + @PropertySource(name = "psName", value="classpath:org/springframework/context/annotation/p2.properties") + }) + static class ConfigWithPropertySources { + } + + @Configuration + @PropertySources({ + @PropertySource(name = "psName", value="classpath:org/springframework/context/annotation/p1.properties"), + @PropertySource(name = "psName", value="classpath:org/springframework/context/annotation/missing.properties"), + @PropertySource(name = "psName", value="classpath:org/springframework/context/annotation/p2.properties") + }) + static class ConfigWithMissingPropertySource { + } + + + @Configuration + @PropertySources({ + @PropertySource(name = "psName", value="classpath:org/springframework/context/annotation/p1.properties"), + @PropertySource(name = "psName", value="classpath:org/springframework/context/annotation/missing.properties", ignoreResourceNotFound=true), + @PropertySource(name = "psName", value="classpath:org/springframework/context/annotation/p2.properties") + }) + static class ConfigWithIgnoredPropertySource { + } + @Configuration @PropertySource(value = {}) static class ConfigWithEmptyResourceLocations { diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationUtils.java b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationUtils.java index fbc00e2d8b4..56350dd70d0 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationUtils.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationUtils.java @@ -19,12 +19,18 @@ package org.springframework.core.annotation; import java.lang.annotation.Annotation; import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.WeakHashMap; import org.springframework.core.BridgeMethodResolver; import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; /** * General utility methods for working with annotations, handling bridge methods (which the compiler @@ -43,6 +49,7 @@ import org.springframework.util.Assert; * @author Sam Brannen * @author Mark Fisher * @author Chris Beams + * @author Phillip Webb * @since 2.0 * @see java.lang.reflect.Method#getAnnotations() * @see java.lang.reflect.Method#getAnnotation(Class) @@ -117,6 +124,45 @@ public abstract class AnnotationUtils { return getAnnotation((AnnotatedElement) resolvedMethod, annotationType); } + /** + * Get the possibly repeating {@link Annotation}s of {@code annotationType} from the + * supplied {@link Method}. Deals with both a single direct annotation and repeating + * annotations nested within a containing annotation. + *

Correctly handles bridge {@link Method Methods} generated by the compiler. + * @param method the method to look for annotations on + * @param containerAnnotationType the class of the container that holds the annotations + * @param annotationType the annotation class to look for + * @return the annotations found + * @see org.springframework.core.BridgeMethodResolver#findBridgedMethod(Method) + * @since 4.0 + */ + public static Set getRepeatableAnnotation(Method method, + Class containerAnnotationType, Class annotationType) { + Method resolvedMethod = BridgeMethodResolver.findBridgedMethod(method); + return getRepeatableAnnotation((AnnotatedElement) resolvedMethod, + containerAnnotationType, annotationType); + } + + /** + * Get the possibly repeating {@link Annotation}s of {@code annotationType} from the + * supplied {@link AnnotatedElement}. Deals with both a single direct annotation and + * repeating annotations nested within a containing annotation. + *

Correctly handles bridge {@link Method Methods} generated by the compiler. + * @param annotatedElement the element to look for annotations on + * @param containerAnnotationType the class of the container that holds the annotations + * @param annotationType the annotation class to look for + * @return the annotations found + * @see org.springframework.core.BridgeMethodResolver#findBridgedMethod(Method) + * @since 4.0 + */ + public static Set getRepeatableAnnotation(AnnotatedElement annotatedElement, + Class containerAnnotationType, Class annotationType) { + if (annotatedElement.getAnnotations().length == 0) { + return Collections.emptySet(); + } + return new AnnotationCollector(containerAnnotationType, annotationType).getResult(annotatedElement); + } + /** * Get a single {@link Annotation} of {@code annotationType} from the supplied {@link Method}, * traversing its super methods if no annotation can be found on the given method itself. @@ -521,4 +567,59 @@ public abstract class AnnotationUtils { } } + + private static class AnnotationCollector { + + + private final Class containerAnnotationType; + + private final Class annotationType; + + private final Set visited = new HashSet(); + + private final Set result = new LinkedHashSet(); + + + public AnnotationCollector(Class containerAnnotationType, + Class annotationType) { + this.containerAnnotationType = containerAnnotationType; + this.annotationType = annotationType; + } + + + public Set getResult(AnnotatedElement element) { + process(element); + return Collections.unmodifiableSet(this.result); + } + + @SuppressWarnings("unchecked") + private void process(AnnotatedElement annotatedElement) { + if (this.visited.add(annotatedElement)) { + for (Annotation annotation : annotatedElement.getAnnotations()) { + if (ObjectUtils.nullSafeEquals(this.annotationType, annotation.annotationType())) { + this.result.add((A) annotation); + } + else if (ObjectUtils.nullSafeEquals(this.containerAnnotationType, annotation.annotationType())) { + result.addAll(Arrays.asList(getValue(annotation))); + } + else { + process(annotation.annotationType()); + } + } + } + } + + @SuppressWarnings("unchecked") + private A[] getValue(Annotation annotation) { + try { + Method method = annotation.annotationType().getDeclaredMethod("value"); + return (A[]) method.invoke(annotation); + } + catch (Exception ex) { + throw new IllegalStateException("Unable to read value from repeating annotation container " + + this.containerAnnotationType.getName(), ex); + } + } + + } } diff --git a/spring-core/src/test/java/org/springframework/core/annotation/AnnotationUtilsTests.java b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationUtilsTests.java index c718971966d..1163704d861 100644 --- a/spring-core/src/test/java/org/springframework/core/annotation/AnnotationUtilsTests.java +++ b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationUtilsTests.java @@ -18,17 +18,21 @@ package org.springframework.core.annotation; import java.lang.annotation.Annotation; import java.lang.annotation.Inherited; +import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.reflect.Method; import java.util.Arrays; +import java.util.HashSet; import java.util.List; +import java.util.Set; import org.junit.Test; - import org.springframework.core.Ordered; import org.springframework.stereotype.Component; +import static org.hamcrest.Matchers.*; + import static org.junit.Assert.*; import static org.springframework.core.annotation.AnnotationUtils.*; @@ -37,6 +41,7 @@ import static org.springframework.core.annotation.AnnotationUtils.*; * @author Juergen Hoeller * @author Sam Brannen * @author Chris Beams + * @author Phillip Webb */ public class AnnotationUtilsTests { @@ -268,6 +273,19 @@ public class AnnotationUtilsTests { assertNotNull(order); } + @Test + public void testGetRepeatableFromMethod() throws Exception { + Method method = InterfaceWithRepeated.class.getMethod("foo"); + Set annotions = AnnotationUtils.getRepeatableAnnotation(method, + MyRepeatableContainer.class, MyRepeatable.class); + Set values = new HashSet(); + for (MyRepeatable myRepeatable : annotions) { + values.add(myRepeatable.value()); + } + assertThat(values, equalTo((Set) new HashSet( + Arrays.asList("a", "b", "c", "meta")))); + } + @Component(value = "meta1") @Retention(RetentionPolicy.RUNTIME) @@ -428,10 +446,46 @@ public class AnnotationUtilsTests { } } + public static interface InterfaceWithRepeated { + + @MyRepeatable("a") + @MyRepeatableContainer({ @MyRepeatable("b"), @MyRepeatable("c") }) + @MyRepeatableMeta + void foo(); + + } + } + @Retention(RetentionPolicy.RUNTIME) @Inherited @interface Transactional { } + + +@Retention(RetentionPolicy.RUNTIME) +@Inherited +@interface MyRepeatableContainer { + + MyRepeatable[] value(); + +} + + +@Retention(RetentionPolicy.RUNTIME) +@Inherited +@Repeatable(MyRepeatableContainer.class) +@interface MyRepeatable { + + String value(); + +} + + +@Retention(RetentionPolicy.RUNTIME) +@Inherited +@MyRepeatable("meta") +@interface MyRepeatableMeta { +}