From ca66e076d159c0f0d100781e4679d9d723e8cd5d Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Thu, 14 May 2015 23:32:30 +0200 Subject: [PATCH] Support annotation attribute aliases and overrides via @AliasFor This commit introduces first-class support for aliases for annotation attributes. Specifically, this commit introduces a new @AliasFor annotation that can be used to declare a pair of aliased attributes within a single annotation or an alias from an attribute in a custom composed annotation to an attribute in a meta-annotation. To support @AliasFor within annotation instances, AnnotationUtils has been overhauled to "synthesize" any annotations returned by "get" and "find" searches. A SynthesizedAnnotation is an annotation that is wrapped in a JDK dynamic proxy which provides run-time support for @AliasFor semantics. SynthesizedAnnotationInvocationHandler is the actual handler behind the proxy. In addition, the contract for @AliasFor is fully validated, and an AnnotationConfigurationException is thrown in case invalid configuration is detected. For example, @ContextConfiguration from the spring-test module is now declared as follows: public @interface ContextConfiguration { @AliasFor(attribute = "locations") String[] value() default {}; @AliasFor(attribute = "value") String[] locations() default {}; // ... } The following annotations and their related support classes have been modified to use @AliasFor. - @ManagedResource - @ContextConfiguration - @ActiveProfiles - @TestExecutionListeners - @TestPropertySource - @Sql - @ControllerAdvice - @RequestMapping Similarly, support for AnnotationAttributes has been reworked to support @AliasFor as well. This allows for fine-grained control over exactly which attributes are overridden within an annotation hierarchy. In fact, it is now possible to declare an alias for the 'value' attribute of a meta-annotation. For example, given the revised declaration of @ContextConfiguration above, one can now develop a composed annotation with a custom attribute override as follows. @ContextConfiguration public @interface MyTestConfig { @AliasFor( annotation = ContextConfiguration.class, attribute = "locations" ) String[] xmlFiles(); // ... } Consequently, the following are functionally equivalent. - @MyTestConfig(xmlFiles = "test.xml") - @ContextConfiguration("test.xml") - @ContextConfiguration(locations = "test.xml"). Issue: SPR-11512, SPR-11513 --- .../AnnotationJmxAttributeSource.java | 11 +- .../export/annotation/ManagedResource.java | 10 +- .../core/annotation/AliasFor.java | 41 ++ .../annotation/AnnotatedElementUtils.java | 129 ++-- .../core/annotation/AnnotationAttributes.java | 60 +- .../AnnotationConfigurationException.java | 47 ++ .../core/annotation/AnnotationUtils.java | 578 ++++++++++++++++-- .../annotation/SynthesizedAnnotation.java | 28 + ...ynthesizedAnnotationInvocationHandler.java | 118 ++++ .../core/annotation/package-info.java | 3 +- .../AnnotatedElementUtilsTests.java | 226 ++++++- .../core/annotation/AnnotationUtilsTests.java | 413 ++++++++++++- .../test/context/ActiveProfiles.java | 6 +- .../test/context/ContextConfiguration.java | 3 + .../ContextConfigurationAttributes.java | 44 +- .../test/context/TestExecutionListeners.java | 6 +- .../test/context/TestPropertySource.java | 6 +- .../test/context/jdbc/Sql.java | 6 +- .../jdbc/SqlScriptsTestExecutionListener.java | 20 +- .../AbstractTestContextBootstrapper.java | 12 - .../context/support/ActiveProfilesUtils.java | 20 +- .../DefaultActiveProfilesResolver.java | 21 +- .../support/TestPropertySourceAttributes.java | 30 +- .../context/TestExecutionListenersTests.java | 5 +- .../SqlScriptsTestExecutionListenerTests.java | 17 +- .../support/ActiveProfilesUtilsTests.java | 3 +- ...aderUtilsConfigurationAttributesTests.java | 21 +- .../support/TestPropertySourceUtilsTests.java | 3 +- .../web/bind/annotation/ControllerAdvice.java | 18 +- .../web/bind/annotation/RequestMapping.java | 6 +- .../web/method/ControllerAdviceBean.java | 7 +- 31 files changed, 1582 insertions(+), 336 deletions(-) create mode 100644 spring-core/src/main/java/org/springframework/core/annotation/AliasFor.java create mode 100644 spring-core/src/main/java/org/springframework/core/annotation/AnnotationConfigurationException.java create mode 100644 spring-core/src/main/java/org/springframework/core/annotation/SynthesizedAnnotation.java create mode 100644 spring-core/src/main/java/org/springframework/core/annotation/SynthesizedAnnotationInvocationHandler.java diff --git a/spring-context/src/main/java/org/springframework/jmx/export/annotation/AnnotationJmxAttributeSource.java b/spring-context/src/main/java/org/springframework/jmx/export/annotation/AnnotationJmxAttributeSource.java index 64c2a80811..4fbe0e765f 100644 --- a/spring-context/src/main/java/org/springframework/jmx/export/annotation/AnnotationJmxAttributeSource.java +++ b/spring-context/src/main/java/org/springframework/jmx/export/annotation/AnnotationJmxAttributeSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 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. @@ -32,7 +32,6 @@ import org.springframework.jmx.export.metadata.ManagedNotification; import org.springframework.jmx.export.metadata.ManagedOperation; import org.springframework.jmx.export.metadata.ManagedOperationParameter; import org.springframework.jmx.export.metadata.ManagedResource; -import org.springframework.util.StringUtils; import org.springframework.util.StringValueResolver; /** @@ -66,7 +65,6 @@ public class AnnotationJmxAttributeSource implements JmxAttributeSource, BeanFac } } - @Override public ManagedResource getManagedResource(Class beanClass) throws InvalidMetadataException { org.springframework.jmx.export.annotation.ManagedResource ann = @@ -76,13 +74,6 @@ public class AnnotationJmxAttributeSource implements JmxAttributeSource, BeanFac } ManagedResource managedResource = new ManagedResource(); AnnotationBeanUtils.copyPropertiesToBean(ann, managedResource, this.embeddedValueResolver); - if (!"".equals(ann.value()) && !StringUtils.hasLength(managedResource.getObjectName())) { - String value = ann.value(); - if (this.embeddedValueResolver != null) { - value = this.embeddedValueResolver.resolveStringValue(value); - } - managedResource.setObjectName(value); - } return managedResource; } diff --git a/spring-context/src/main/java/org/springframework/jmx/export/annotation/ManagedResource.java b/spring-context/src/main/java/org/springframework/jmx/export/annotation/ManagedResource.java index d5d1f3a774..0dd1fb7ad7 100644 --- a/spring-context/src/main/java/org/springframework/jmx/export/annotation/ManagedResource.java +++ b/spring-context/src/main/java/org/springframework/jmx/export/annotation/ManagedResource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2015 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. @@ -23,6 +23,8 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.springframework.core.annotation.AliasFor; + /** * JDK 1.5+ class-level annotation that indicates to register instances of a * class with a JMX server, corresponding to the ManagedResource attribute. @@ -34,6 +36,7 @@ import java.lang.annotation.Target; * * @author Rob Harrop * @author Juergen Hoeller + * @author Sam Brannen * @since 1.2 * @see org.springframework.jmx.export.metadata.ManagedResource */ @@ -44,11 +47,12 @@ import java.lang.annotation.Target; public @interface ManagedResource { /** - * The annotation value is equivalent to the {@code objectName} - * attribute, for simple default usage. + * Alias for the {@link #objectName} attribute, for simple default usage. */ + @AliasFor(attribute = "objectName") String value() default ""; + @AliasFor(attribute = "value") String objectName() default ""; String description() default ""; diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AliasFor.java b/spring-core/src/main/java/org/springframework/core/annotation/AliasFor.java new file mode 100644 index 0000000000..337db773d1 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/annotation/AliasFor.java @@ -0,0 +1,41 @@ +/* + * Copyright 2002-2015 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.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; + +/** + * TODO Document @AliasFor. + * + * @author Sam Brannen + * @since 4.2 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +@Documented +public @interface AliasFor { + + String attribute(); + + Class annotation() default Annotation.class; + +} diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AnnotatedElementUtils.java b/spring-core/src/main/java/org/springframework/core/annotation/AnnotatedElementUtils.java index da93678e2e..21330cf2a3 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/AnnotatedElementUtils.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/AnnotatedElementUtils.java @@ -31,6 +31,8 @@ import org.springframework.core.BridgeMethodResolver; import org.springframework.util.Assert; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.StringUtils; /** * General utility methods for finding annotations and meta-annotations on @@ -145,7 +147,7 @@ public class AnnotatedElementUtils { searchWithGetSemantics(annotation.annotationType(), annotationType, new SimpleAnnotationProcessor() { @Override - public Object process(Annotation annotation, int metaDepth) { + public Object process(AnnotatedElement annotatedElement, Annotation annotation, int metaDepth) { types.add(annotation.annotationType().getName()); return CONTINUE; } @@ -153,6 +155,7 @@ public class AnnotatedElementUtils { } } catch (Throwable ex) { + AnnotationUtils.rethrowAnnotationConfigurationException(ex); throw new IllegalStateException("Failed to introspect annotations on " + element, ex); } @@ -179,7 +182,7 @@ public class AnnotatedElementUtils { return Boolean.TRUE.equals(searchWithGetSemantics(element, annotationType, new SimpleAnnotationProcessor() { @Override - public Boolean process(Annotation annotation, int metaDepth) { + public Boolean process(AnnotatedElement annotatedElement, Annotation annotation, int metaDepth) { boolean found = annotation.annotationType().getName().equals(annotationType); return ((found && (metaDepth > 0)) ? Boolean.TRUE : CONTINUE); } @@ -208,7 +211,7 @@ public class AnnotatedElementUtils { return Boolean.TRUE.equals(searchWithGetSemantics(element, annotationType, new SimpleAnnotationProcessor() { @Override - public Boolean process(Annotation annotation, int metaDepth) { + public Boolean process(AnnotatedElement annotatedElement, Annotation annotation, int metaDepth) { boolean found = annotation.annotationType().getName().equals(annotationType); return (found ? Boolean.TRUE : CONTINUE); } @@ -273,8 +276,12 @@ public class AnnotatedElementUtils { */ public static AnnotationAttributes getAnnotationAttributes(AnnotatedElement element, String annotationType, boolean classValuesAsString, boolean nestedAnnotationsAsMap) { - return searchWithGetSemantics(element, annotationType, new MergedAnnotationAttributesProcessor(annotationType, - classValuesAsString, nestedAnnotationsAsMap)); + + AnnotationAttributes attributes = searchWithGetSemantics(element, annotationType, + new MergedAnnotationAttributesProcessor(annotationType, classValuesAsString, nestedAnnotationsAsMap)); + AnnotationUtils.postProcessAnnotationAttributes(element, attributes, classValuesAsString, + nestedAnnotationsAsMap); + return attributes; } /** @@ -297,7 +304,7 @@ public class AnnotatedElementUtils { public static AnnotationAttributes findAnnotationAttributes(AnnotatedElement element, Class annotationType) { Assert.notNull(annotationType, "annotationType must not be null"); - return findAnnotationAttributes(element, annotationType.getName(), false, false); + return findAnnotationAttributes(element, annotationType.getName()); } /** @@ -357,8 +364,12 @@ public class AnnotatedElementUtils { */ public static AnnotationAttributes findAnnotationAttributes(AnnotatedElement element, String annotationType, boolean classValuesAsString, boolean nestedAnnotationsAsMap) { - return searchWithFindSemantics(element, annotationType, new MergedAnnotationAttributesProcessor(annotationType, - classValuesAsString, nestedAnnotationsAsMap)); + + AnnotationAttributes attributes = searchWithFindSemantics(element, annotationType, + new MergedAnnotationAttributesProcessor(annotationType, classValuesAsString, nestedAnnotationsAsMap)); + AnnotationUtils.postProcessAnnotationAttributes(element, attributes, classValuesAsString, + nestedAnnotationsAsMap); + return attributes; } /** @@ -417,7 +428,7 @@ public class AnnotatedElementUtils { searchWithGetSemantics(element, annotationType, new SimpleAnnotationProcessor() { @Override - public Void process(Annotation annotation, int metaDepth) { + public Void process(AnnotatedElement annotatedElement, Annotation annotation, int metaDepth) { boolean found = annotation.annotationType().getName().equals(annotationType); if (found) { AnnotationAttributes annotationAttributes = AnnotationUtils.getAnnotationAttributes(annotation, @@ -450,6 +461,7 @@ public class AnnotatedElementUtils { return searchWithGetSemantics(element, annotationType, processor, new HashSet(), 0); } catch (Throwable ex) { + AnnotationUtils.rethrowAnnotationConfigurationException(ex); throw new IllegalStateException("Failed to introspect annotations on " + element, ex); } } @@ -482,8 +494,8 @@ public class AnnotatedElementUtils { // Start searching within locally declared annotations List declaredAnnotations = Arrays.asList(element.getDeclaredAnnotations()); - T result = searchWithGetSemanticsInAnnotations(declaredAnnotations, annotationType, processor, visited, - metaDepth); + T result = searchWithGetSemanticsInAnnotations(element, declaredAnnotations, annotationType, processor, + visited, metaDepth); if (result != null) { return result; } @@ -496,16 +508,17 @@ public class AnnotatedElementUtils { } // Continue searching within inherited annotations - result = searchWithGetSemanticsInAnnotations(inheritedAnnotations, annotationType, processor, visited, - metaDepth); + result = searchWithGetSemanticsInAnnotations(element, inheritedAnnotations, annotationType, processor, + visited, metaDepth); if (result != null) { return result; } } catch (Exception ex) { - AnnotationUtils.logIntrospectionFailure(element, ex); + AnnotationUtils.handleIntrospectionFailure(element, ex); } } + return null; } @@ -521,6 +534,8 @@ public class AnnotatedElementUtils { * {@link Processor#process process()} method of the {@link Processor} * API. * + * @param annotatedElement the element that is annotated with the supplied + * annotations, used for contextual logging; may be {@code null} if unknown * @param annotations the annotations to search in; never {@code null} * @param annotationType the fully qualified class name of the annotation * type to find; never {@code null} or empty @@ -529,14 +544,15 @@ public class AnnotatedElementUtils { * @param metaDepth the meta-depth of the annotation * @return the result of the processor, potentially {@code null} */ - private static T searchWithGetSemanticsInAnnotations(List annotations, String annotationType, - Processor processor, Set visited, int metaDepth) { + private static T searchWithGetSemanticsInAnnotations(AnnotatedElement annotatedElement, + List annotations, String annotationType, Processor processor, Set visited, + int metaDepth) { // Search in annotations for (Annotation annotation : annotations) { if (!AnnotationUtils.isInJavaLangAnnotationPackage(annotation) && (annotation.annotationType().getName().equals(annotationType) || metaDepth > 0)) { - T result = processor.process(annotation, metaDepth); + T result = processor.process(annotatedElement, annotation, metaDepth); if (result != null) { return result; } @@ -549,7 +565,7 @@ public class AnnotatedElementUtils { T result = searchWithGetSemantics(annotation.annotationType(), annotationType, processor, visited, metaDepth + 1); if (result != null) { - processor.postProcess(annotation, result); + processor.postProcess(annotatedElement, annotation, result); return result; } } @@ -599,6 +615,7 @@ public class AnnotatedElementUtils { searchOnMethodsInInterfaces, searchOnMethodsInSuperclasses, processor, new HashSet(), 0); } catch (Throwable ex) { + AnnotationUtils.rethrowAnnotationConfigurationException(ex); throw new IllegalStateException("Failed to introspect annotations on " + element, ex); } } @@ -646,7 +663,7 @@ public class AnnotatedElementUtils { for (Annotation annotation : annotations) { if (!AnnotationUtils.isInJavaLangAnnotationPackage(annotation) && (annotation.annotationType().getName().equals(annotationType) || metaDepth > 0)) { - T result = processor.process(annotation, metaDepth); + T result = processor.process(element, annotation, metaDepth); if (result != null) { return result; } @@ -660,7 +677,7 @@ public class AnnotatedElementUtils { searchOnInterfaces, searchOnSuperclasses, searchOnMethodsInInterfaces, searchOnMethodsInSuperclasses, processor, visited, metaDepth + 1); if (result != null) { - processor.postProcess(annotation, result); + processor.postProcess(annotation.annotationType(), annotation, result); return result; } } @@ -756,7 +773,7 @@ public class AnnotatedElementUtils { } } catch (Exception ex) { - AnnotationUtils.logIntrospectionFailure(element, ex); + AnnotationUtils.handleIntrospectionFailure(element, ex); } } return null; @@ -840,12 +857,15 @@ public class AnnotatedElementUtils { * of 0; a meta-annotation will have a depth of 1; and a * meta-meta-annotation will have a depth of 2; etc. * + * @param annotatedElement the element that is annotated with the + * supplied annotation, used for contextual logging; may be + * {@code null} if unknown * @param annotation the annotation to process * @param metaDepth the meta-depth of the annotation * @return the result of the processing, or {@code null} to continue * searching for additional annotations */ - T process(Annotation annotation, int metaDepth); + T process(AnnotatedElement annotatedElement, Annotation annotation, int metaDepth); /** * Post-process the result returned by the {@link #process} method. @@ -855,10 +875,13 @@ public class AnnotatedElementUtils { * {@link AnnotatedElement} and an invocation of {@link #process} * that returned a non-null value. * + * @param annotatedElement the element that is annotated with the + * supplied annotation, used for contextual logging; may be + * {@code null} if unknown * @param annotation the annotation to post-process * @param result the result to post-process */ - void postProcess(Annotation annotation, T result); + void postProcess(AnnotatedElement annotatedElement, Annotation annotation, T result); } /** @@ -872,7 +895,7 @@ public class AnnotatedElementUtils { * No-op. */ @Override - public final void postProcess(Annotation annotation, T result) { + public final void postProcess(AnnotatedElement annotatedElement, Annotation annotation, T result) { /* no-op */ } } @@ -887,30 +910,64 @@ public class AnnotatedElementUtils { */ private static class MergedAnnotationAttributesProcessor implements Processor { - private final String annotationType; + private final String annotationTypeName; private final boolean classValuesAsString; private final boolean nestedAnnotationsAsMap; - MergedAnnotationAttributesProcessor(String annotationType, boolean classValuesAsString, boolean nestedAnnotationsAsMap) { - this.annotationType = annotationType; + MergedAnnotationAttributesProcessor(String annotationType, boolean classValuesAsString, + boolean nestedAnnotationsAsMap) { + this.annotationTypeName = annotationType; this.classValuesAsString = classValuesAsString; this.nestedAnnotationsAsMap = nestedAnnotationsAsMap; } @Override - public AnnotationAttributes process(Annotation annotation, int metaDepth) { - boolean found = annotation.annotationType().getName().equals(annotationType); - return (found ? AnnotationUtils.getAnnotationAttributes(annotation, classValuesAsString, nestedAnnotationsAsMap) : null); + public AnnotationAttributes process(AnnotatedElement annotatedElement, Annotation annotation, int metaDepth) { + boolean found = annotation.annotationType().getName().equals(this.annotationTypeName); + return (found ? AnnotationUtils.getAnnotationAttributes(annotatedElement, annotation, + this.classValuesAsString, this.nestedAnnotationsAsMap, true, false) : null); } @Override - public void postProcess(Annotation annotation, AnnotationAttributes attributes) { - for (String key : attributes.keySet()) { - if (!AnnotationUtils.VALUE.equals(key)) { - Object value = AnnotationUtils.getValue(annotation, key); - if (value != null) { - attributes.put(key, AnnotationUtils.adaptValue(value, classValuesAsString, nestedAnnotationsAsMap)); + public void postProcess(AnnotatedElement element, Annotation annotation, AnnotationAttributes attributes) { + annotation = AnnotationUtils.synthesizeAnnotation(annotation); + Class targetAnnotationType = attributes.annotationType(); + + for (Method attributeMethod : AnnotationUtils.getAttributeMethods(annotation.annotationType())) { + String attributeName = attributeMethod.getName(); + String aliasedAttributeName = AnnotationUtils.getAliasedAttributeName(attributeMethod, + targetAnnotationType); + + // Explicit annotation attribute override declared via @AliasFor + if (StringUtils.hasText(aliasedAttributeName)) { + if (attributes.containsKey(aliasedAttributeName)) { + Object value = AnnotationUtils.getValue(annotation, attributeName); + attributes.put(aliasedAttributeName, AnnotationUtils.adaptValue(element, value, + this.classValuesAsString, this.nestedAnnotationsAsMap)); + } + } + // Implicit annotation attribute override based on convention + else if (!AnnotationUtils.VALUE.equals(attributeName) && attributes.containsKey(attributeName)) { + Object value = AnnotationUtils.getValue(annotation, attributeName); + Object adaptedValue = AnnotationUtils.adaptValue(element, value, this.classValuesAsString, + this.nestedAnnotationsAsMap); + attributes.put(attributeName, adaptedValue); + + // If an aliased attribute defined by @AliasFor semantics does not + // already have an explicit value, ensure that the aliased attribute + // is also present in the map with a value identical to its mirror + // alias. + Method attributeMethodInTarget = ReflectionUtils.findMethod(targetAnnotationType, attributeName); + if (attributeMethodInTarget != null) { + String aliasedAttributeNameInTarget = AnnotationUtils.getAliasedAttributeName( + attributeMethodInTarget, null); + if (aliasedAttributeNameInTarget != null) { + Object aliasedValueInTarget = attributes.get(aliasedAttributeNameInTarget); + if (aliasedValueInTarget == null) { + attributes.put(aliasedAttributeNameInTarget, adaptedValue); + } + } } } } diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationAttributes.java b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationAttributes.java index 8a76e7b16f..8713c2967b 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationAttributes.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationAttributes.java @@ -16,6 +16,7 @@ package org.springframework.core.annotation; +import java.lang.annotation.Annotation; import java.lang.reflect.Array; import java.util.Iterator; import java.util.LinkedHashMap; @@ -25,21 +26,41 @@ import org.springframework.util.Assert; import org.springframework.util.StringUtils; /** - * {@link LinkedHashMap} subclass representing annotation attribute key/value pairs - * as read by Spring's reflection- or ASM-based {@link org.springframework.core.type.AnnotationMetadata} - * implementations. Provides 'pseudo-reification' to avoid noisy Map generics in the calling code - * as well as convenience methods for looking up annotation attributes in a type-safe fashion. + * {@link LinkedHashMap} subclass representing annotation attribute key/value + * pairs as read by Spring's reflection- or ASM-based + * {@link org.springframework.core.type.AnnotationMetadata} implementations, + * {@link AnnotationUtils}, and {@link AnnotatedElementUtils}. + * + *

Provides 'pseudo-reification' to avoid noisy Map generics in the calling + * code as well as convenience methods for looking up annotation attributes + * in a type-safe fashion. * * @author Chris Beams + * @author Sam Brannen * @since 3.1.1 */ @SuppressWarnings("serial") public class AnnotationAttributes extends LinkedHashMap { + private final Class annotationType; + + /** * Create a new, empty {@link AnnotationAttributes} instance. */ public AnnotationAttributes() { + this.annotationType = null; + } + + /** + * Create a new, empty {@link AnnotationAttributes} instance for the + * specified {@code annotationType}. + * @param annotationType the type of annotation represented by this + * {@code AnnotationAttributes} instance + * @since 4.2 + */ + public AnnotationAttributes(Class annotationType) { + this.annotationType = annotationType; } /** @@ -49,6 +70,7 @@ public class AnnotationAttributes extends LinkedHashMap { */ public AnnotationAttributes(int initialCapacity) { super(initialCapacity); + this.annotationType = null; } /** @@ -59,8 +81,18 @@ public class AnnotationAttributes extends LinkedHashMap { */ public AnnotationAttributes(Map map) { super(map); + this.annotationType = null; } + /** + * Get the type of annotation represented by this {@code AnnotationAttributes} + * instance. + * @return the annotation type, or {@code null} if unknown + * @since 4.2 + */ + public Class annotationType() { + return this.annotationType; + } public String getString(String attributeName) { return doGet(attributeName, String.class); @@ -106,7 +138,9 @@ public class AnnotationAttributes extends LinkedHashMap { Assert.hasText(attributeName, "attributeName must not be null or empty"); Object value = get(attributeName); if (value == null) { - throw new IllegalArgumentException(String.format("Attribute '%s' not found", attributeName)); + throw new IllegalArgumentException(String.format( + "Attribute '%s' not found in attributes for annotation [%s]", + attributeName, (annotationType() != null ? annotationType.getName() : "unknown"))); } if (!expectedType.isInstance(value)) { if (expectedType.isArray() && expectedType.getComponentType().isInstance(value)) { @@ -115,9 +149,10 @@ public class AnnotationAttributes extends LinkedHashMap { value = arrayValue; } else { - throw new IllegalArgumentException( - String.format("Attribute '%s' is of type [%s], but [%s] was expected.", - attributeName, value.getClass().getSimpleName(), expectedType.getSimpleName())); + throw new IllegalArgumentException(String.format( + "Attribute '%s' is of type [%s], but [%s] was expected in attributes for annotation [%s]", + attributeName, value.getClass().getSimpleName(), expectedType.getSimpleName(), + (annotationType() != null ? annotationType.getName() : "unknown"))); } } return (T) value; @@ -150,10 +185,11 @@ public class AnnotationAttributes extends LinkedHashMap { /** - * Return an {@link AnnotationAttributes} instance based on the given map; if the map - * is already an {@code AnnotationAttributes} instance, it is casted and returned - * immediately without creating any new instance; otherwise create a new instance by - * wrapping the map with the {@link #AnnotationAttributes(Map)} constructor. + * Return an {@link AnnotationAttributes} instance based on the given map. + *

If the map is already an {@code AnnotationAttributes} instance, it + * will be cast and returned immediately without creating a new instance. + * Otherwise a new instance will be created by passing the supplied map + * to the {@link #AnnotationAttributes(Map)} constructor. * @param map original source of annotation attribute key/value pairs */ public static AnnotationAttributes fromMap(Map map) { diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationConfigurationException.java b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationConfigurationException.java new file mode 100644 index 0000000000..b3317b1b6d --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationConfigurationException.java @@ -0,0 +1,47 @@ +/* + * Copyright 2002-2015 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; + +/** + * Thrown by {@link AnnotationUtils} if an annotation is improperly configured. + * + * @author Sam Brannen + * @since 4.2 + */ +@SuppressWarnings("serial") +public class AnnotationConfigurationException extends RuntimeException { + + /** + * Construct a new {@code AnnotationConfigurationException} with the + * supplied message. + * @param message the detail message + */ + public AnnotationConfigurationException(String message) { + super(message); + } + + /** + * Construct a new {@code AnnotationConfigurationException} with the + * supplied message and cause. + * @param message the detail message + * @param cause the root cause + */ + public AnnotationConfigurationException(String message, Throwable cause) { + super(message, cause); + } + +} 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 336fe8ce87..7c35d70ca4 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 @@ -18,9 +18,13 @@ package org.springframework.core.annotation; import java.lang.annotation.Annotation; import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; -import java.util.Arrays; +import java.lang.reflect.Proxy; +import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; @@ -29,9 +33,9 @@ import java.util.Set; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; - import org.springframework.core.BridgeMethodResolver; import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; import org.springframework.util.ConcurrentReferenceHashMap; import org.springframework.util.ObjectUtils; import org.springframework.util.ReflectionUtils; @@ -88,9 +92,17 @@ import org.springframework.util.StringUtils; */ public abstract class AnnotationUtils { - /** The attribute name for annotations with a single element */ + /** + * The attribute name for annotations with a single element. + */ public static final String VALUE = "value"; + /** + * A object that can be stored in {@link AnnotationAttributes} as a + * placeholder for an attribute's declared default value. + */ + public static final Object DEFAULT_VALUE_PLACEHOLDER = ""; + private static final Map findAnnotationCache = new ConcurrentReferenceHashMap(256); @@ -116,14 +128,15 @@ public abstract class AnnotationUtils { @SuppressWarnings("unchecked") public static A getAnnotation(Annotation ann, Class annotationType) { if (annotationType.isInstance(ann)) { - return (A) ann; + return synthesizeAnnotation((A) ann); } + Class annotatedElement = ann.annotationType(); try { - return ann.annotationType().getAnnotation(annotationType); + return synthesizeAnnotation(annotatedElement, annotatedElement.getAnnotation(annotationType)); } catch (Exception ex) { // Assuming nested Class values not resolvable within annotation attributes... - logIntrospectionFailure(ann.annotationType(), ex); + handleIntrospectionFailure(annotatedElement, ex); return null; } } @@ -151,11 +164,11 @@ public abstract class AnnotationUtils { } } } - return ann; + return synthesizeAnnotation(annotatedElement, ann); } catch (Exception ex) { // Assuming nested Class values not resolvable within annotation attributes... - logIntrospectionFailure(annotatedElement, ex); + handleIntrospectionFailure(annotatedElement, ex); return null; } } @@ -195,7 +208,7 @@ public abstract class AnnotationUtils { } catch (Exception ex) { // Assuming nested Class values not resolvable within annotation attributes... - logIntrospectionFailure(annotatedElement, ex); + handleIntrospectionFailure(annotatedElement, ex); } return null; } @@ -218,7 +231,7 @@ public abstract class AnnotationUtils { } catch (Exception ex) { // Assuming nested Class values not resolvable within annotation attributes... - logIntrospectionFailure(method, ex); + handleIntrospectionFailure(method, ex); } return null; } @@ -274,7 +287,7 @@ public abstract class AnnotationUtils { } catch (Exception ex) { // Assuming nested Class values not resolvable within annotation attributes... - logIntrospectionFailure(annotatedElement, ex); + handleIntrospectionFailure(annotatedElement, ex); } return Collections.emptySet(); } @@ -298,7 +311,8 @@ public abstract class AnnotationUtils { public static A findAnnotation(AnnotatedElement annotatedElement, Class annotationType) { // Do NOT store result in the findAnnotationCache since doing so could break // findAnnotation(Class, Class) and findAnnotation(Method, Class). - return findAnnotation(annotatedElement, annotationType, new HashSet()); + return synthesizeAnnotation(annotatedElement, + findAnnotation(annotatedElement, annotationType, new HashSet())); } /** @@ -332,7 +346,7 @@ public abstract class AnnotationUtils { } catch (Exception ex) { // Assuming nested Class values not resolvable within annotation attributes... - logIntrospectionFailure(annotatedElement, ex); + handleIntrospectionFailure(annotatedElement, ex); } return null; } @@ -389,7 +403,7 @@ public abstract class AnnotationUtils { } } - return result; + return synthesizeAnnotation(method, result); } private static A searchOnInterfaces(Method method, Class annotationType, Class... ifcs) { @@ -426,7 +440,7 @@ public abstract class AnnotationUtils { } catch (Exception ex) { // Assuming nested Class values not resolvable within annotation attributes... - logIntrospectionFailure(ifcMethod, ex); + handleIntrospectionFailure(ifcMethod, ex); } } annotatedInterfaceCache.put(iface, found); @@ -465,7 +479,7 @@ public abstract class AnnotationUtils { findAnnotationCache.put(cacheKey, result); } } - return result; + return synthesizeAnnotation(clazz, result); } /** @@ -499,7 +513,7 @@ public abstract class AnnotationUtils { } catch (Exception ex) { // Assuming nested Class values not resolvable within annotation attributes... - logIntrospectionFailure(clazz, ex); + handleIntrospectionFailure(clazz, ex); return null; } @@ -617,7 +631,7 @@ public abstract class AnnotationUtils { } catch (Exception ex) { // Assuming nested Class values not resolvable within annotation attributes... - logIntrospectionFailure(clazz, ex); + handleIntrospectionFailure(clazz, ex); } return false; } @@ -682,9 +696,10 @@ public abstract class AnnotationUtils { * @return the Map of annotation attributes, with attribute names as keys and * corresponding attribute values as values; never {@code null} * @see #getAnnotationAttributes(Annotation, boolean, boolean) + * @see #getAnnotationAttributes(AnnotatedElement, Annotation, boolean, boolean) */ public static Map getAnnotationAttributes(Annotation annotation) { - return getAnnotationAttributes(annotation, false, false); + return getAnnotationAttributes(null, annotation); } /** @@ -706,8 +721,7 @@ public abstract class AnnotationUtils { } /** - * Retrieve the given annotation's attributes as an {@link AnnotationAttributes} - * map structure. + * Retrieve the given annotation's attributes as an {@link AnnotationAttributes} map. *

This method provides fully recursive annotation reading capabilities on par with * the reflection-based {@link org.springframework.core.type.StandardAnnotationMetadata}. * @param annotation the annotation to retrieve the attributes for @@ -724,19 +738,106 @@ public abstract class AnnotationUtils { */ public static AnnotationAttributes getAnnotationAttributes(Annotation annotation, boolean classValuesAsString, boolean nestedAnnotationsAsMap) { + return getAnnotationAttributes(null, annotation, classValuesAsString, nestedAnnotationsAsMap); + } - AnnotationAttributes attrs = new AnnotationAttributes(); - Method[] methods = annotation.annotationType().getDeclaredMethods(); - for (Method method : methods) { - if (method.getParameterTypes().length == 0 && method.getReturnType() != void.class) { - try { - ReflectionUtils.makeAccessible(method); - Object value = method.invoke(annotation); - attrs.put(method.getName(), adaptValue(value, classValuesAsString, nestedAnnotationsAsMap)); + /** + * Retrieve the given annotation's attributes as an {@link AnnotationAttributes} map. + *

Equivalent to calling {@link #getAnnotationAttributes(AnnotatedElement, Annotation, boolean, boolean)} + * with the {@code classValuesAsString} and {@code nestedAnnotationsAsMap} parameters + * set to {@code false}. + * @param annotation the annotation to retrieve the attributes for + * @return the Map of annotation attributes, with attribute names as keys and + * corresponding attribute values as values; never {@code null} + * @see #getAnnotationAttributes(AnnotatedElement, Annotation, boolean, boolean) + * @since 4.2 + */ + public static AnnotationAttributes getAnnotationAttributes(AnnotatedElement annotatedElement, Annotation annotation) { + return getAnnotationAttributes(annotatedElement, annotation, false, false); + } + + /** + * Retrieve the given annotation's attributes as an {@link AnnotationAttributes} map. + *

This method provides fully recursive annotation reading capabilities on par with + * the reflection-based {@link org.springframework.core.type.StandardAnnotationMetadata}. + * @param annotatedElement the element that is annotated with the supplied annotation, + * used for contextual logging; may be {@code null} if unknown + * @param annotation the annotation to retrieve the attributes for + * @param classValuesAsString whether to convert Class references into Strings (for + * compatibility with {@link org.springframework.core.type.AnnotationMetadata}) + * or to preserve them as Class references + * @param nestedAnnotationsAsMap whether to convert nested Annotation instances into + * {@link AnnotationAttributes} maps (for compatibility with + * {@link org.springframework.core.type.AnnotationMetadata}) or to preserve them as + * Annotation instances + * @param defaultValuesAsPlaceholder whether to replace default values with + * {@link #DEFAULT_VALUE_PLACEHOLDER} or leave them as is + * @return the annotation attributes (a specialized Map) with attribute names as keys + * and corresponding attribute values as values; never {@code null} + * @since 4.2 + */ + public static AnnotationAttributes getAnnotationAttributes(AnnotatedElement annotatedElement, + Annotation annotation, boolean classValuesAsString, boolean nestedAnnotationsAsMap) { + + return getAnnotationAttributes(annotatedElement, annotation, classValuesAsString, nestedAnnotationsAsMap, + false, true); + } + + /** + * Retrieve the given annotation's attributes as an {@link AnnotationAttributes} map. + * + *

This method provides fully recursive annotation reading capabilities on par with + * the reflection-based {@link org.springframework.core.type.StandardAnnotationMetadata}. + * + * @param annotatedElement the element that is annotated with the supplied annotation, + * used for contextual logging; may be {@code null} if unknown + * @param annotation the annotation to retrieve the attributes for + * @param classValuesAsString whether to convert Class references into Strings (for + * compatibility with {@link org.springframework.core.type.AnnotationMetadata}) + * or to preserve them as Class references + * @param nestedAnnotationsAsMap whether to convert nested Annotation instances into + * {@link AnnotationAttributes} maps (for compatibility with + * {@link org.springframework.core.type.AnnotationMetadata}) or to preserve them as + * Annotation instances + * @param defaultValuesAsPlaceholder whether to replace default values with + * {@link #DEFAULT_VALUE_PLACEHOLDER} or leave them as is + * @param synthesizeAnnotation whether or not the annotation should be + * {@linkplain #synthesizeAnnotation synthesized} before processing + * @return the annotation attributes (a specialized Map) with attribute names as keys + * and corresponding attribute values as values; never {@code null} + * @since 4.2 + */ + static AnnotationAttributes getAnnotationAttributes(AnnotatedElement annotatedElement, + Annotation annotation, boolean classValuesAsString, boolean nestedAnnotationsAsMap, + boolean defaultValuesAsPlaceholder, boolean synthesizeAnnotation) { + + if (synthesizeAnnotation) { + annotation = synthesizeAnnotation(annotatedElement, annotation); + } + + Class annotationType = annotation.annotationType(); + AnnotationAttributes attrs = new AnnotationAttributes(annotationType); + for (Method method : getAttributeMethods(annotationType)) { + try { + ReflectionUtils.makeAccessible(method); + Object value = method.invoke(annotation); + + Object defaultValue = method.getDefaultValue(); + if (defaultValuesAsPlaceholder && (defaultValue != null)) { + if (ObjectUtils.nullSafeEquals(value, defaultValue)) { + value = DEFAULT_VALUE_PLACEHOLDER; + } } - catch (Exception ex) { - throw new IllegalStateException("Could not obtain annotation attribute values", ex); + + attrs.put(method.getName(), + adaptValue(annotatedElement, value, classValuesAsString, nestedAnnotationsAsMap)); + } + catch (Exception ex) { + if (ex instanceof InvocationTargetException) { + Throwable targetException = ((InvocationTargetException) ex).getTargetException(); + rethrowAnnotationConfigurationException(targetException); } + throw new IllegalStateException("Could not obtain annotation attribute value for " + method, ex); } } return attrs; @@ -744,6 +845,10 @@ public abstract class AnnotationUtils { /** * Adapt the given value according to the given class and nested annotation settings. + *

Nested annotations will be + * {@linkplain #synthesizeAnnotation(AnnotatedElement, Annotation) synthesized}. + * @param annotatedElement the element that is annotated, used for contextual + * logging; may be {@code null} if unknown * @param value the annotation attribute value * @param classValuesAsString whether to turn Class references into Strings (for * compatibility with {@link org.springframework.core.type.AnnotationMetadata}) @@ -754,34 +859,57 @@ public abstract class AnnotationUtils { * Annotation instances * @return the adapted value, or the original value if no adaptation is needed */ - static Object adaptValue(Object value, boolean classValuesAsString, boolean nestedAnnotationsAsMap) { + static Object adaptValue(AnnotatedElement annotatedElement, Object value, boolean classValuesAsString, + boolean nestedAnnotationsAsMap) { + if (classValuesAsString) { if (value instanceof Class) { - value = ((Class) value).getName(); + return ((Class) value).getName(); } else if (value instanceof Class[]) { Class[] clazzArray = (Class[]) value; - String[] newValue = new String[clazzArray.length]; + String[] classNames = new String[clazzArray.length]; for (int i = 0; i < clazzArray.length; i++) { - newValue[i] = clazzArray[i].getName(); + classNames[i] = clazzArray[i].getName(); } - value = newValue; + return classNames; } } - if (nestedAnnotationsAsMap && value instanceof Annotation) { - return getAnnotationAttributes((Annotation) value, classValuesAsString, true); - } - else if (nestedAnnotationsAsMap && value instanceof Annotation[]) { - Annotation[] realAnnotations = (Annotation[]) value; - AnnotationAttributes[] mappedAnnotations = new AnnotationAttributes[realAnnotations.length]; - for (int i = 0; i < realAnnotations.length; i++) { - mappedAnnotations[i] = getAnnotationAttributes(realAnnotations[i], classValuesAsString, true); + + if (value instanceof Annotation) { + Annotation annotation = (Annotation) value; + + if (nestedAnnotationsAsMap) { + return getAnnotationAttributes(annotatedElement, annotation, classValuesAsString, + nestedAnnotationsAsMap); + } + else { + return synthesizeAnnotation(annotatedElement, annotation); } - return mappedAnnotations; } - else { - return value; + + if (value instanceof Annotation[]) { + Annotation[] annotations = (Annotation[]) value; + + if (nestedAnnotationsAsMap) { + AnnotationAttributes[] mappedAnnotations = new AnnotationAttributes[annotations.length]; + for (int i = 0; i < annotations.length; i++) { + mappedAnnotations[i] = getAnnotationAttributes(annotatedElement, annotations[i], + classValuesAsString, nestedAnnotationsAsMap); + } + return mappedAnnotations; + } + else { + Annotation[] synthesizedAnnotations = new Annotation[annotations.length]; + for (int i = 0; i < annotations.length; i++) { + synthesizedAnnotations[i] = synthesizeAnnotation(annotatedElement, annotations[i]); + } + return synthesizedAnnotations; + } } + + // Fallback + return value; } /** @@ -803,7 +931,7 @@ public abstract class AnnotationUtils { * @see #getValue(Annotation) */ public static Object getValue(Annotation annotation, String attributeName) { - if (annotation == null || !StringUtils.hasLength(attributeName)) { + if (annotation == null || !StringUtils.hasText(attributeName)) { return null; } try { @@ -861,7 +989,7 @@ public abstract class AnnotationUtils { * @see #getDefaultValue(Annotation, String) */ public static Object getDefaultValue(Class annotationType, String attributeName) { - if (annotationType == null || !StringUtils.hasLength(attributeName)) { + if (annotationType == null || !StringUtils.hasText(attributeName)) { return null; } try { @@ -872,14 +1000,343 @@ public abstract class AnnotationUtils { } } + /** + * TODO Document synthesizeAnnotation(). + * + * @param annotation the annotation to synthesize + * @since 4.2 + * @see #synthesizeAnnotation(AnnotatedElement, Annotation) + */ + public static A synthesizeAnnotation(A annotation) { + return synthesizeAnnotation(null, annotation); + } /** - * Log an introspection failure (in particular {@code TypeNotPresentExceptions}) - - * before moving on, pretending there were no annotations on this specific element. + * TODO Document synthesizeAnnotation(). + * + * @param annotatedElement the element that is annotated with the supplied + * annotation, used for contextual logging; may be {@code null} if unknown + * @param annotation the annotation to synthesize + * @since 4.2 + */ + @SuppressWarnings("unchecked") + public static A synthesizeAnnotation(AnnotatedElement annotatedElement, A annotation) { + if (annotation == null) { + return null; + } + if (annotation instanceof SynthesizedAnnotation) { + return annotation; + } + + Class annotationType = annotation.annotationType(); + + // No need to synthesize? + if (!isSynthesizable(annotationType)) { + return annotation; + } + + InvocationHandler handler = new SynthesizedAnnotationInvocationHandler(annotatedElement, annotation, getAliasMap(annotationType)); + A synthesizedAnnotation = (A) Proxy.newProxyInstance(ClassUtils.getDefaultClassLoader(), new Class[] { + (Class) annotationType, SynthesizedAnnotation.class }, handler); + + return synthesizedAnnotation; + } + + /** + * TODO Document getAliasMap(). + * @since 4.2 + */ + private static Map getAliasMap(Class annotationType) { + if (annotationType == null) { + return null; + } + + Map map = new HashMap(); + for (Method attribute : getAttributeMethods(annotationType)) { + String attributeName = attribute.getName(); + String aliasedAttributeName = getAliasedAttributeName(attribute); + if (aliasedAttributeName != null) { + map.put(attributeName, aliasedAttributeName); + } + } + return map; + } + + /** + * TODO Document isSynthesizable(). + * @since 4.2 + */ + @SuppressWarnings("unchecked") + private static boolean isSynthesizable(Class annotationType) { + + for (Method attribute : getAttributeMethods(annotationType)) { + if (getAliasedAttributeName(attribute) != null) { + return true; + } + + Class returnType = attribute.getReturnType(); + + if (Annotation[].class.isAssignableFrom(returnType)) { + Class nestedAnnotationType = (Class) returnType.getComponentType(); + if (isSynthesizable(nestedAnnotationType)) { + return true; + } + } + else if (Annotation.class.isAssignableFrom(returnType)) { + Class nestedAnnotationType = (Class) returnType; + if (isSynthesizable(nestedAnnotationType)) { + return true; + } + } + } + + return false; + } + + /** + * Get the name of the aliased attribute configured via + * {@link AliasFor @AliasFor} on the supplied annotation {@code attribute}. + * + *

This method does not resolve aliases in other annotations. In + * other words, if {@code @AliasFor} is present on the supplied + * {@code attribute} but {@linkplain AliasFor#annotation references an + * annotation} other than {@link Annotation}, this method will return + * {@code null} immediately. + * + * @param attribute the attribute to find an alias for + * @return the name of the aliased attribute, or {@code null} if not found + * @throws IllegalArgumentException if the supplied attribute method is + * not from an annotation, or if the supplied target type is {@link Annotation} + * @throws AnnotationConfigurationException if invalid configuration of + * {@code @AliasFor} is detected + * @see #getAliasedAttributeName(Method, Class) + * @since 4.2 + */ + static String getAliasedAttributeName(Method attribute) { + return getAliasedAttributeName(attribute, null); + } + + /** + * Get the name of the aliased attribute configured via + * {@link AliasFor @AliasFor} on the supplied annotation {@code attribute}. + * + * @param attribute the attribute to find an alias for + * @param targetAnnotationType the type of annotation in which the + * aliased attribute is allowed to be declared; {@code null} implies + * within the same annotation + * @return the name of the aliased attribute, or {@code null} if not found + * @throws IllegalArgumentException if the supplied attribute method is + * not from an annotation, or if the supplied target type is {@link Annotation} + * @throws AnnotationConfigurationException if invalid configuration of + * {@code @AliasFor} is detected + * @since 4.2 + */ + @SuppressWarnings("unchecked") + static String getAliasedAttributeName(Method attribute, Class targetAnnotationType) { + Class declaringClass = attribute.getDeclaringClass(); + Assert.isTrue(declaringClass.isAnnotation(), "attribute method must be from an annotation"); + Assert.isTrue(!Annotation.class.equals(targetAnnotationType), + "targetAnnotationType must not be java.lang.annotation.Annotation"); + + AliasFor aliasFor = attribute.getAnnotation(AliasFor.class); + + // Nothing to check + if (aliasFor == null) { + return null; + } + + Class sourceAnnotationType = (Class) declaringClass; + Class aliasedAnnotationType = aliasFor.annotation(); + + boolean searchWithinSameAnnotation = (targetAnnotationType == null); + boolean sameTargetDeclared = (sourceAnnotationType.equals(aliasedAnnotationType) || Annotation.class.equals(aliasedAnnotationType)); + + // Wrong search scope? + if (searchWithinSameAnnotation && !sameTargetDeclared) { + return null; + } + + String attributeName = attribute.getName(); + String aliasedAttributeName = aliasFor.attribute(); + + if (!StringUtils.hasText(aliasedAttributeName)) { + String msg = String.format( + "@AliasFor declaration on attribute [%s] in annotation [%s] is missing required 'attribute' value.", + attributeName, sourceAnnotationType.getName()); + throw new AnnotationConfigurationException(msg); + } + + if (sameTargetDeclared) { + aliasedAnnotationType = sourceAnnotationType; + } + + Method aliasedAttribute = null; + try { + aliasedAttribute = aliasedAnnotationType.getDeclaredMethod(aliasedAttributeName); + } + catch (NoSuchMethodException e) { + String msg = String.format( + "Attribute [%s] in annotation [%s] is declared as an @AliasFor nonexistent attribute [%s] in annotation [%s].", + attributeName, sourceAnnotationType.getName(), aliasedAttributeName, aliasedAnnotationType.getName()); + throw new AnnotationConfigurationException(msg, e); + } + + if (sameTargetDeclared) { + AliasFor mirrorAliasFor = aliasedAttribute.getAnnotation(AliasFor.class); + if (mirrorAliasFor == null) { + String msg = String.format("Attribute [%s] in annotation [%s] must be declared as an @AliasFor [%s].", + aliasedAttributeName, sourceAnnotationType.getName(), attributeName); + throw new AnnotationConfigurationException(msg); + } + + String mirrorAliasedAttributeName = mirrorAliasFor.attribute(); + if (!attributeName.equals(mirrorAliasedAttributeName)) { + String msg = String.format( + "Attribute [%s] in annotation [%s] must be declared as an @AliasFor [%s], not [%s].", + aliasedAttributeName, sourceAnnotationType.getName(), attributeName, mirrorAliasedAttributeName); + throw new AnnotationConfigurationException(msg); + } + } + + Class returnType = attribute.getReturnType(); + Class aliasedReturnType = aliasedAttribute.getReturnType(); + if (!returnType.equals(aliasedReturnType)) { + String msg = String.format("Misconfigured aliases: attribute [%s] in annotation [%s] " + + "and attribute [%s] in annotation [%s] must declare the same return type.", attributeName, + sourceAnnotationType.getName(), aliasedAttributeName, aliasedAnnotationType.getName()); + throw new AnnotationConfigurationException(msg); + } + + if (sameTargetDeclared) { + Object defaultValue = attribute.getDefaultValue(); + Object aliasedDefaultValue = aliasedAttribute.getDefaultValue(); + + if ((defaultValue == null) || (aliasedDefaultValue == null)) { + String msg = String.format("Misconfigured aliases: attribute [%s] in annotation [%s] " + + "and attribute [%s] in annotation [%s] must declare default values.", attributeName, + sourceAnnotationType.getName(), aliasedAttributeName, aliasedAnnotationType.getName()); + throw new AnnotationConfigurationException(msg); + } + + if (!ObjectUtils.nullSafeEquals(defaultValue, aliasedDefaultValue)) { + String msg = String.format("Misconfigured aliases: attribute [%s] in annotation [%s] " + + "and attribute [%s] in annotation [%s] must declare the same default value.", attributeName, + sourceAnnotationType.getName(), aliasedAttributeName, aliasedAnnotationType.getName()); + throw new AnnotationConfigurationException(msg); + } + } + + return aliasedAttributeName; + } + + /** + * TODO Document getAttributeMethods(). + * + * @since 4.2 + */ + static List getAttributeMethods(Class annotationType) { + List methods = new ArrayList(); + for (Method method : annotationType.getDeclaredMethods()) { + if ((method.getParameterTypes().length == 0) && (method.getReturnType() != void.class)) { + methods.add(method); + } + } + return methods; + } + + /** + * TODO Document postProcessAnnotationAttributes(). + * + * @param annotatedElement the element that is annotated with the supplied + * annotation, used for contextual logging; may be {@code null} if unknown + * @param attributes the annotation attributes to validate + * @since 4.2 + */ + static void postProcessAnnotationAttributes(AnnotatedElement element, AnnotationAttributes attributes, + boolean classValuesAsString, boolean nestedAnnotationsAsMap) { + + // Abort? + if (attributes == null) { + return; + } + + Class annotationType = attributes.annotationType(); + Map aliasMap = getAliasMap(annotationType); + + // Validate @AliasFor configuration + if (aliasMap != null) { + Set validated = new HashSet(); + + for (String attributeName : aliasMap.keySet()) { + String aliasedAttributeName = aliasMap.get(attributeName); + + if (validated.add(attributeName) && validated.add(aliasedAttributeName)) { + Object value = attributes.get(attributeName); + Object aliasedValue = attributes.get(aliasedAttributeName); + + if (!ObjectUtils.nullSafeEquals(value, aliasedValue) && !DEFAULT_VALUE_PLACEHOLDER.equals(value) + && !DEFAULT_VALUE_PLACEHOLDER.equals(aliasedValue)) { + String elementAsString = (element == null ? "unknown element" : element.toString()); + String msg = String.format( + "In AnnotationAttributes for annotation [%s] declared on [%s], attribute [%s] and its alias [%s] are " + + "declared with values of [%s] and [%s], but only one declaration is permitted.", + annotationType.getName(), elementAsString, attributeName, aliasedAttributeName, + ObjectUtils.nullSafeToString(value), ObjectUtils.nullSafeToString(aliasedValue)); + throw new AnnotationConfigurationException(msg); + } + + // Replace default values with aliased values... + if (DEFAULT_VALUE_PLACEHOLDER.equals(value)) { + attributes.put(attributeName, + adaptValue(element, aliasedValue, classValuesAsString, nestedAnnotationsAsMap)); + } + if (DEFAULT_VALUE_PLACEHOLDER.equals(aliasedValue)) { + attributes.put(aliasedAttributeName, + adaptValue(element, value, classValuesAsString, nestedAnnotationsAsMap)); + } + } + } + } + + for (String attributeName : attributes.keySet()) { + Object value = attributes.get(attributeName); + if (DEFAULT_VALUE_PLACEHOLDER.equals(value)) { + attributes.put(attributeName, + adaptValue(element, getDefaultValue(annotationType, attributeName), classValuesAsString, + nestedAnnotationsAsMap)); + } + } + } + + /** + *

If the supplied throwable is an {@link AnnotationConfigurationException}, + * it will be cast to an {@code AnnotationConfigurationException} and thrown, + * allowing it to propagate to the caller. + *

Otherwise, this method does nothing. + * @since 4.2 + */ + static void rethrowAnnotationConfigurationException(Throwable t) { + if (t instanceof AnnotationConfigurationException) { + throw (AnnotationConfigurationException) t; + } + } + + /** + * Handle the supplied annotation introspection exception. + *

If the supplied exception is an {@link AnnotationConfigurationException}, + * it will simply be thrown, allowing it to propagate to the caller, and + * nothing will be logged. + *

Otherwise, this method logs an introspection failure (in particular + * {@code TypeNotPresentExceptions}) — before moving on, pretending + * there were no annotations on this specific element. * @param element the element that we tried to introspect annotations on * @param ex the exception that we encountered + * @see #rethrowAnnotationConfigurationException */ - static void logIntrospectionFailure(AnnotatedElement element, Exception ex) { + static void handleIntrospectionFailure(AnnotatedElement element, Exception ex) { + + rethrowAnnotationConfigurationException(ex); + Log loggerToUse = logger; if (loggerToUse == null) { loggerToUse = LogFactory.getLog(AnnotationUtils.class); @@ -896,7 +1353,6 @@ public abstract class AnnotationUtils { if (loggerToUse.isInfoEnabled()) { logger.info("Failed to introspect annotations on [" + element + "]: " + ex); } - } } @@ -962,10 +1418,10 @@ public abstract class AnnotationUtils { for (Annotation ann : element.getAnnotations()) { Class currentAnnotationType = ann.annotationType(); if (ObjectUtils.nullSafeEquals(this.annotationType, currentAnnotationType)) { - this.result.add((A) ann); + this.result.add(synthesizeAnnotation(element, (A) ann)); } else if (ObjectUtils.nullSafeEquals(this.containerAnnotationType, currentAnnotationType)) { - this.result.addAll(getValue(ann)); + this.result.addAll(getValue(element, ann)); } else if (!isInJavaLangAnnotationPackage(ann)) { process(currentAnnotationType); @@ -973,17 +1429,23 @@ public abstract class AnnotationUtils { } } catch (Exception ex) { - logIntrospectionFailure(element, ex); + handleIntrospectionFailure(element, ex); } } } @SuppressWarnings("unchecked") - private List getValue(Annotation annotation) { + private List getValue(AnnotatedElement element, Annotation annotation) { try { Method method = annotation.annotationType().getDeclaredMethod("value"); ReflectionUtils.makeAccessible(method); - return Arrays.asList((A[]) method.invoke(annotation)); + A[] annotations = (A[]) method.invoke(annotation); + + List synthesizedAnnotations = new ArrayList(); + for (A anno : annotations) { + synthesizedAnnotations.add(synthesizeAnnotation(element, anno)); + } + return synthesizedAnnotations; } catch (Exception ex) { // Unable to read value from repeating annotation container -> ignore it. diff --git a/spring-core/src/main/java/org/springframework/core/annotation/SynthesizedAnnotation.java b/spring-core/src/main/java/org/springframework/core/annotation/SynthesizedAnnotation.java new file mode 100644 index 0000000000..b2f082e61b --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/annotation/SynthesizedAnnotation.java @@ -0,0 +1,28 @@ +/* + * Copyright 2002-2015 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; + +/** + * Marker interface implemented by synthesized annotation proxies. + * + *

Used to detect whether an annotation has already been synthesized. + * + * @author Sam Brannen + * @since 4.2 + */ +interface SynthesizedAnnotation { +} diff --git a/spring-core/src/main/java/org/springframework/core/annotation/SynthesizedAnnotationInvocationHandler.java b/spring-core/src/main/java/org/springframework/core/annotation/SynthesizedAnnotationInvocationHandler.java new file mode 100644 index 0000000000..a4b6555a19 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/annotation/SynthesizedAnnotationInvocationHandler.java @@ -0,0 +1,118 @@ +/* + * Copyright 2002-2015 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.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.util.Map; + +import org.springframework.util.ObjectUtils; +import org.springframework.util.ReflectionUtils; + +/** + * TODO Document SynthesizedAnnotationInvocationHandler. + * + * @author Sam Brannen + * @since 4.2 + */ +class SynthesizedAnnotationInvocationHandler implements InvocationHandler { + + private final AnnotatedElement annotatedElement; + + private final Annotation annotation; + + private final Map aliasPairs; + + + public SynthesizedAnnotationInvocationHandler(Annotation annotation, Map aliasPairs) { + this(null, annotation, aliasPairs); + } + + public SynthesizedAnnotationInvocationHandler(AnnotatedElement annotatedElement, Annotation annotation, + Map aliasPairs) { + this.annotatedElement = annotatedElement; + this.annotation = annotation; + this.aliasPairs = aliasPairs; + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + String attributeName = method.getName(); + Class returnType = method.getReturnType(); + boolean nestedAnnotation = (Annotation[].class.isAssignableFrom(returnType) || Annotation.class.isAssignableFrom(returnType)); + String aliasedAttributeName = aliasPairs.get(attributeName); + boolean aliasPresent = aliasedAttributeName != null; + + ReflectionUtils.makeAccessible(method); + Object value = ReflectionUtils.invokeMethod(method, this.annotation, args); + + // Nothing special to do? + if (!aliasPresent && !nestedAnnotation) { + return value; + } + + if (aliasPresent) { + Method aliasedMethod = null; + try { + aliasedMethod = annotation.annotationType().getDeclaredMethod(aliasedAttributeName); + } + catch (NoSuchMethodException e) { + String msg = String.format("In annotation [%s], attribute [%s] is declared as an @AliasFor [%s], " + + "but attribute [%s] does not exist.", annotation.annotationType().getName(), attributeName, + aliasedAttributeName, aliasedAttributeName); + throw new AnnotationConfigurationException(msg); + } + + ReflectionUtils.makeAccessible(aliasedMethod); + Object aliasedValue = ReflectionUtils.invokeMethod(aliasedMethod, this.annotation, args); + Object defaultValue = AnnotationUtils.getDefaultValue(annotation, attributeName); + + if (!ObjectUtils.nullSafeEquals(value, aliasedValue) && !ObjectUtils.nullSafeEquals(value, defaultValue) + && !ObjectUtils.nullSafeEquals(aliasedValue, defaultValue)) { + String elementAsString = (annotatedElement == null ? "unknown element" : annotatedElement.toString()); + String msg = String.format( + "In annotation [%s] declared on [%s], attribute [%s] and its alias [%s] are " + + "declared with values of [%s] and [%s], but only one declaration is permitted.", + annotation.annotationType().getName(), elementAsString, attributeName, aliasedAttributeName, + ObjectUtils.nullSafeToString(value), ObjectUtils.nullSafeToString(aliasedValue)); + throw new AnnotationConfigurationException(msg); + } + + // If the user didn't declare the annotation with an explicit value, return + // the value of the alias. + if (ObjectUtils.nullSafeEquals(value, defaultValue)) { + value = aliasedValue; + } + } + + // Synthesize nested annotations before returning them. + if (value instanceof Annotation) { + value = AnnotationUtils.synthesizeAnnotation(annotatedElement, (Annotation) value); + } + else if (value instanceof Annotation[]) { + Annotation[] annotations = (Annotation[]) value; + for (int i = 0; i < annotations.length; i++) { + annotations[i] = AnnotationUtils.synthesizeAnnotation(annotatedElement, annotations[i]); + } + } + + return value; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/annotation/package-info.java b/spring-core/src/main/java/org/springframework/core/annotation/package-info.java index 1e9c577708..6aceac2c80 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/package-info.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/package-info.java @@ -1,4 +1,5 @@ /** - * Core support package for Java 5 annotations. + * Core support package for annotations, meta-annotations, and composed + * annotations with attribute overrides. */ package org.springframework.core.annotation; diff --git a/spring-core/src/test/java/org/springframework/core/annotation/AnnotatedElementUtilsTests.java b/spring-core/src/test/java/org/springframework/core/annotation/AnnotatedElementUtilsTests.java index 1f53718edc..c9bfe2876e 100644 --- a/spring-core/src/test/java/org/springframework/core/annotation/AnnotatedElementUtilsTests.java +++ b/spring-core/src/test/java/org/springframework/core/annotation/AnnotatedElementUtilsTests.java @@ -16,7 +16,6 @@ package org.springframework.core.annotation; -import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Inherited; import java.lang.annotation.Retention; @@ -26,11 +25,14 @@ import java.lang.reflect.Method; import java.util.Set; import java.util.stream.Collectors; +import org.junit.Rule; import org.junit.Test; - +import org.junit.rules.ExpectedException; import org.springframework.stereotype.Component; import org.springframework.util.MultiValueMap; +import static org.hamcrest.Matchers.*; + import static java.util.Arrays.*; import static org.junit.Assert.*; import static org.springframework.core.annotation.AnnotatedElementUtils.*; @@ -46,9 +48,8 @@ public class AnnotatedElementUtilsTests { private static final String TX_NAME = Transactional.class.getName(); - private Set names(Class... classes) { - return stream(classes).map(clazz -> clazz.getName()).collect(Collectors.toSet()); - } + @Rule + public final ExpectedException exception = ExpectedException.none(); @Test public void getMetaAnnotationTypesOnNonAnnotatedClass() { @@ -180,7 +181,8 @@ public class AnnotatedElementUtilsTests { public void getAllAnnotationAttributesOnClassWithMultipleComposedAnnotations() { MultiValueMap attributes = getAllAnnotationAttributes(TxFromMultipleComposedAnnotations.class, TX_NAME); assertNotNull("Annotation attributes map for @Transactional on TxFromMultipleComposedAnnotations", attributes); - assertEquals("value for TxFromMultipleComposedAnnotations.", asList("TxComposed1", "TxComposed2"), attributes.get("value")); + assertEquals("value for TxFromMultipleComposedAnnotations.", asList("TxInheritedComposed", "TxComposed"), + attributes.get("value")); } @Test @@ -274,6 +276,77 @@ public class AnnotatedElementUtilsTests { assertTrue(isAnnotated(element, name)); } + @Test + public void getAnnotationAttributesWithConventionBasedComposedAnnotation() { + Class element = ConventionBasedComposedContextConfigClass.class; + String name = ContextConfig.class.getName(); + AnnotationAttributes attributes = getAnnotationAttributes(element, name); + + assertNotNull("Should find @ContextConfig on " + element.getSimpleName(), attributes); + assertArrayEquals("locations", new String[] { "explicitDeclaration" }, attributes.getStringArray("locations")); + assertArrayEquals("value", new String[] { "explicitDeclaration" }, attributes.getStringArray("value")); + + // Verify contracts between utility methods: + assertTrue(isAnnotated(element, name)); + } + + @Test + public void getAnnotationAttributesWithAliasedComposedAnnotation() { + Class element = AliasedComposedContextConfigClass.class; + String name = ContextConfig.class.getName(); + AnnotationAttributes attributes = getAnnotationAttributes(element, name); + + assertNotNull("Should find @ContextConfig on " + element.getSimpleName(), attributes); + assertArrayEquals("value", new String[] { "test.xml" }, attributes.getStringArray("value")); + assertArrayEquals("locations", new String[] { "test.xml" }, attributes.getStringArray("locations")); + + // Verify contracts between utility methods: + assertTrue(isAnnotated(element, name)); + } + + @Test + public void getAnnotationAttributesWithAliasedValueComposedAnnotation() { + Class element = AliasedValueComposedContextConfigClass.class; + String name = ContextConfig.class.getName(); + AnnotationAttributes attributes = getAnnotationAttributes(element, name); + + assertNotNull("Should find @ContextConfig on " + element.getSimpleName(), attributes); + assertArrayEquals("locations", new String[] { "test.xml" }, attributes.getStringArray("locations")); + assertArrayEquals("value", new String[] { "test.xml" }, attributes.getStringArray("value")); + + // Verify contracts between utility methods: + assertTrue(isAnnotated(element, name)); + } + + @Test + public void getAnnotationAttributesWithInvalidConventionBasedComposedAnnotation() { + Class element = InvalidConventionBasedComposedContextConfigClass.class; + String name = ContextConfig.class.getName(); + + exception.expect(AnnotationConfigurationException.class); + exception.expectMessage(either(containsString("attribute [value] and its alias [locations]")).or( + containsString("attribute [locations] and its alias [value]"))); + exception.expectMessage(either( + containsString("values of [{duplicateDeclaration}] and [{requiredLocationsDeclaration}]")).or( + containsString("values of [{requiredLocationsDeclaration}] and [{duplicateDeclaration}]"))); + exception.expectMessage(containsString("but only one declaration is permitted")); + getAnnotationAttributes(element, name); + } + + @Test + public void getAnnotationAttributesWithInvalidAliasedComposedAnnotation() { + Class element = InvalidAliasedComposedContextConfigClass.class; + String name = ContextConfig.class.getName(); + + exception.expect(AnnotationConfigurationException.class); + exception.expectMessage(either(containsString("attribute [value] and its alias [locations]")).or( + containsString("attribute [locations] and its alias [value]"))); + exception.expectMessage(either(containsString("values of [{duplicateDeclaration}] and [{test.xml}]")).or( + containsString("values of [{test.xml}] and [{duplicateDeclaration}]"))); + exception.expectMessage(containsString("but only one declaration is permitted")); + getAnnotationAttributes(element, name); + } + @Test public void findAnnotationAttributesOnInheritedAnnotationInterface() { AnnotationAttributes attributes = findAnnotationAttributes(InheritedAnnotationInterface.class, Transactional.class); @@ -375,27 +448,39 @@ public class AnnotatedElementUtilsTests { assertEquals("TX qualifier for MetaAndLocalTxConfigClass.", "localTxMgr", attributes.getString("qualifier")); } + @Test + public void findAnnotationAttributesOnClassWithAttributeAliasesInTargetAnnotation() { + AnnotationAttributes attributes = findAnnotationAttributes(AliasedTransactionalComponentClass.class, + AliasedTransactional.class); + assertNotNull("Should find @AliasedTransactional on AliasedTransactionalComponentClass", attributes); + assertEquals("TX value for AliasedTransactionalComponentClass.", "aliasForQualifier", + attributes.getString("value")); + assertEquals("TX qualifier for AliasedTransactionalComponentClass.", "aliasForQualifier", + attributes.getString("qualifier")); + } + + private Set names(Class... classes) { + return stream(classes).map(clazz -> clazz.getName()).collect(Collectors.toSet()); + } + // ------------------------------------------------------------------------- @MetaCycle3 @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.ANNOTATION_TYPE) - @Documented @interface MetaCycle1 { } @MetaCycle1 @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.ANNOTATION_TYPE) - @Documented @interface MetaCycle2 { } @MetaCycle2 @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) - @Documented @interface MetaCycle3 { } @@ -407,7 +492,6 @@ public class AnnotatedElementUtilsTests { @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.TYPE, ElementType.METHOD }) - @Documented @Inherited @interface Transactional { @@ -418,19 +502,29 @@ public class AnnotatedElementUtilsTests { boolean readOnly() default false; } + @Retention(RetentionPolicy.RUNTIME) + @Target({ ElementType.TYPE, ElementType.METHOD }) + @Inherited + @interface AliasedTransactional { + + @AliasFor(attribute = "qualifier") + String value() default ""; + + @AliasFor(attribute = "value") + String qualifier() default ""; + } + @Transactional(qualifier = "composed1") @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) - @Documented @Inherited - @interface Composed1 { + @interface InheritedComposed { } @Transactional(qualifier = "composed2", readOnly = true) @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) - @Documented - @interface Composed2 { + @interface Composed { } @Transactional @@ -440,14 +534,14 @@ public class AnnotatedElementUtilsTests { String qualifier() default "txMgr"; } - @Transactional("TxComposed1") + @Transactional("TxInheritedComposed") @Retention(RetentionPolicy.RUNTIME) - @interface TxComposed1 { + @interface TxInheritedComposed { } - @Transactional("TxComposed2") + @Transactional("TxComposed") @Retention(RetentionPolicy.RUNTIME) - @interface TxComposed2 { + @interface TxComposed { } @Transactional @@ -461,6 +555,12 @@ public class AnnotatedElementUtilsTests { @interface ComposedTransactionalComponent { } + @AliasedTransactional(value = "aliasForQualifier") + @Component + @Retention(RetentionPolicy.RUNTIME) + @interface AliasedTransactionalComponent { + } + @TxComposedWithOverride // Override default "txMgr" from @TxComposedWithOverride with "localTxMgr" @Transactional(qualifier = "localTxMgr") @@ -469,6 +569,63 @@ public class AnnotatedElementUtilsTests { @interface MetaAndLocalTxConfig { } + /** + * Mock of {@link org.springframework.test.context.ContextConfiguration}. + */ + @Retention(RetentionPolicy.RUNTIME) + static @interface ContextConfig { + + @AliasFor(attribute = "locations") + String[] value() default {}; + + @AliasFor(attribute = "value") + String[] locations() default {}; + } + + @ContextConfig + @Retention(RetentionPolicy.RUNTIME) + static @interface ConventionBasedComposedContextConfig { + + String[] locations() default {}; + } + + @ContextConfig(value = "duplicateDeclaration") + @Retention(RetentionPolicy.RUNTIME) + static @interface InvalidConventionBasedComposedContextConfig { + + String[] locations(); + } + + @ContextConfig + @Retention(RetentionPolicy.RUNTIME) + static @interface AliasedComposedContextConfig { + + @AliasFor(annotation = ContextConfig.class, attribute = "locations") + String[] xmlConfigFiles(); + } + + @ContextConfig + @Retention(RetentionPolicy.RUNTIME) + static @interface AliasedValueComposedContextConfig { + + @AliasFor(annotation = ContextConfig.class, attribute = "value") + String[] locations(); + } + + /** + * Invalid because the configuration declares a value for 'value' and + * requires a value for the aliased 'locations'. So we likely end up with + * both 'value' and 'locations' being present in {@link AnnotationAttributes} + * but with different values, which violates the contract of {@code @AliasFor}. + */ + @ContextConfig(value = "duplicateDeclaration") + @Retention(RetentionPolicy.RUNTIME) + static @interface InvalidAliasedComposedContextConfig { + + @AliasFor(annotation = ContextConfig.class, attribute = "locations") + String[] xmlConfigFiles(); + } + // ------------------------------------------------------------------------- static class NonAnnotatedClass { @@ -485,22 +642,26 @@ public class AnnotatedElementUtilsTests { static class ComposedTransactionalComponentClass { } + @AliasedTransactionalComponent + static class AliasedTransactionalComponentClass { + } + @Transactional static class ClassWithInheritedAnnotation { } - @Composed2 + @Composed static class SubClassWithInheritedAnnotation extends ClassWithInheritedAnnotation { } static class SubSubClassWithInheritedAnnotation extends SubClassWithInheritedAnnotation { } - @Composed1 + @InheritedComposed static class ClassWithInheritedComposedAnnotation { } - @Composed2 + @Composed static class SubClassWithInheritedComposedAnnotation extends ClassWithInheritedComposedAnnotation { } @@ -519,8 +680,8 @@ public class AnnotatedElementUtilsTests { static class DerivedTxConfig extends TxConfig { } - @TxComposed1 - @TxComposed2 + @TxInheritedComposed + @TxComposed static class TxFromMultipleComposedAnnotations { } @@ -595,4 +756,23 @@ public class AnnotatedElementUtilsTests { public static interface SubSubNonInheritedAnnotationInterface extends SubNonInheritedAnnotationInterface { } + @ConventionBasedComposedContextConfig(locations = "explicitDeclaration") + static class ConventionBasedComposedContextConfigClass { + } + + @InvalidConventionBasedComposedContextConfig(locations = "requiredLocationsDeclaration") + static class InvalidConventionBasedComposedContextConfigClass { + } + + @AliasedComposedContextConfig(xmlConfigFiles = "test.xml") + static class AliasedComposedContextConfigClass { + } + + @AliasedValueComposedContextConfig(locations = "test.xml") + static class AliasedValueComposedContextConfigClass { + } + + @InvalidAliasedComposedContextConfig(xmlConfigFiles = "test.xml") + static class InvalidAliasedComposedContextConfigClass { + } } 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 cf4f7543d5..ab2b29bc62 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 @@ -23,11 +23,13 @@ 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 java.util.stream.Collectors; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.ExpectedException; import org.springframework.core.Ordered; import org.springframework.core.annotation.subpackage.NonPublicAnnotatedClass; @@ -48,6 +50,9 @@ import static org.springframework.core.annotation.AnnotationUtils.*; */ public class AnnotationUtilsTests { + @Rule + public final ExpectedException exception = ExpectedException.none(); + @Test public void findMethodAnnotationOnLeaf() throws Exception { Method m = Leaf.class.getMethod("annotatedOnLeaf"); @@ -154,7 +159,8 @@ public class AnnotationUtilsTests { /** @since 4.1.2 */ @Test public void findClassAnnotationFavorsMoreLocallyDeclaredComposedAnnotationsOverAnnotationsOnInterfaces() { - Component component = AnnotationUtils.findAnnotation(ClassWithLocalMetaAnnotationAndMetaAnnotatedInterface.class, Component.class); + Component component = findAnnotation(ClassWithLocalMetaAnnotationAndMetaAnnotatedInterface.class, + Component.class); assertNotNull(component); assertEquals("meta2", component.value()); } @@ -162,7 +168,7 @@ public class AnnotationUtilsTests { /** @since 4.0.3 */ @Test public void findClassAnnotationFavorsMoreLocallyDeclaredComposedAnnotationsOverInheritedAnnotations() { - Transactional transactional = AnnotationUtils.findAnnotation(SubSubClassWithInheritedAnnotation.class, Transactional.class); + Transactional transactional = findAnnotation(SubSubClassWithInheritedAnnotation.class, Transactional.class); assertNotNull(transactional); assertTrue("readOnly flag for SubSubClassWithInheritedAnnotation", transactional.readOnly()); } @@ -170,21 +176,21 @@ public class AnnotationUtilsTests { /** @since 4.0.3 */ @Test public void findClassAnnotationFavorsMoreLocallyDeclaredComposedAnnotationsOverInheritedComposedAnnotations() { - Component component = AnnotationUtils.findAnnotation(SubSubClassWithInheritedMetaAnnotation.class, Component.class); + Component component = findAnnotation(SubSubClassWithInheritedMetaAnnotation.class, Component.class); assertNotNull(component); assertEquals("meta2", component.value()); } @Test public void findClassAnnotationOnMetaMetaAnnotatedClass() { - Component component = AnnotationUtils.findAnnotation(MetaMetaAnnotatedClass.class, Component.class); + Component component = findAnnotation(MetaMetaAnnotatedClass.class, Component.class); assertNotNull("Should find meta-annotation on composed annotation on class", component); assertEquals("meta2", component.value()); } @Test public void findClassAnnotationOnMetaMetaMetaAnnotatedClass() { - Component component = AnnotationUtils.findAnnotation(MetaMetaMetaAnnotatedClass.class, Component.class); + Component component = findAnnotation(MetaMetaMetaAnnotatedClass.class, Component.class); assertNotNull("Should find meta-annotation on meta-annotation on composed annotation on class", component); assertEquals("meta2", component.value()); } @@ -192,55 +198,55 @@ public class AnnotationUtilsTests { @Test public void findClassAnnotationOnAnnotatedClassWithMissingTargetMetaAnnotation() { // TransactionalClass is NOT annotated or meta-annotated with @Component - Component component = AnnotationUtils.findAnnotation(TransactionalClass.class, Component.class); + Component component = findAnnotation(TransactionalClass.class, Component.class); assertNull("Should not find @Component on TransactionalClass", component); } @Test public void findClassAnnotationOnMetaCycleAnnotatedClassWithMissingTargetMetaAnnotation() { - Component component = AnnotationUtils.findAnnotation(MetaCycleAnnotatedClass.class, Component.class); + Component component = findAnnotation(MetaCycleAnnotatedClass.class, Component.class); assertNull("Should not find @Component on MetaCycleAnnotatedClass", component); } /** @since 4.2 */ @Test public void findClassAnnotationOnInheritedAnnotationInterface() { - Transactional tx = AnnotationUtils.findAnnotation(InheritedAnnotationInterface.class, Transactional.class); + Transactional tx = findAnnotation(InheritedAnnotationInterface.class, Transactional.class); assertNotNull("Should find @Transactional on InheritedAnnotationInterface", tx); } /** @since 4.2 */ @Test public void findClassAnnotationOnSubInheritedAnnotationInterface() { - Transactional tx = AnnotationUtils.findAnnotation(SubInheritedAnnotationInterface.class, Transactional.class); + Transactional tx = findAnnotation(SubInheritedAnnotationInterface.class, Transactional.class); assertNotNull("Should find @Transactional on SubInheritedAnnotationInterface", tx); } /** @since 4.2 */ @Test public void findClassAnnotationOnSubSubInheritedAnnotationInterface() { - Transactional tx = AnnotationUtils.findAnnotation(SubSubInheritedAnnotationInterface.class, Transactional.class); + Transactional tx = findAnnotation(SubSubInheritedAnnotationInterface.class, Transactional.class); assertNotNull("Should find @Transactional on SubSubInheritedAnnotationInterface", tx); } /** @since 4.2 */ @Test public void findClassAnnotationOnNonInheritedAnnotationInterface() { - Order order = AnnotationUtils.findAnnotation(NonInheritedAnnotationInterface.class, Order.class); + Order order = findAnnotation(NonInheritedAnnotationInterface.class, Order.class); assertNotNull("Should find @Order on NonInheritedAnnotationInterface", order); } /** @since 4.2 */ @Test public void findClassAnnotationOnSubNonInheritedAnnotationInterface() { - Order order = AnnotationUtils.findAnnotation(SubNonInheritedAnnotationInterface.class, Order.class); + Order order = findAnnotation(SubNonInheritedAnnotationInterface.class, Order.class); assertNotNull("Should find @Order on SubNonInheritedAnnotationInterface", order); } /** @since 4.2 */ @Test public void findClassAnnotationOnSubSubNonInheritedAnnotationInterface() { - Order order = AnnotationUtils.findAnnotation(SubSubNonInheritedAnnotationInterface.class, Order.class); + Order order = findAnnotation(SubSubNonInheritedAnnotationInterface.class, Order.class); assertNotNull("Should find @Order on SubSubNonInheritedAnnotationInterface", order); } @@ -375,13 +381,53 @@ public class AnnotationUtilsTests { assertFalse(isAnnotationInherited(Order.class, SubNonInheritedAnnotationClass.class)); } + @Test + public void getAnnotationAttributesWithoutAttributeAliases() { + Component component = WebController.class.getAnnotation(Component.class); + assertNotNull(component); + + AnnotationAttributes attributes = (AnnotationAttributes) getAnnotationAttributes(component); + assertNotNull(attributes); + assertEquals("value attribute: ", "webController", attributes.getString(VALUE)); + assertEquals(Component.class, attributes.annotationType()); + } + + @Test + public void getAnnotationAttributesWithAttributeAliases() throws Exception { + Method method = WebController.class.getMethod("handleMappedWithValueAttribute"); + WebMapping webMapping = method.getAnnotation(WebMapping.class); + AnnotationAttributes attributes = (AnnotationAttributes) getAnnotationAttributes(webMapping); + assertNotNull(attributes); + assertEquals(WebMapping.class, attributes.annotationType()); + assertEquals("name attribute: ", "foo", attributes.getString("name")); + assertEquals("value attribute: ", "/test", attributes.getString(VALUE)); + assertEquals("path attribute: ", "/test", attributes.getString("path")); + + method = WebController.class.getMethod("handleMappedWithPathAttribute"); + webMapping = method.getAnnotation(WebMapping.class); + attributes = (AnnotationAttributes) getAnnotationAttributes(webMapping); + assertNotNull(attributes); + assertEquals(WebMapping.class, attributes.annotationType()); + assertEquals("name attribute: ", "bar", attributes.getString("name")); + assertEquals("value attribute: ", "/test", attributes.getString(VALUE)); + assertEquals("path attribute: ", "/test", attributes.getString("path")); + + method = WebController.class.getMethod("handleMappedWithPathValueAndAttributes"); + webMapping = method.getAnnotation(WebMapping.class); + exception.expect(AnnotationConfigurationException.class); + exception.expectMessage(containsString("attribute [value] and its alias [path]")); + exception.expectMessage(containsString("values of [/enigma] and [/test]")); + exception.expectMessage(containsString("but only one declaration is permitted")); + getAnnotationAttributes(webMapping); + } + @Test public void getValueFromAnnotation() throws Exception { Method method = SimpleFoo.class.getMethod("something", Object.class); Order order = findAnnotation(method, Order.class); - assertEquals(1, AnnotationUtils.getValue(order, AnnotationUtils.VALUE)); - assertEquals(1, AnnotationUtils.getValue(order)); + assertEquals(1, getValue(order, VALUE)); + assertEquals(1, getValue(order)); } @Test @@ -391,8 +437,8 @@ public class AnnotationUtilsTests { Annotation annotation = declaredAnnotations[0]; assertNotNull(annotation); assertEquals("NonPublicAnnotation", annotation.annotationType().getSimpleName()); - assertEquals(42, AnnotationUtils.getValue(annotation, AnnotationUtils.VALUE)); - assertEquals(42, AnnotationUtils.getValue(annotation)); + assertEquals(42, getValue(annotation, VALUE)); + assertEquals(42, getValue(annotation)); } @Test @@ -400,8 +446,8 @@ public class AnnotationUtilsTests { Method method = SimpleFoo.class.getMethod("something", Object.class); Order order = findAnnotation(method, Order.class); - assertEquals(Ordered.LOWEST_PRECEDENCE, AnnotationUtils.getDefaultValue(order, AnnotationUtils.VALUE)); - assertEquals(Ordered.LOWEST_PRECEDENCE, AnnotationUtils.getDefaultValue(order)); + assertEquals(Ordered.LOWEST_PRECEDENCE, getDefaultValue(order, VALUE)); + assertEquals(Ordered.LOWEST_PRECEDENCE, getDefaultValue(order)); } @Test @@ -411,14 +457,14 @@ public class AnnotationUtilsTests { Annotation annotation = declaredAnnotations[0]; assertNotNull(annotation); assertEquals("NonPublicAnnotation", annotation.annotationType().getSimpleName()); - assertEquals(-1, AnnotationUtils.getDefaultValue(annotation, AnnotationUtils.VALUE)); - assertEquals(-1, AnnotationUtils.getDefaultValue(annotation)); + assertEquals(-1, getDefaultValue(annotation, VALUE)); + assertEquals(-1, getDefaultValue(annotation)); } @Test public void getDefaultValueFromAnnotationType() throws Exception { - assertEquals(Ordered.LOWEST_PRECEDENCE, AnnotationUtils.getDefaultValue(Order.class, AnnotationUtils.VALUE)); - assertEquals(Ordered.LOWEST_PRECEDENCE, AnnotationUtils.getDefaultValue(Order.class)); + assertEquals(Ordered.LOWEST_PRECEDENCE, getDefaultValue(Order.class, VALUE)); + assertEquals(Ordered.LOWEST_PRECEDENCE, getDefaultValue(Order.class)); } @Test @@ -431,14 +477,174 @@ public class AnnotationUtilsTests { @Test public void getRepeatableFromMethod() 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()); + Set annotations = getRepeatableAnnotation(method, MyRepeatableContainer.class, MyRepeatable.class); + assertNotNull(annotations); + List values = annotations.stream().map(MyRepeatable::value).collect(Collectors.toList()); + assertThat(values, equalTo(Arrays.asList("a", "b", "c", "meta"))); + } + + @Test + public void getRepeatableWithAttributeAliases() throws Exception { + Set annotations = getRepeatableAnnotation(TestCase.class, Hierarchy.class, ContextConfig.class); + assertNotNull(annotations); + + List locations = annotations.stream().map(ContextConfig::locations).collect(Collectors.toList()); + assertThat(locations, equalTo(Arrays.asList("A", "B"))); + + List values = annotations.stream().map(ContextConfig::value).collect(Collectors.toList()); + assertThat(values, equalTo(Arrays.asList("A", "B"))); + } + + @Test + public void getAliasedAttributeNameFromAliasedComposedAnnotation() throws Exception { + Method attribute = AliasedComposedContextConfig.class.getDeclaredMethod("xmlConfigFile"); + assertEquals("locations", getAliasedAttributeName(attribute, ContextConfig.class)); + } + + @Test + public void synthesizeAnnotationWithoutAttributeAliases() throws Exception { + Component component = findAnnotation(WebController.class, Component.class); + assertNotNull(component); + Component synthesizedComponent = synthesizeAnnotation(component); + assertNotNull(synthesizedComponent); + assertSame(component, synthesizedComponent); + assertEquals("value attribute: ", "webController", synthesizedComponent.value()); + } + + @Test + public void synthesizeAnnotationWithAttributeAliasForNonexistentAttribute() throws Exception { + AliasForNonexistentAttribute annotation = AliasForNonexistentAttributeClass.class.getAnnotation(AliasForNonexistentAttribute.class); + exception.expect(AnnotationConfigurationException.class); + exception.expectMessage(containsString("Attribute [foo] in")); + exception.expectMessage(containsString(AliasForNonexistentAttribute.class.getName())); + exception.expectMessage(containsString("is declared as an @AliasFor nonexistent attribute [bar]")); + synthesizeAnnotation(annotation); + } + + @Test + public void synthesizeAnnotationWithAttributeAliasWithoutMirroredAliasFor() throws Exception { + AliasForWithoutMirroredAliasFor annotation = AliasForWithoutMirroredAliasForClass.class.getAnnotation(AliasForWithoutMirroredAliasFor.class); + exception.expect(AnnotationConfigurationException.class); + exception.expectMessage(containsString("Attribute [bar] in")); + exception.expectMessage(containsString(AliasForWithoutMirroredAliasFor.class.getName())); + exception.expectMessage(containsString("must be declared as an @AliasFor [foo]")); + synthesizeAnnotation(annotation); + } + + @Test + public void synthesizeAnnotationWithAttributeAliasWithMirroredAliasForWrongAttribute() throws Exception { + AliasForWithMirroredAliasForWrongAttribute annotation = AliasForWithMirroredAliasForWrongAttributeClass.class.getAnnotation(AliasForWithMirroredAliasForWrongAttribute.class); + + // Since JDK 7+ does not guarantee consistent ordering of methods returned using + // reflection, we cannot make the test dependent on any specific ordering. + // + // In other words, we can't be certain which type of exception message we'll get, + // so we allow for both possibilities. + exception.expect(AnnotationConfigurationException.class); + exception.expectMessage(containsString("Attribute [bar] in")); + exception.expectMessage(containsString(AliasForWithMirroredAliasForWrongAttribute.class.getName())); + exception.expectMessage(either(containsString("must be declared as an @AliasFor [foo], not [quux]")). + or(containsString("is declared as an @AliasFor nonexistent attribute [quux]"))); + synthesizeAnnotation(annotation); + } + + @Test + public void synthesizeAnnotationWithAttributeAliasForAttributeOfDifferentType() throws Exception { + AliasForAttributeOfDifferentType annotation = AliasForAttributeOfDifferentTypeClass.class.getAnnotation(AliasForAttributeOfDifferentType.class); + exception.expect(AnnotationConfigurationException.class); + exception.expectMessage(startsWith("Misconfigured aliases")); + exception.expectMessage(containsString(AliasForAttributeOfDifferentType.class.getName())); + // Since JDK 7+ does not guarantee consistent ordering of methods returned using + // reflection, we cannot make the test dependent on any specific ordering. + // + // In other words, we don't know if "foo" or "bar" will come first. + exception.expectMessage(containsString("attribute [foo]")); + exception.expectMessage(containsString("attribute [bar]")); + exception.expectMessage(containsString("must declare the same return type")); + synthesizeAnnotation(annotation); + } + + @Test + public void synthesizeAnnotationWithAttributeAliasForWithMissingDefaultValues() throws Exception { + AliasForWithMissingDefaultValues annotation = AliasForWithMissingDefaultValuesClass.class.getAnnotation(AliasForWithMissingDefaultValues.class); + exception.expectMessage(startsWith("Misconfigured aliases")); + exception.expectMessage(containsString(AliasForWithMissingDefaultValues.class.getName())); + // Since JDK 7+ does not guarantee consistent ordering of methods returned using + // reflection, we cannot make the test dependent on any specific ordering. + // + // In other words, we don't know if "foo" or "bar" will come first. + exception.expectMessage(containsString("attribute [foo]")); + exception.expectMessage(containsString("attribute [bar]")); + exception.expectMessage(containsString("must declare default values")); + synthesizeAnnotation(annotation); + } + + @Test + public void synthesizeAnnotationWithAttributeAliasForAttributeWithDifferentDefaultValue() throws Exception { + AliasForAttributeWithDifferentDefaultValue annotation = AliasForAttributeWithDifferentDefaultValueClass.class.getAnnotation(AliasForAttributeWithDifferentDefaultValue.class); + exception.expectMessage(startsWith("Misconfigured aliases")); + exception.expectMessage(containsString(AliasForAttributeWithDifferentDefaultValue.class.getName())); + // Since JDK 7+ does not guarantee consistent ordering of methods returned using + // reflection, we cannot make the test dependent on any specific ordering. + // + // In other words, we don't know if "foo" or "bar" will come first. + exception.expectMessage(containsString("attribute [foo]")); + exception.expectMessage(containsString("attribute [bar]")); + exception.expectMessage(containsString("must declare the same default value")); + synthesizeAnnotation(annotation); + } + + @Test + public void synthesizeAnnotationWithAttributeAliases() throws Exception { + Method method = WebController.class.getMethod("handleMappedWithValueAttribute"); + WebMapping webMapping = method.getAnnotation(WebMapping.class); + assertNotNull(webMapping); + WebMapping synthesizedWebMapping = synthesizeAnnotation(webMapping); + assertNotSame(webMapping, synthesizedWebMapping); + assertThat(synthesizedWebMapping, instanceOf(SynthesizedAnnotation.class)); + + assertNotNull(synthesizedWebMapping); + assertEquals("name attribute: ", "foo", synthesizedWebMapping.name()); + assertEquals("aliased path attribute: ", "/test", synthesizedWebMapping.path()); + assertEquals("actual value attribute: ", "/test", synthesizedWebMapping.value()); + } + + @Test + public void synthesizeAnnotationWithAttributeAliasesInNestedAnnotations() throws Exception { + Hierarchy hierarchy = TestCase.class.getAnnotation(Hierarchy.class); + assertNotNull(hierarchy); + Hierarchy synthesizedHierarchy = synthesizeAnnotation(hierarchy); + assertNotSame(hierarchy, synthesizedHierarchy); + assertThat(synthesizedHierarchy, instanceOf(SynthesizedAnnotation.class)); + + ContextConfig[] configs = synthesizedHierarchy.value(); + assertNotNull(configs); + for (ContextConfig contextConfig : configs) { + assertThat(contextConfig, instanceOf(SynthesizedAnnotation.class)); } - assertThat(values, equalTo((Set) new HashSet( - Arrays.asList("a", "b", "c", "meta")))); + + List locations = Arrays.stream(configs).map(ContextConfig::locations).collect(Collectors.toList()); + assertThat(locations, equalTo(Arrays.asList("A", "B"))); + + List values = Arrays.stream(configs).map(ContextConfig::value).collect(Collectors.toList()); + assertThat(values, equalTo(Arrays.asList("A", "B"))); + } + + @Test + public void synthesizeAlreadySynthesizedAnnotation() throws Exception { + Method method = WebController.class.getMethod("handleMappedWithValueAttribute"); + WebMapping webMapping = method.getAnnotation(WebMapping.class); + assertNotNull(webMapping); + WebMapping synthesizedWebMapping = synthesizeAnnotation(webMapping); + assertNotSame(webMapping, synthesizedWebMapping); + WebMapping synthesizedAgainWebMapping = synthesizeAnnotation(synthesizedWebMapping); + assertSame(synthesizedWebMapping, synthesizedAgainWebMapping); + assertThat(synthesizedAgainWebMapping, instanceOf(SynthesizedAnnotation.class)); + + assertNotNull(synthesizedAgainWebMapping); + assertEquals("name attribute: ", "foo", synthesizedAgainWebMapping.name()); + assertEquals("aliased path attribute: ", "/test", synthesizedAgainWebMapping.path()); + assertEquals("actual value attribute: ", "/test", synthesizedAgainWebMapping.value()); } @@ -710,4 +916,149 @@ public class AnnotationUtilsTests { void foo(); } + /** + * Mock of {@link org.springframework.web.bind.annotation.RequestMapping}. + */ + @Retention(RetentionPolicy.RUNTIME) + @interface WebMapping { + + String name(); + + @AliasFor(attribute = "path") + String value() default ""; + + @AliasFor(attribute = "value") + String path() default ""; + } + + @Component("webController") + static class WebController { + + @WebMapping(value = "/test", name = "foo") + public void handleMappedWithValueAttribute() { + } + + @WebMapping(path = "/test", name = "bar") + public void handleMappedWithPathAttribute() { + } + + @WebMapping(value = "/enigma", path = "/test", name = "baz") + public void handleMappedWithPathValueAndAttributes() { + } + } + + /** + * Mock of {@link org.springframework.test.context.ContextConfiguration}. + */ + @Retention(RetentionPolicy.RUNTIME) + static @interface ContextConfig { + + @AliasFor(attribute = "locations") + String value() default ""; + + @AliasFor(attribute = "value") + String locations() default ""; + } + + /** + * Mock of {@link org.springframework.test.context.ContextHierarchy}. + */ + @Retention(RetentionPolicy.RUNTIME) + static @interface Hierarchy { + + ContextConfig[] value(); + } + + @Hierarchy({ @ContextConfig("A"), @ContextConfig(locations = "B") }) + static class TestCase { + } + + @Retention(RetentionPolicy.RUNTIME) + static @interface AliasForNonexistentAttribute { + + @AliasFor(attribute = "bar") + String foo() default ""; + } + + @AliasForNonexistentAttribute + static class AliasForNonexistentAttributeClass { + } + + @Retention(RetentionPolicy.RUNTIME) + static @interface AliasForWithoutMirroredAliasFor { + + @AliasFor(attribute = "bar") + String foo() default ""; + + String bar() default ""; + } + + @AliasForWithoutMirroredAliasFor + static class AliasForWithoutMirroredAliasForClass { + } + + @Retention(RetentionPolicy.RUNTIME) + static @interface AliasForWithMirroredAliasForWrongAttribute { + + @AliasFor(attribute = "bar") + String[] foo() default ""; + + @AliasFor(attribute = "quux") + String[] bar() default ""; + } + + @AliasForWithMirroredAliasForWrongAttribute + static class AliasForWithMirroredAliasForWrongAttributeClass { + } + + @Retention(RetentionPolicy.RUNTIME) + static @interface AliasForAttributeOfDifferentType { + + @AliasFor(attribute = "bar") + String[] foo() default ""; + + @AliasFor(attribute = "foo") + boolean bar() default true; + } + + @AliasForAttributeOfDifferentType + static class AliasForAttributeOfDifferentTypeClass { + } + + @Retention(RetentionPolicy.RUNTIME) + static @interface AliasForWithMissingDefaultValues { + + @AliasFor(attribute = "bar") + String foo(); + + @AliasFor(attribute = "foo") + String bar(); + } + + @AliasForWithMissingDefaultValues(foo = "foo", bar = "bar") + static class AliasForWithMissingDefaultValuesClass { + } + + @Retention(RetentionPolicy.RUNTIME) + static @interface AliasForAttributeWithDifferentDefaultValue { + + @AliasFor(attribute = "bar") + String foo() default "X"; + + @AliasFor(attribute = "foo") + String bar() default "Z"; + } + + @AliasForAttributeWithDifferentDefaultValue + static class AliasForAttributeWithDifferentDefaultValueClass { + } + + @ContextConfig + @Retention(RetentionPolicy.RUNTIME) + static @interface AliasedComposedContextConfig { + + @AliasFor(annotation = ContextConfig.class, attribute = "locations") + String xmlConfigFile(); + } + } diff --git a/spring-test/src/main/java/org/springframework/test/context/ActiveProfiles.java b/spring-test/src/main/java/org/springframework/test/context/ActiveProfiles.java index c0f73c3fe5..8511bb9da8 100644 --- a/spring-test/src/main/java/org/springframework/test/context/ActiveProfiles.java +++ b/spring-test/src/main/java/org/springframework/test/context/ActiveProfiles.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 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. @@ -23,6 +23,8 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.springframework.core.annotation.AliasFor; + /** * {@code ActiveProfiles} is a class-level annotation that is used to declare * which active bean definition profiles should be used when loading @@ -53,6 +55,7 @@ public @interface ActiveProfiles { *

This attribute may not be used in conjunction with * {@link #profiles}, but it may be used instead of {@link #profiles}. */ + @AliasFor(attribute = "profiles") String[] value() default {}; /** @@ -61,6 +64,7 @@ public @interface ActiveProfiles { *

This attribute may not be used in conjunction with * {@link #value}, but it may be used instead of {@link #value}. */ + @AliasFor(attribute = "value") String[] profiles() default {}; /** diff --git a/spring-test/src/main/java/org/springframework/test/context/ContextConfiguration.java b/spring-test/src/main/java/org/springframework/test/context/ContextConfiguration.java index 7719589b8b..b9e15f749c 100644 --- a/spring-test/src/main/java/org/springframework/test/context/ContextConfiguration.java +++ b/spring-test/src/main/java/org/springframework/test/context/ContextConfiguration.java @@ -25,6 +25,7 @@ import java.lang.annotation.Target; import org.springframework.context.ApplicationContextInitializer; import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.annotation.AliasFor; /** * {@code @ContextConfiguration} defines class-level metadata that is used to determine @@ -97,6 +98,7 @@ public @interface ContextConfiguration { * @since 3.0 * @see #inheritLocations */ + @AliasFor(attribute = "locations") String[] value() default {}; /** @@ -127,6 +129,7 @@ public @interface ContextConfiguration { * @since 2.5 * @see #inheritLocations */ + @AliasFor(attribute = "value") String[] locations() default {}; /** diff --git a/spring-test/src/main/java/org/springframework/test/context/ContextConfigurationAttributes.java b/spring-test/src/main/java/org/springframework/test/context/ContextConfigurationAttributes.java index db36807fe8..0e0feb47f9 100644 --- a/spring-test/src/main/java/org/springframework/test/context/ContextConfigurationAttributes.java +++ b/spring-test/src/main/java/org/springframework/test/context/ContextConfigurationAttributes.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 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. @@ -68,7 +68,7 @@ public class ContextConfigurationAttributes { * @param contextConfiguration the annotation from which to retrieve the attributes */ public ContextConfigurationAttributes(Class declaringClass, ContextConfiguration contextConfiguration) { - this(declaringClass, resolveLocations(declaringClass, contextConfiguration), contextConfiguration.classes(), + this(declaringClass, contextConfiguration.locations(), contextConfiguration.classes(), contextConfiguration.inheritLocations(), contextConfiguration.initializers(), contextConfiguration.inheritInitializers(), contextConfiguration.name(), contextConfiguration.loader()); } @@ -83,12 +83,9 @@ public class ContextConfigurationAttributes { */ @SuppressWarnings("unchecked") public ContextConfigurationAttributes(Class declaringClass, AnnotationAttributes annAttrs) { - this(declaringClass, - resolveLocations(declaringClass, annAttrs.getStringArray("locations"), annAttrs.getStringArray("value")), - annAttrs.getClassArray("classes"), annAttrs.getBoolean("inheritLocations"), - (Class>[]) annAttrs.getClassArray("initializers"), - annAttrs.getBoolean("inheritInitializers"), annAttrs.getString("name"), - (Class) annAttrs.getClass("loader")); + this(declaringClass, annAttrs.getStringArray("locations"), annAttrs.getClassArray("classes"), annAttrs.getBoolean("inheritLocations"), + (Class>[]) annAttrs.getClassArray("initializers"), + annAttrs.getBoolean("inheritInitializers"), annAttrs.getString("name"), (Class) annAttrs.getClass("loader")); } /** @@ -159,37 +156,6 @@ public class ContextConfigurationAttributes { } - /** - * Resolve resource locations from the {@link ContextConfiguration#locations() locations} - * and {@link ContextConfiguration#value() value} attributes of the supplied - * {@link ContextConfiguration} annotation. - * @throws IllegalStateException if both the locations and value attributes have been declared - */ - private static String[] resolveLocations(Class declaringClass, ContextConfiguration contextConfiguration) { - return resolveLocations(declaringClass, contextConfiguration.locations(), contextConfiguration.value()); - } - - /** - * Resolve resource locations from the supplied {@code locations} and - * {@code value} arrays, which correspond to attributes of the same names in - * the {@link ContextConfiguration} annotation. - * @throws IllegalStateException if both the locations and value attributes have been declared - */ - private static String[] resolveLocations(Class declaringClass, String[] locations, String[] value) { - Assert.notNull(declaringClass, "declaringClass must not be null"); - if (!ObjectUtils.isEmpty(value) && !ObjectUtils.isEmpty(locations)) { - throw new IllegalStateException(String.format("Test class [%s] has been configured with " + - "@ContextConfiguration's 'value' %s and 'locations' %s attributes. Only one declaration " + - "of resource locations is permitted per @ContextConfiguration annotation.", - declaringClass.getName(), ObjectUtils.nullSafeToString(value), ObjectUtils.nullSafeToString(locations))); - } - else if (!ObjectUtils.isEmpty(value)) { - locations = value; - } - return locations; - } - - /** * Get the {@linkplain Class class} that declared the * {@link ContextConfiguration @ContextConfiguration} annotation. diff --git a/spring-test/src/main/java/org/springframework/test/context/TestExecutionListeners.java b/spring-test/src/main/java/org/springframework/test/context/TestExecutionListeners.java index 0258990d1b..3f67b6d1d9 100644 --- a/spring-test/src/main/java/org/springframework/test/context/TestExecutionListeners.java +++ b/spring-test/src/main/java/org/springframework/test/context/TestExecutionListeners.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 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. @@ -23,6 +23,8 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.springframework.core.annotation.AliasFor; + /** * {@code TestExecutionListeners} defines class-level metadata for configuring * which {@link TestExecutionListener TestExecutionListeners} should be @@ -85,6 +87,7 @@ public @interface TestExecutionListeners { *

This attribute may not be used in conjunction with * {@link #listeners}, but it may be used instead of {@link #listeners}. */ + @AliasFor(attribute = "listeners") Class[] value() default {}; /** @@ -100,6 +103,7 @@ public @interface TestExecutionListeners { * @see org.springframework.test.context.transaction.TransactionalTestExecutionListener * @see org.springframework.test.context.jdbc.SqlScriptsTestExecutionListener */ + @AliasFor(attribute = "value") Class[] listeners() default {}; /** diff --git a/spring-test/src/main/java/org/springframework/test/context/TestPropertySource.java b/spring-test/src/main/java/org/springframework/test/context/TestPropertySource.java index 248fd7f9f7..fad1dc65de 100644 --- a/spring-test/src/main/java/org/springframework/test/context/TestPropertySource.java +++ b/spring-test/src/main/java/org/springframework/test/context/TestPropertySource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 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. @@ -23,6 +23,8 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.springframework.core.annotation.AliasFor; + /** * {@code @TestPropertySource} is a class-level annotation that is used to * configure the {@link #locations} of properties files and inlined @@ -94,6 +96,7 @@ public @interface TestPropertySource { * * @see #locations */ + @AliasFor(attribute = "locations") String[] value() default {}; /** @@ -141,6 +144,7 @@ public @interface TestPropertySource { * @see #properties * @see org.springframework.core.env.PropertySource */ + @AliasFor(attribute = "value") String[] locations() default {}; /** diff --git a/spring-test/src/main/java/org/springframework/test/context/jdbc/Sql.java b/spring-test/src/main/java/org/springframework/test/context/jdbc/Sql.java index ef63713c5f..ae11d7d5be 100644 --- a/spring-test/src/main/java/org/springframework/test/context/jdbc/Sql.java +++ b/spring-test/src/main/java/org/springframework/test/context/jdbc/Sql.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 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. @@ -22,6 +22,8 @@ import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.Target; +import org.springframework.core.annotation.AliasFor; + import static java.lang.annotation.ElementType.*; import static java.lang.annotation.RetentionPolicy.*; @@ -93,6 +95,7 @@ public @interface Sql { *

This attribute may not be used in conjunction with * {@link #scripts}, but it may be used instead of {@link #scripts}. */ + @AliasFor(attribute = "scripts") String[] value() default {}; /** @@ -126,6 +129,7 @@ public @interface Sql { * {@code "classpath:com/example/MyTest.testMethod.sql"}. * */ + @AliasFor(attribute = "value") String[] scripts() default {}; /** diff --git a/spring-test/src/main/java/org/springframework/test/context/jdbc/SqlScriptsTestExecutionListener.java b/spring-test/src/main/java/org/springframework/test/context/jdbc/SqlScriptsTestExecutionListener.java index 348a34d0b4..2201edcca7 100644 --- a/spring-test/src/main/java/org/springframework/test/context/jdbc/SqlScriptsTestExecutionListener.java +++ b/spring-test/src/main/java/org/springframework/test/context/jdbc/SqlScriptsTestExecutionListener.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 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. @@ -255,24 +255,6 @@ public class SqlScriptsTestExecutionListener extends AbstractTestExecutionListen private String[] getScripts(Sql sql, TestContext testContext, boolean classLevel) { String[] scripts = sql.scripts(); - String[] value = sql.value(); - boolean scriptsDeclared = !ObjectUtils.isEmpty(scripts); - boolean valueDeclared = !ObjectUtils.isEmpty(value); - - if (valueDeclared && scriptsDeclared) { - String elementType = (classLevel ? "class" : "method"); - String elementName = (classLevel ? testContext.getTestClass().getName() - : testContext.getTestMethod().toString()); - String msg = String.format("Test %s [%s] has been configured with @Sql's 'value' [%s] " - + "and 'scripts' [%s] attributes. Only one declaration of SQL script " - + "paths is permitted per @Sql annotation.", elementType, elementName, - ObjectUtils.nullSafeToString(value), ObjectUtils.nullSafeToString(scripts)); - logger.error(msg); - throw new IllegalStateException(msg); - } - if (valueDeclared) { - scripts = value; - } if (ObjectUtils.isEmpty(scripts)) { scripts = new String[] { detectDefaultScript(testContext, classLevel) }; } diff --git a/spring-test/src/main/java/org/springframework/test/context/support/AbstractTestContextBootstrapper.java b/spring-test/src/main/java/org/springframework/test/context/support/AbstractTestContextBootstrapper.java index bdbc08814f..e10cd78fb6 100644 --- a/spring-test/src/main/java/org/springframework/test/context/support/AbstractTestContextBootstrapper.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/AbstractTestContextBootstrapper.java @@ -53,7 +53,6 @@ import org.springframework.test.util.MetaAnnotationUtils; import org.springframework.test.util.MetaAnnotationUtils.AnnotationDescriptor; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; -import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; /** @@ -147,18 +146,7 @@ public abstract class AbstractTestContextBootstrapper implements TestContextBoot declaringClass.getName())); } - Class[] valueListenerClasses = (Class[]) annAttrs.getClassArray("value"); Class[] listenerClasses = (Class[]) annAttrs.getClassArray("listeners"); - if (!ObjectUtils.isEmpty(valueListenerClasses) && !ObjectUtils.isEmpty(listenerClasses)) { - throw new IllegalStateException(String.format( - "Class [%s] configured with @TestExecutionListeners' " - + "'value' [%s] and 'listeners' [%s] attributes. Use one or the other, but not both.", - declaringClass.getName(), ObjectUtils.nullSafeToString(valueListenerClasses), - ObjectUtils.nullSafeToString(listenerClasses))); - } - else if (!ObjectUtils.isEmpty(valueListenerClasses)) { - listenerClasses = valueListenerClasses; - } boolean inheritListeners = annAttrs.getBoolean("inheritListeners"); AnnotationDescriptor superDescriptor = MetaAnnotationUtils.findAnnotationDescriptor( diff --git a/spring-test/src/main/java/org/springframework/test/context/support/ActiveProfilesUtils.java b/spring-test/src/main/java/org/springframework/test/context/support/ActiveProfilesUtils.java index 68508b54db..57cc51496e 100644 --- a/spring-test/src/main/java/org/springframework/test/context/support/ActiveProfilesUtils.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/ActiveProfilesUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 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. @@ -29,7 +29,6 @@ import org.springframework.test.context.ActiveProfilesResolver; import org.springframework.test.util.MetaAnnotationUtils; import org.springframework.test.util.MetaAnnotationUtils.AnnotationDescriptor; import org.springframework.util.Assert; -import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; /** @@ -94,7 +93,6 @@ abstract class ActiveProfilesUtils { logger.trace(String.format("Retrieved @ActiveProfiles attributes [%s] for declaring class [%s].", annAttrs, declaringClass.getName())); } - validateActiveProfilesConfiguration(declaringClass, annAttrs); Class resolverClass = annAttrs.getClass("resolver"); if (ActiveProfilesResolver.class == resolverClass) { @@ -134,20 +132,4 @@ abstract class ActiveProfilesUtils { return StringUtils.toStringArray(activeProfiles); } - private static void validateActiveProfilesConfiguration(Class declaringClass, AnnotationAttributes annAttrs) { - String[] valueProfiles = annAttrs.getStringArray("value"); - String[] profiles = annAttrs.getStringArray("profiles"); - boolean valueDeclared = !ObjectUtils.isEmpty(valueProfiles); - boolean profilesDeclared = !ObjectUtils.isEmpty(profiles); - - if (valueDeclared && profilesDeclared) { - String msg = String.format("Class [%s] has been configured with @ActiveProfiles' 'value' [%s] " - + "and 'profiles' [%s] attributes. Only one declaration of active bean " - + "definition profiles is permitted per @ActiveProfiles annotation.", declaringClass.getName(), - ObjectUtils.nullSafeToString(valueProfiles), ObjectUtils.nullSafeToString(profiles)); - logger.error(msg); - throw new IllegalStateException(msg); - } - } - } diff --git a/spring-test/src/main/java/org/springframework/test/context/support/DefaultActiveProfilesResolver.java b/spring-test/src/main/java/org/springframework/test/context/support/DefaultActiveProfilesResolver.java index 7651d3dac0..36610422ec 100644 --- a/spring-test/src/main/java/org/springframework/test/context/support/DefaultActiveProfilesResolver.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/DefaultActiveProfilesResolver.java @@ -26,7 +26,6 @@ import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.ActiveProfilesResolver; import org.springframework.util.Assert; -import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; import static org.springframework.test.util.MetaAnnotationUtils.*; @@ -81,25 +80,7 @@ public class DefaultActiveProfilesResolver implements ActiveProfilesResolver { annAttrs, declaringClass.getName())); } - String[] profiles = annAttrs.getStringArray("profiles"); - String[] valueProfiles = annAttrs.getStringArray("value"); - boolean valueDeclared = !ObjectUtils.isEmpty(valueProfiles); - boolean profilesDeclared = !ObjectUtils.isEmpty(profiles); - - if (valueDeclared && profilesDeclared) { - String msg = String.format("Class [%s] has been configured with @ActiveProfiles' 'value' [%s] " - + "and 'profiles' [%s] attributes. Only one declaration of active bean " - + "definition profiles is permitted per @ActiveProfiles annotation.", declaringClass.getName(), - ObjectUtils.nullSafeToString(valueProfiles), ObjectUtils.nullSafeToString(profiles)); - logger.error(msg); - throw new IllegalStateException(msg); - } - - if (valueDeclared) { - profiles = valueProfiles; - } - - for (String profile : profiles) { + for (String profile : annAttrs.getStringArray("profiles")) { if (StringUtils.hasText(profile)) { activeProfiles.add(profile.trim()); } diff --git a/spring-test/src/main/java/org/springframework/test/context/support/TestPropertySourceAttributes.java b/spring-test/src/main/java/org/springframework/test/context/support/TestPropertySourceAttributes.java index 338bd202ba..041362a7bd 100644 --- a/spring-test/src/main/java/org/springframework/test/context/support/TestPropertySourceAttributes.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/TestPropertySourceAttributes.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 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. @@ -67,8 +67,7 @@ class TestPropertySourceAttributes { * @param annAttrs the annotation attributes from which to retrieve the attributes */ TestPropertySourceAttributes(Class declaringClass, AnnotationAttributes annAttrs) { - this(declaringClass, resolveLocations(declaringClass, annAttrs.getStringArray("locations"), - annAttrs.getStringArray("value")), annAttrs.getBoolean("inheritLocations"), + this(declaringClass, annAttrs.getStringArray("locations"), annAttrs.getBoolean("inheritLocations"), annAttrs.getStringArray("properties"), annAttrs.getBoolean("inheritProperties")); } @@ -156,31 +155,6 @@ class TestPropertySourceAttributes { .toString(); } - /** - * Resolve resource locations from the supplied {@code locations} and - * {@code value} arrays, which correspond to attributes of the same names in - * the {@link TestPropertySource} annotation. - * - * @throws IllegalStateException if both the locations and value attributes have been declared - */ - private static String[] resolveLocations(Class declaringClass, String[] locations, String[] value) { - Assert.notNull(declaringClass, "declaringClass must not be null"); - - if (!ObjectUtils.isEmpty(value) && !ObjectUtils.isEmpty(locations)) { - String msg = String.format("Class [%s] has been configured with @TestPropertySource's 'value' [%s] " - + "and 'locations' [%s] attributes. Only one declaration of resource " - + "locations is permitted per @TestPropertySource annotation.", declaringClass.getName(), - ObjectUtils.nullSafeToString(value), ObjectUtils.nullSafeToString(locations)); - logger.error(msg); - throw new IllegalStateException(msg); - } - else if (!ObjectUtils.isEmpty(value)) { - locations = value; - } - - return locations; - } - /** * Detect a default properties file for the supplied class, as specified * in the class-level Javadoc for {@link TestPropertySource}. diff --git a/spring-test/src/test/java/org/springframework/test/context/TestExecutionListenersTests.java b/spring-test/src/test/java/org/springframework/test/context/TestExecutionListenersTests.java index 28678a379d..650bd09834 100644 --- a/spring-test/src/test/java/org/springframework/test/context/TestExecutionListenersTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/TestExecutionListenersTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 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. @@ -24,6 +24,7 @@ import java.util.stream.Collectors; import org.junit.Test; import org.springframework.core.Ordered; +import org.springframework.core.annotation.AnnotationConfigurationException; import org.springframework.test.context.jdbc.SqlScriptsTestExecutionListener; import org.springframework.test.context.support.AbstractTestExecutionListener; import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; @@ -185,7 +186,7 @@ public class TestExecutionListenersTests { testContextManager.getTestExecutionListeners().size()); } - @Test(expected = IllegalStateException.class) + @Test(expected = AnnotationConfigurationException.class) public void listenersAndValueAttributesDeclared() { new TestContextManager(DuplicateListenersConfigTestCase.class); } diff --git a/spring-test/src/test/java/org/springframework/test/context/jdbc/SqlScriptsTestExecutionListenerTests.java b/spring-test/src/test/java/org/springframework/test/context/jdbc/SqlScriptsTestExecutionListenerTests.java index bbbc2b7470..a8883bab2d 100644 --- a/spring-test/src/test/java/org/springframework/test/context/jdbc/SqlScriptsTestExecutionListenerTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/jdbc/SqlScriptsTestExecutionListenerTests.java @@ -16,15 +16,20 @@ package org.springframework.test.context.jdbc; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.ExpectedException; + import org.mockito.BDDMockito; import org.springframework.beans.factory.config.AutowireCapableBeanFactory; import org.springframework.context.ApplicationContext; +import org.springframework.core.annotation.AnnotationConfigurationException; import org.springframework.core.io.Resource; import org.springframework.test.context.TestContext; import org.springframework.test.context.jdbc.SqlConfig.TransactionMode; +import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; import static org.mockito.BDDMockito.*; @@ -40,6 +45,9 @@ public class SqlScriptsTestExecutionListenerTests { private final TestContext testContext = mock(TestContext.class); + @Rule + public final ExpectedException exception = ExpectedException.none(); + @Test public void missingValueAndScriptsAtClassLevel() throws Exception { @@ -65,7 +73,14 @@ public class SqlScriptsTestExecutionListenerTests { BDDMockito.> given(testContext.getTestClass()).willReturn(clazz); given(testContext.getTestMethod()).willReturn(clazz.getDeclaredMethod("foo")); - assertExceptionContains("Only one declaration of SQL script paths is permitted"); + exception.expect(AnnotationConfigurationException.class); + exception.expectMessage(either( + containsString("attribute [value] and its alias [scripts]")).or( + containsString("attribute [scripts] and its alias [value]"))); + exception.expectMessage(either(containsString("values of [{foo}] and [{bar}]")).or( + containsString("values of [{bar}] and [{foo}]"))); + exception.expectMessage(containsString("but only one declaration is permitted")); + listener.beforeTestMethod(testContext); } @Test diff --git a/spring-test/src/test/java/org/springframework/test/context/support/ActiveProfilesUtilsTests.java b/spring-test/src/test/java/org/springframework/test/context/support/ActiveProfilesUtilsTests.java index a4a7c9081d..ceb19dde6d 100644 --- a/spring-test/src/test/java/org/springframework/test/context/support/ActiveProfilesUtilsTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/support/ActiveProfilesUtilsTests.java @@ -28,6 +28,7 @@ import java.util.Set; import org.junit.Test; +import org.springframework.core.annotation.AnnotationConfigurationException; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.ActiveProfilesResolver; import org.springframework.util.StringUtils; @@ -190,7 +191,7 @@ public class ActiveProfilesUtilsTests extends AbstractContextConfigurationUtilsT /** * @since 4.0 */ - @Test(expected = IllegalStateException.class) + @Test(expected = AnnotationConfigurationException.class) public void resolveActiveProfilesWithConflictingProfilesAndValue() { resolveActiveProfiles(ConflictingProfilesAndValueTestCase.class); } diff --git a/spring-test/src/test/java/org/springframework/test/context/support/ContextLoaderUtilsConfigurationAttributesTests.java b/spring-test/src/test/java/org/springframework/test/context/support/ContextLoaderUtilsConfigurationAttributesTests.java index 7ff4a9e991..d0fb0f7079 100644 --- a/spring-test/src/test/java/org/springframework/test/context/support/ContextLoaderUtilsConfigurationAttributesTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/support/ContextLoaderUtilsConfigurationAttributesTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 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,12 +18,16 @@ package org.springframework.test.context.support; import java.util.List; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.springframework.core.annotation.AnnotationConfigurationException; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.ContextConfigurationAttributes; import org.springframework.test.context.ContextLoader; +import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; import static org.springframework.test.context.support.ContextLoaderUtils.*; @@ -35,6 +39,10 @@ import static org.springframework.test.context.support.ContextLoaderUtils.*; */ public class ContextLoaderUtilsConfigurationAttributesTests extends AbstractContextConfigurationUtilsTests { + @Rule + public final ExpectedException exception = ExpectedException.none(); + + private void assertLocationsFooAttributes(ContextConfigurationAttributes attributes) { assertAttributes(attributes, LocationsFoo.class, new String[] { "/foo.xml" }, EMPTY_CLASS_ARRAY, ContextLoader.class, false); @@ -55,8 +63,17 @@ public class ContextLoaderUtilsConfigurationAttributesTests extends AbstractCont AnnotationConfigContextLoader.class, true); } - @Test(expected = IllegalStateException.class) + @Test public void resolveConfigAttributesWithConflictingLocations() { + exception.expect(AnnotationConfigurationException.class); + exception.expectMessage(containsString(ConflictingLocations.class.getName())); + exception.expectMessage(either( + containsString("attribute [value] and its alias [locations]")).or( + containsString("attribute [locations] and its alias [value]"))); + exception.expectMessage(either( + containsString("values of [{x}] and [{y}]")).or( + containsString("values of [{y}] and [{x}]"))); + exception.expectMessage(containsString("but only one declaration is permitted")); resolveContextConfigurationAttributes(ConflictingLocations.class); } diff --git a/spring-test/src/test/java/org/springframework/test/context/support/TestPropertySourceUtilsTests.java b/spring-test/src/test/java/org/springframework/test/context/support/TestPropertySourceUtilsTests.java index f6ac1a4e64..8f6c8d7efb 100644 --- a/spring-test/src/test/java/org/springframework/test/context/support/TestPropertySourceUtilsTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/support/TestPropertySourceUtilsTests.java @@ -23,6 +23,7 @@ import org.junit.Test; import org.junit.rules.ExpectedException; import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.annotation.AnnotationConfigurationException; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.MutablePropertySources; import org.springframework.mock.env.MockEnvironment; @@ -81,7 +82,7 @@ public class TestPropertySourceUtilsTests { @Test public void locationsAndValueAttributes() { - expectedException.expect(IllegalStateException.class); + expectedException.expect(AnnotationConfigurationException.class); buildMergedTestPropertySources(LocationsAndValuePropertySources.class); } diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/ControllerAdvice.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/ControllerAdvice.java index 0dec918c2b..1f5e65432d 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/annotation/ControllerAdvice.java +++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/ControllerAdvice.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 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. @@ -23,6 +23,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.springframework.core.annotation.AliasFor; import org.springframework.stereotype.Component; /** @@ -50,6 +51,7 @@ import org.springframework.stereotype.Component; * * @author Rossen Stoyanchev * @author Brian Clozel + * @author Sam Brannen * @since 3.2 */ @Target(ElementType.TYPE) @@ -59,26 +61,28 @@ import org.springframework.stereotype.Component; public @interface ControllerAdvice { /** - * Alias for the {@link #basePackages()} attribute. - * Allows for more concise annotation declarations e.g.: + * Alias for the {@link #basePackages} attribute. + *

Allows for more concise annotation declarations e.g.: * {@code @ControllerAdvice("org.my.pkg")} is equivalent to * {@code @ControllerAdvice(basePackages="org.my.pkg")}. * @since 4.0 * @see #basePackages() */ + @AliasFor(attribute = "basePackages") String[] value() default {}; /** * Array of base packages. - * Controllers that belong to those base packages or sub-packages thereof + *

Controllers that belong to those base packages or sub-packages thereof * will be included, e.g.: {@code @ControllerAdvice(basePackages="org.my.pkg")} * or {@code @ControllerAdvice(basePackages={"org.my.pkg", "org.my.other.pkg"})}. - *

{@link #value()} is an alias for this attribute, simply allowing for + *

{@link #value} is an alias for this attribute, simply allowing for * more concise use of the annotation. *

Also consider using {@link #basePackageClasses()} as a type-safe * alternative to String-based package names. * @since 4.0 */ + @AliasFor(attribute = "value") String[] basePackages() default {}; /** @@ -93,7 +97,7 @@ public @interface ControllerAdvice { /** * Array of classes. - * Controllers that are assignable to at least one of the given types + *

Controllers that are assignable to at least one of the given types * will be assisted by the {@code @ControllerAdvice} annotated class. * @since 4.0 */ @@ -101,7 +105,7 @@ public @interface ControllerAdvice { /** * Array of annotations. - * Controllers that are annotated with this/one of those annotation(s) + *

Controllers that are annotated with this/one of those annotation(s) * will be assisted by the {@code @ControllerAdvice} annotated class. *

Consider creating a special annotation or use a predefined one, * like {@link RestController @RestController}. diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java index 69c86f4fd6..5431eee6e6 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java +++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java @@ -23,6 +23,8 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.util.concurrent.Callable; +import org.springframework.core.annotation.AliasFor; + /** * Annotation for mapping web requests onto specific handler classes and/or * handler methods. Provides a consistent style between Servlet and Portlet @@ -298,7 +300,7 @@ public @interface RequestMapping { /** * The primary mapping expressed by this annotation. - *

In a Servlet environment this is an alias for {@link #path()}. + *

In a Servlet environment this is an alias for {@link #path}. * For example {@code @RequestMapping("/foo")} is equivalent to * {@code @RequestMapping(path="/foo")}. *

In a Portlet environment this is the mapped portlet modes @@ -307,6 +309,7 @@ public @interface RequestMapping { * When used at the type level, all method-level mappings inherit * this primary mapping, narrowing it for a specific handler method. */ + @AliasFor(attribute = "path") String[] value() default {}; /** @@ -321,6 +324,7 @@ public @interface RequestMapping { * @see org.springframework.web.bind.annotation.ValueConstants#DEFAULT_NONE * @since 4.2 */ + @AliasFor(attribute = "value") String[] path() default {}; /** diff --git a/spring-web/src/main/java/org/springframework/web/method/ControllerAdviceBean.java b/spring-web/src/main/java/org/springframework/web/method/ControllerAdviceBean.java index 5ac8556c32..91f868987b 100644 --- a/spring-web/src/main/java/org/springframework/web/method/ControllerAdviceBean.java +++ b/spring-web/src/main/java/org/springframework/web/method/ControllerAdviceBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 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. @@ -223,11 +223,6 @@ public class ControllerAdviceBean implements Ordered { private static Set initBasePackages(ControllerAdvice annotation) { Set basePackages = new LinkedHashSet(); - for (String basePackage : annotation.value()) { - if (StringUtils.hasText(basePackage)) { - basePackages.add(adaptBasePackage(basePackage)); - } - } for (String basePackage : annotation.basePackages()) { if (StringUtils.hasText(basePackage)) { basePackages.add(adaptBasePackage(basePackage));