From 8ff9e818a53aed90da80e68f4948147f07bec394 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Mon, 22 Feb 2016 23:24:30 +0100 Subject: [PATCH] Allow for a single element overriding an array attribute in a meta-annotation Includes refinements for consistent quoting of names in exception messages. Issue: SPR-13972 --- .../core/annotation/AnnotationUtils.java | 31 ++++++------ .../MapAnnotationAttributeExtractor.java | 12 ++--- .../AnnotatedElementUtilsTests.java | 6 +-- .../core/annotation/AnnotationUtilsTests.java | 50 +++++++++---------- 4 files changed, 49 insertions(+), 50 deletions(-) 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 a5b56e7d8f..d592ac363f 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -1953,7 +1953,7 @@ public abstract class AnnotationUtils { this.aliasedAttributeName = getAliasedAttributeName(aliasFor, sourceAttribute); if (this.aliasedAnnotationType == this.sourceAnnotationType && this.aliasedAttributeName.equals(this.sourceAttributeName)) { - String msg = String.format("@AliasFor declaration on attribute [%s] in annotation [%s] points to " + + String msg = String.format("@AliasFor declaration on attribute '%s' in annotation [%s] points to " + "itself. Specify 'annotation' to point to a same-named attribute on a meta-annotation.", sourceAttribute.getName(), declaringClass.getName()); throw new AnnotationConfigurationException(msg); @@ -1963,7 +1963,7 @@ public abstract class AnnotationUtils { } catch (NoSuchMethodException ex) { String msg = String.format( - "Attribute [%s] in annotation [%s] is declared as an @AliasFor nonexistent attribute [%s] in annotation [%s].", + "Attribute '%s' in annotation [%s] is declared as an @AliasFor nonexistent attribute '%s' in annotation [%s].", this.sourceAttributeName, this.sourceAnnotationType.getName(), this.aliasedAttributeName, this.aliasedAnnotationType.getName()); throw new AnnotationConfigurationException(msg, ex); @@ -1975,8 +1975,8 @@ public abstract class AnnotationUtils { private void validate() { // Target annotation is not meta-present? if (!this.isAliasPair && !isAnnotationMetaPresent(this.sourceAnnotationType, this.aliasedAnnotationType)) { - String msg = String.format("@AliasFor declaration on attribute [%s] in annotation [%s] declares " + - "an alias for attribute [%s] in meta-annotation [%s] which is not meta-present.", + String msg = String.format("@AliasFor declaration on attribute '%s' in annotation [%s] declares " + + "an alias for attribute '%s' in meta-annotation [%s] which is not meta-present.", this.sourceAttributeName, this.sourceAnnotationType.getName(), this.aliasedAttributeName, this.aliasedAnnotationType.getName()); throw new AnnotationConfigurationException(msg); @@ -1985,14 +1985,14 @@ public abstract class AnnotationUtils { if (this.isAliasPair) { AliasFor mirrorAliasFor = this.aliasedAttribute.getAnnotation(AliasFor.class); if (mirrorAliasFor == null) { - String msg = String.format("Attribute [%s] in annotation [%s] must be declared as an @AliasFor [%s].", + String msg = String.format("Attribute '%s' in annotation [%s] must be declared as an @AliasFor [%s].", this.aliasedAttributeName, this.sourceAnnotationType.getName(), this.sourceAttributeName); throw new AnnotationConfigurationException(msg); } String mirrorAliasedAttributeName = getAliasedAttributeName(mirrorAliasFor, this.aliasedAttribute); if (!this.sourceAttributeName.equals(mirrorAliasedAttributeName)) { - String msg = String.format("Attribute [%s] in annotation [%s] must be declared as an @AliasFor [%s], not [%s].", + String msg = String.format("Attribute '%s' in annotation [%s] must be declared as an @AliasFor [%s], not [%s].", this.aliasedAttributeName, this.sourceAnnotationType.getName(), this.sourceAttributeName, mirrorAliasedAttributeName); throw new AnnotationConfigurationException(msg); @@ -2001,9 +2001,10 @@ public abstract class AnnotationUtils { Class returnType = this.sourceAttribute.getReturnType(); Class aliasedReturnType = this.aliasedAttribute.getReturnType(); - if (returnType != aliasedReturnType) { - String msg = String.format("Misconfigured aliases: attribute [%s] in annotation [%s] " + - "and attribute [%s] in annotation [%s] must declare the same return type.", + if (returnType != aliasedReturnType && + (!aliasedReturnType.isArray() || returnType != aliasedReturnType.getComponentType())) { + String msg = String.format("Misconfigured aliases: attribute '%s' in annotation [%s] " + + "and attribute '%s' in annotation [%s] must declare the same return type.", this.sourceAttributeName, this.sourceAnnotationType.getName(), this.aliasedAttributeName, this.aliasedAnnotationType.getName()); throw new AnnotationConfigurationException(msg); @@ -2020,16 +2021,16 @@ public abstract class AnnotationUtils { 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.", + String msg = String.format("Misconfigured aliases: attribute '%s' in annotation [%s] " + + "and attribute '%s' in annotation [%s] must declare default values.", this.sourceAttributeName, this.sourceAnnotationType.getName(), aliasedAttribute.getName(), aliasedAttribute.getDeclaringClass().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.", + String msg = String.format("Misconfigured aliases: attribute '%s' in annotation [%s] " + + "and attribute '%s' in annotation [%s] must declare the same default value.", this.sourceAttributeName, this.sourceAnnotationType.getName(), aliasedAttribute.getName(), aliasedAttribute.getDeclaringClass().getName()); throw new AnnotationConfigurationException(msg); @@ -2154,7 +2155,7 @@ public abstract class AnnotationUtils { // Ensure user did not declare both 'value' and 'attribute' in @AliasFor if (attributeDeclared && valueDeclared) { - String msg = String.format("In @AliasFor declared on attribute [%s] in annotation [%s], attribute 'attribute' " + + String msg = String.format("In @AliasFor declared on attribute '%s' in annotation [%s], attribute 'attribute' " + "and its alias 'value' are present with values of [%s] and [%s], but only one is permitted.", attribute.getName(), attribute.getDeclaringClass().getName(), attributeName, value); throw new AnnotationConfigurationException(msg); diff --git a/spring-core/src/main/java/org/springframework/core/annotation/MapAnnotationAttributeExtractor.java b/spring-core/src/main/java/org/springframework/core/annotation/MapAnnotationAttributeExtractor.java index 005ae431b3..7bb920f744 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/MapAnnotationAttributeExtractor.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/MapAnnotationAttributeExtractor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,7 +19,7 @@ package org.springframework.core.annotation; import java.lang.annotation.Annotation; import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Method; -import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -87,7 +87,7 @@ class MapAnnotationAttributeExtractor extends AbstractAliasAwareAnnotationAttrib private static Map enrichAndValidateAttributes( Map originalAttributes, Class annotationType) { - Map attributes = new HashMap(originalAttributes); + Map attributes = new LinkedHashMap(originalAttributes); Map> attributeAliasMap = getAttributeAliasMap(annotationType); for (Method attributeMethod : getAttributeMethods(annotationType)) { @@ -121,7 +121,7 @@ class MapAnnotationAttributeExtractor extends AbstractAliasAwareAnnotationAttrib // if still null if (attributeValue == null) { throw new IllegalArgumentException(String.format( - "Attributes map [%s] returned null for required attribute [%s] defined by annotation type [%s].", + "Attributes map %s returned null for required attribute '%s' defined by annotation type [%s].", attributes, attributeName, annotationType.getName())); } @@ -155,8 +155,8 @@ class MapAnnotationAttributeExtractor extends AbstractAliasAwareAnnotationAttrib if (!converted) { throw new IllegalArgumentException(String.format( - "Attributes map [%s] returned a value of type [%s] for attribute [%s], " - + "but a value of type [%s] is required as defined by annotation type [%s].", + "Attributes map %s returned a value of type [%s] for attribute '%s', " + + "but a value of type [%s] is required as defined by annotation type [%s].", attributes, actualReturnType.getName(), attributeName, requiredReturnType.getName(), annotationType.getName())); } 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 57dd98478b..680a7505b9 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -959,7 +959,7 @@ public class AnnotatedElementUtilsTests { @interface TestComponentScan { @AliasFor(attribute = "basePackages", annotation = ComponentScan.class) - String[] packages(); + String pkg(); } // ------------------------------------------------------------------------- @@ -1152,7 +1152,7 @@ public class AnnotatedElementUtilsTests { static class ComponentScanWithBasePackagesAndValueAliasClass { } - @TestComponentScan(packages = "com.example.app.test") + @TestComponentScan(pkg = "com.example.app.test") static class TestComponentScanClass { } 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 3f2d2aaf79..ae35941c5f 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -210,8 +210,7 @@ public class AnnotationUtilsTests { /** @since 4.1.2 */ @Test public void findClassAnnotationFavorsMoreLocallyDeclaredComposedAnnotationsOverAnnotationsOnInterfaces() { - Component component = findAnnotation(ClassWithLocalMetaAnnotationAndMetaAnnotatedInterface.class, - Component.class); + Component component = findAnnotation(ClassWithLocalMetaAnnotationAndMetaAnnotatedInterface.class, Component.class); assertNotNull(component); assertEquals("meta2", component.value()); } @@ -552,7 +551,7 @@ public class AnnotationUtilsTests { @Test public void getRepeatableAnnotationsDeclaredOnClassWithMissingAttributeAliasDeclaration() throws Exception { exception.expect(AnnotationConfigurationException.class); - exception.expectMessage(startsWith("Attribute [value] in")); + exception.expectMessage(startsWith("Attribute 'value' in")); exception.expectMessage(containsString(BrokenContextConfig.class.getName())); exception.expectMessage(containsString("@AliasFor [location]")); @@ -844,7 +843,7 @@ public class AnnotationUtilsTests { public void synthesizeAnnotationWhereAliasForIsMissingAttributeDeclaration() throws Exception { AliasForWithMissingAttributeDeclaration annotation = AliasForWithMissingAttributeDeclarationClass.class.getAnnotation(AliasForWithMissingAttributeDeclaration.class); exception.expect(AnnotationConfigurationException.class); - exception.expectMessage(startsWith("@AliasFor declaration on attribute [foo] in annotation")); + exception.expectMessage(startsWith("@AliasFor declaration on attribute 'foo' in annotation")); exception.expectMessage(containsString(AliasForWithMissingAttributeDeclaration.class.getName())); exception.expectMessage(containsString("points to itself")); synthesizeAnnotation(annotation); @@ -854,7 +853,7 @@ public class AnnotationUtilsTests { public void synthesizeAnnotationWhereAliasForHasDuplicateAttributeDeclaration() throws Exception { AliasForWithDuplicateAttributeDeclaration annotation = AliasForWithDuplicateAttributeDeclarationClass.class.getAnnotation(AliasForWithDuplicateAttributeDeclaration.class); exception.expect(AnnotationConfigurationException.class); - exception.expectMessage(startsWith("In @AliasFor declared on attribute [foo] in annotation")); + exception.expectMessage(startsWith("In @AliasFor declared on attribute 'foo' in annotation")); exception.expectMessage(containsString(AliasForWithDuplicateAttributeDeclaration.class.getName())); exception.expectMessage(containsString("attribute 'attribute' and its alias 'value' are present with values of [baz] and [bar]")); synthesizeAnnotation(annotation); @@ -864,9 +863,9 @@ public class AnnotationUtilsTests { public void synthesizeAnnotationWithAttributeAliasForNonexistentAttribute() throws Exception { AliasForNonexistentAttribute annotation = AliasForNonexistentAttributeClass.class.getAnnotation(AliasForNonexistentAttribute.class); exception.expect(AnnotationConfigurationException.class); - exception.expectMessage(startsWith("Attribute [foo] in")); + exception.expectMessage(startsWith("Attribute 'foo' in")); exception.expectMessage(containsString(AliasForNonexistentAttribute.class.getName())); - exception.expectMessage(containsString("is declared as an @AliasFor nonexistent attribute [bar]")); + exception.expectMessage(containsString("is declared as an @AliasFor nonexistent attribute 'bar'")); synthesizeAnnotation(annotation); } @@ -875,7 +874,7 @@ public class AnnotationUtilsTests { AliasForWithoutMirroredAliasFor annotation = AliasForWithoutMirroredAliasForClass.class.getAnnotation(AliasForWithoutMirroredAliasFor.class); exception.expect(AnnotationConfigurationException.class); - exception.expectMessage(startsWith("Attribute [bar] in")); + exception.expectMessage(startsWith("Attribute 'bar' in")); exception.expectMessage(containsString(AliasForWithoutMirroredAliasFor.class.getName())); exception.expectMessage(containsString("@AliasFor [foo]")); synthesizeAnnotation(annotation); @@ -887,10 +886,10 @@ public class AnnotationUtilsTests { AliasForWithMirroredAliasForWrongAttributeClass.class.getAnnotation(AliasForWithMirroredAliasForWrongAttribute.class); exception.expect(AnnotationConfigurationException.class); - exception.expectMessage(startsWith("Attribute [bar] in")); + exception.expectMessage(startsWith("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]"))); + or(containsString("is declared as an @AliasFor nonexistent attribute 'quux'"))); synthesizeAnnotation(annotation); } @@ -901,8 +900,8 @@ public class AnnotationUtilsTests { exception.expect(AnnotationConfigurationException.class); exception.expectMessage(startsWith("Misconfigured aliases")); exception.expectMessage(containsString(AliasForAttributeOfDifferentType.class.getName())); - exception.expectMessage(containsString("attribute [foo]")); - exception.expectMessage(containsString("attribute [bar]")); + exception.expectMessage(containsString("attribute 'foo'")); + exception.expectMessage(containsString("attribute 'bar'")); exception.expectMessage(containsString("same return type")); synthesizeAnnotation(annotation); } @@ -914,8 +913,8 @@ public class AnnotationUtilsTests { exception.expect(AnnotationConfigurationException.class); exception.expectMessage(startsWith("Misconfigured aliases")); exception.expectMessage(containsString(AliasForWithMissingDefaultValues.class.getName())); - exception.expectMessage(containsString("attribute [foo] in annotation")); - exception.expectMessage(containsString("attribute [bar] in annotation")); + exception.expectMessage(containsString("attribute 'foo' in annotation")); + exception.expectMessage(containsString("attribute 'bar' in annotation")); exception.expectMessage(containsString("default values")); synthesizeAnnotation(annotation); } @@ -927,8 +926,8 @@ public class AnnotationUtilsTests { exception.expect(AnnotationConfigurationException.class); exception.expectMessage(startsWith("Misconfigured aliases")); exception.expectMessage(containsString(AliasForAttributeWithDifferentDefaultValue.class.getName())); - exception.expectMessage(containsString("attribute [foo] in annotation")); - exception.expectMessage(containsString("attribute [bar] in annotation")); + exception.expectMessage(containsString("attribute 'foo' in annotation")); + exception.expectMessage(containsString("attribute 'bar' in annotation")); exception.expectMessage(containsString("same default value")); synthesizeAnnotation(annotation); } @@ -938,9 +937,9 @@ public class AnnotationUtilsTests { AliasedComposedContextConfigNotMetaPresent annotation = AliasedComposedContextConfigNotMetaPresentClass.class.getAnnotation(AliasedComposedContextConfigNotMetaPresent.class); exception.expect(AnnotationConfigurationException.class); - exception.expectMessage(startsWith("@AliasFor declaration on attribute [xmlConfigFile] in annotation")); + exception.expectMessage(startsWith("@AliasFor declaration on attribute 'xmlConfigFile' in annotation")); exception.expectMessage(containsString(AliasedComposedContextConfigNotMetaPresent.class.getName())); - exception.expectMessage(containsString("declares an alias for attribute [location] in meta-annotation")); + exception.expectMessage(containsString("declares an alias for attribute 'location' in meta-annotation")); exception.expectMessage(containsString(ContextConfig.class.getName())); exception.expectMessage(containsString("not meta-present")); synthesizeAnnotation(annotation); @@ -1038,8 +1037,8 @@ public class AnnotationUtilsTests { exception.expect(AnnotationConfigurationException.class); exception.expectMessage(startsWith("Misconfigured aliases:")); - exception.expectMessage(containsString("attribute [location1] in annotation [" + annotationType.getName() + "]")); - exception.expectMessage(containsString("attribute [location2] in annotation [" + annotationType.getName() + "]")); + exception.expectMessage(containsString("attribute 'location1' in annotation [" + annotationType.getName() + "]")); + exception.expectMessage(containsString("attribute 'location2' in annotation [" + annotationType.getName() + "]")); exception.expectMessage(containsString("default values")); synthesizeAnnotation(config, clazz); @@ -1054,8 +1053,8 @@ public class AnnotationUtilsTests { exception.expect(AnnotationConfigurationException.class); exception.expectMessage(startsWith("Misconfigured aliases:")); - exception.expectMessage(containsString("attribute [location1] in annotation [" + annotationType.getName() + "]")); - exception.expectMessage(containsString("attribute [location2] in annotation [" + annotationType.getName() + "]")); + exception.expectMessage(containsString("attribute 'location1' in annotation [" + annotationType.getName() + "]")); + exception.expectMessage(containsString("attribute 'location2' in annotation [" + annotationType.getName() + "]")); exception.expectMessage(containsString("same default value")); synthesizeAnnotation(config, clazz); @@ -1220,7 +1219,7 @@ public class AnnotationUtilsTests { private void assertMissingTextAttribute(Map attributes) { exception.expect(IllegalArgumentException.class); exception.expectMessage(startsWith("Attributes map")); - exception.expectMessage(containsString("returned null for required attribute [text]")); + exception.expectMessage(containsString("returned null for required attribute 'text'")); exception.expectMessage(containsString("defined by annotation type [" + AnnotationWithoutDefaults.class.getName() + "]")); synthesizeAnnotation(attributes, AnnotationWithoutDefaults.class, null); } @@ -1232,7 +1231,7 @@ public class AnnotationUtilsTests { exception.expect(IllegalArgumentException.class); exception.expectMessage(startsWith("Attributes map")); exception.expectMessage(containsString("returned a value of type [java.lang.Long]")); - exception.expectMessage(containsString("for attribute [value]")); + exception.expectMessage(containsString("for attribute 'value'")); exception.expectMessage(containsString("but a value of type [java.lang.String] is required")); exception.expectMessage(containsString("as defined by annotation type [" + Component.class.getName() + "]")); @@ -1241,7 +1240,6 @@ public class AnnotationUtilsTests { @Test public void synthesizeAnnotationFromAnnotationAttributesWithoutAttributeAliases() throws Exception { - // 1) Get an annotation Component component = WebController.class.getAnnotation(Component.class); assertNotNull(component);