diff --git a/spring-test/src/main/java/org/springframework/test/context/MetaAnnotationUtils.java b/spring-test/src/main/java/org/springframework/test/context/MetaAnnotationUtils.java
index 0c215dc92a..24e3c8fbdd 100644
--- a/spring-test/src/main/java/org/springframework/test/context/MetaAnnotationUtils.java
+++ b/spring-test/src/main/java/org/springframework/test/context/MetaAnnotationUtils.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2013 the original author or authors.
+ * Copyright 2002-2014 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.
@@ -17,6 +17,8 @@
package org.springframework.test.context;
import java.lang.annotation.Annotation;
+import java.util.HashSet;
+import java.util.Set;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.AnnotationAttributes;
@@ -29,20 +31,22 @@ import static org.springframework.core.annotation.AnnotationUtils.*;
/**
* {@code MetaAnnotationUtils} is a collection of utility methods that complements
- * support already available in {@link AnnotationUtils}.
+ * the standard support already available in {@link AnnotationUtils}.
*
- *
Whereas {@code AnnotationUtils} only provides utilities for getting
- * or finding an annotation, {@code MetaAnnotationUtils} provides
- * additional support for determining the root class on which an
+ *
Whereas {@code AnnotationUtils} provides utilities for getting or
+ * finding an annotation, {@code MetaAnnotationUtils} goes a step further
+ * by providing support for determining the root class on which an
* annotation is declared, either directly or via a composed annotation.
* This additional information is encapsulated in an {@link AnnotationDescriptor}.
*
*
The additional information provided by an {@code AnnotationDescriptor} is
- * required in the Spring TestContext Framework in order to be able to
- * support class hierarchy traversals for inherited annotations such as
+ * required by the Spring TestContext Framework in order to be able to
+ * support class hierarchy traversals for annotations such as
* {@link ContextConfiguration @ContextConfiguration},
* {@link TestExecutionListeners @TestExecutionListeners}, and
- * {@link ActiveProfiles @ActiveProfiles}.
+ * {@link ActiveProfiles @ActiveProfiles} which offer support for merging and
+ * overriding various inherited annotation attributes (e.g., {@link
+ * ContextConfiguration#inheritLocations}).
*
* @author Sam Brannen
* @since 4.0
@@ -57,7 +61,7 @@ abstract class MetaAnnotationUtils {
/**
* Find the {@link AnnotationDescriptor} for the supplied {@code annotationType}
- * from the supplied {@link Class}, traversing its annotations and superclasses
+ * on the supplied {@link Class}, traversing its annotations and superclasses
* if no annotation can be found on the given class itself.
*
*
This method explicitly handles class-level annotations which are not
@@ -66,23 +70,22 @@ abstract class MetaAnnotationUtils {
*
*
The algorithm operates as follows:
*
- * - Search for a local declaration of the annotation on the given class
- * and return a corresponding {@code AnnotationDescriptor} if found.
- *
- Search through all annotations that the given class declares,
- * returning an {@code AnnotationDescriptor} for the first matching
- * candidate, if any.
- *
- Proceed with introspection of the superclass hierarchy of the given
- * class by returning to step #1 with the superclass as the class to look
- * for annotations on.
+ *
- Search for the annotation on the given class and return a corresponding
+ * {@code AnnotationDescriptor} if found.
+ *
- Recursively search through all annotations that the given class declares.
+ *
- Recursively search through the superclass hierarchy of the given class.
*
*
+ * In this context, the term recursively means that the search
+ * process continues by returning to step #1 with the current annotation or
+ * superclass as the class to look for annotations on.
+ *
*
If the supplied {@code clazz} is an interface, only the interface
* itself will be checked; the inheritance hierarchy for interfaces will not
* be traversed.
*
* @param clazz the class to look for annotations on
- * @param annotationType the annotation class to look for, both locally and
- * as a meta-annotation
+ * @param annotationType the type of annotation to look for
* @return the corresponding annotation descriptor if the annotation was found;
* otherwise {@code null}
* @see AnnotationUtils#findAnnotationDeclaringClass(Class, Class)
@@ -90,6 +93,22 @@ abstract class MetaAnnotationUtils {
*/
public static AnnotationDescriptor findAnnotationDescriptor(Class> clazz,
Class annotationType) {
+ return findAnnotationDescriptor(clazz, new HashSet(), annotationType);
+ }
+
+ /**
+ * Perform the search algorithm for {@link #findAnnotationDescriptor(Class, Class)},
+ * avoiding endless recursion by tracking which annotations have already been
+ * visited.
+ *
+ * @param clazz the class to look for annotations on
+ * @param visited the set of annotations that have already been visited
+ * @param annotationType the type of annotation to look for
+ * @return the corresponding annotation descriptor if the annotation was found;
+ * otherwise {@code null}
+ */
+ private static AnnotationDescriptor findAnnotationDescriptor(Class> clazz,
+ Set visited, Class annotationType) {
Assert.notNull(annotationType, "Annotation type must not be null");
@@ -103,29 +122,30 @@ abstract class MetaAnnotationUtils {
}
// Declared on a composed annotation (i.e., as a meta-annotation)?
- if (!Annotation.class.isAssignableFrom(clazz)) {
- for (Annotation composedAnnotation : clazz.getAnnotations()) {
- T annotation = composedAnnotation.annotationType().getAnnotation(annotationType);
- if (annotation != null) {
- return new AnnotationDescriptor(clazz, composedAnnotation, annotation);
+ for (Annotation composedAnnotation : clazz.getDeclaredAnnotations()) {
+ if (visited.add(composedAnnotation)) {
+ AnnotationDescriptor descriptor = findAnnotationDescriptor(composedAnnotation.annotationType(),
+ visited, annotationType);
+ if (descriptor != null) {
+ return new AnnotationDescriptor(clazz, descriptor.getDeclaringClass(), composedAnnotation,
+ descriptor.getAnnotation());
}
}
}
// Declared on a superclass?
- return findAnnotationDescriptor(clazz.getSuperclass(), annotationType);
+ return findAnnotationDescriptor(clazz.getSuperclass(), visited, annotationType);
}
/**
* Find the {@link UntypedAnnotationDescriptor} for the first {@link Class}
* in the inheritance hierarchy of the specified {@code clazz} (including
* the specified {@code clazz} itself) which declares at least one of the
- * specified {@code annotationTypes}, or {@code null} if none of the
- * specified annotation types could be found.
+ * specified {@code annotationTypes}.
*
* This method traverses the annotations and superclasses of the specified
* {@code clazz} if no annotation can be found on the given class itself.
- *
+ *
*
This method explicitly handles class-level annotations which are not
* declared as {@linkplain java.lang.annotation.Inherited inherited} as
* well as meta-annotations.
@@ -135,21 +155,20 @@ abstract class MetaAnnotationUtils {
*
Search for a local declaration of one of the annotation types on
* the given class and return a corresponding {@code UntypedAnnotationDescriptor}
* if found.
- * Search through all annotations that the given class declares,
- * returning an {@code UntypedAnnotationDescriptor} for the first matching
- * candidate, if any.
- * Proceed with introspection of the superclass hierarchy of the given
- * class by returning to step #1 with the superclass as the class to look
- * for annotations on.
+ * Recursively search through all annotations that the given class declares.
+ * Recursively search through the superclass hierarchy of the given class.
*
*
+ * In this context, the term recursively means that the search
+ * process continues by returning to step #1 with the current annotation or
+ * superclass as the class to look for annotations on.
+ *
*
If the supplied {@code clazz} is an interface, only the interface
* itself will be checked; the inheritance hierarchy for interfaces will not
* be traversed.
*
* @param clazz the class to look for annotations on
- * @param annotationTypes the types of annotations to look for, both locally
- * and as meta-annotations
+ * @param annotationTypes the types of annotations to look for
* @return the corresponding annotation descriptor if one of the annotations
* was found; otherwise {@code null}
* @see AnnotationUtils#findAnnotationDeclaringClassForTypes(java.util.List, Class)
@@ -158,6 +177,23 @@ abstract class MetaAnnotationUtils {
@SuppressWarnings("unchecked")
public static UntypedAnnotationDescriptor findAnnotationDescriptorForTypes(Class> clazz,
Class extends Annotation>... annotationTypes) {
+ return findAnnotationDescriptorForTypes(clazz, new HashSet(), annotationTypes);
+ }
+
+ /**
+ * Perform the search algorithm for {@link #findAnnotationDescriptorForTypes(Class, Class...)},
+ * avoiding endless recursion by tracking which annotations have already been
+ * visited.
+ *
+ * @param clazz the class to look for annotations on
+ * @param visited the set of annotations that have already been visited
+ * @param annotationTypes the types of annotations to look for
+ * @return the corresponding annotation descriptor if one of the annotations
+ * was found; otherwise {@code null}
+ */
+ @SuppressWarnings("unchecked")
+ private static UntypedAnnotationDescriptor findAnnotationDescriptorForTypes(Class> clazz,
+ Set visited, Class extends Annotation>... annotationTypes) {
assertNonEmptyAnnotationTypeArray(annotationTypes, "The list of annotation types must not be empty");
@@ -173,19 +209,19 @@ abstract class MetaAnnotationUtils {
}
// Declared on a composed annotation (i.e., as a meta-annotation)?
- if (!Annotation.class.isAssignableFrom(clazz)) {
- for (Annotation composedAnnotation : clazz.getAnnotations()) {
- for (Class extends Annotation> annotationType : annotationTypes) {
- Annotation annotation = composedAnnotation.annotationType().getAnnotation(annotationType);
- if (annotation != null) {
- return new UntypedAnnotationDescriptor(clazz, composedAnnotation, annotation);
- }
+ for (Annotation composedAnnotation : clazz.getDeclaredAnnotations()) {
+ if (visited.add(composedAnnotation)) {
+ UntypedAnnotationDescriptor descriptor = findAnnotationDescriptorForTypes(
+ composedAnnotation.annotationType(), visited, annotationTypes);
+ if (descriptor != null) {
+ return new UntypedAnnotationDescriptor(clazz, descriptor.getDeclaringClass(), composedAnnotation,
+ descriptor.getAnnotation());
}
}
}
// Declared on a superclass?
- return findAnnotationDescriptorForTypes(clazz.getSuperclass(), annotationTypes);
+ return findAnnotationDescriptorForTypes(clazz.getSuperclass(), visited, annotationTypes);
}
@@ -254,16 +290,16 @@ abstract class MetaAnnotationUtils {
public AnnotationDescriptor(Class> rootDeclaringClass, T annotation) {
- this(rootDeclaringClass, null, annotation);
+ this(rootDeclaringClass, rootDeclaringClass, null, annotation);
}
- public AnnotationDescriptor(Class> rootDeclaringClass, Annotation composedAnnotation, T annotation) {
+ public AnnotationDescriptor(Class> rootDeclaringClass, Class> declaringClass,
+ Annotation composedAnnotation, T annotation) {
Assert.notNull(rootDeclaringClass, "rootDeclaringClass must not be null");
Assert.notNull(annotation, "annotation must not be null");
this.rootDeclaringClass = rootDeclaringClass;
- this.declaringClass = (composedAnnotation != null) ? composedAnnotation.annotationType()
- : rootDeclaringClass;
+ this.declaringClass = declaringClass;
this.composedAnnotation = composedAnnotation;
this.annotation = annotation;
this.annotationAttributes = AnnotatedElementUtils.getAnnotationAttributes(rootDeclaringClass,
@@ -322,12 +358,13 @@ abstract class MetaAnnotationUtils {
*/
public static class UntypedAnnotationDescriptor extends AnnotationDescriptor {
- public UntypedAnnotationDescriptor(Class> declaringClass, Annotation annotation) {
- super(declaringClass, annotation);
+ public UntypedAnnotationDescriptor(Class> rootDeclaringClass, Annotation annotation) {
+ this(rootDeclaringClass, rootDeclaringClass, null, annotation);
}
- public UntypedAnnotationDescriptor(Class> declaringClass, Annotation composedAnnotation, Annotation annotation) {
- super(declaringClass, composedAnnotation, annotation);
+ public UntypedAnnotationDescriptor(Class> rootDeclaringClass, Class> declaringClass,
+ Annotation composedAnnotation, Annotation annotation) {
+ super(rootDeclaringClass, declaringClass, composedAnnotation, annotation);
}
}
diff --git a/spring-test/src/test/java/org/springframework/test/context/MetaAnnotationUtilsTests.java b/spring-test/src/test/java/org/springframework/test/context/MetaAnnotationUtilsTests.java
index dbf1825a42..0fb5ad2533 100644
--- a/spring-test/src/test/java/org/springframework/test/context/MetaAnnotationUtilsTests.java
+++ b/spring-test/src/test/java/org/springframework/test/context/MetaAnnotationUtilsTests.java
@@ -47,28 +47,48 @@ public class MetaAnnotationUtilsTests {
private void assertAtComponentOnComposedAnnotation(Class> startClass, Class> rootDeclaringClass, String name,
Class extends Annotation> composedAnnotationType) {
+ assertAtComponentOnComposedAnnotation(rootDeclaringClass, rootDeclaringClass, composedAnnotationType, name,
+ composedAnnotationType);
+ }
+
+ private void assertAtComponentOnComposedAnnotation(Class> startClass, Class> rootDeclaringClass,
+ Class> declaringClass, String name, Class extends Annotation> composedAnnotationType) {
AnnotationDescriptor descriptor = findAnnotationDescriptor(startClass, Component.class);
- assertNotNull(descriptor);
- assertEquals(rootDeclaringClass, descriptor.getRootDeclaringClass());
- assertEquals(composedAnnotationType, descriptor.getDeclaringClass());
- assertEquals(Component.class, descriptor.getAnnotationType());
- assertEquals(name, descriptor.getAnnotation().value());
- assertNotNull(descriptor.getComposedAnnotation());
- assertEquals(composedAnnotationType, descriptor.getComposedAnnotationType());
+ assertNotNull("AnnotationDescriptor should not be null", descriptor);
+ assertEquals("rootDeclaringClass", rootDeclaringClass, descriptor.getRootDeclaringClass());
+ assertEquals("declaringClass", declaringClass, descriptor.getDeclaringClass());
+ assertEquals("annotationType", Component.class, descriptor.getAnnotationType());
+ assertEquals("component name", name, descriptor.getAnnotation().value());
+ assertNotNull("composedAnnotation should not be null", descriptor.getComposedAnnotation());
+ assertEquals("composedAnnotationType", composedAnnotationType, descriptor.getComposedAnnotationType());
+ }
+
+ private void assertAtComponentOnComposedAnnotationForMultipleCandidateTypes(Class> startClass, String name,
+ Class extends Annotation> composedAnnotationType) {
+ assertAtComponentOnComposedAnnotationForMultipleCandidateTypes(startClass, startClass, name,
+ composedAnnotationType);
+ }
+
+ private void assertAtComponentOnComposedAnnotationForMultipleCandidateTypes(Class> startClass,
+ Class> rootDeclaringClass, String name, Class extends Annotation> composedAnnotationType) {
+ assertAtComponentOnComposedAnnotationForMultipleCandidateTypes(startClass, rootDeclaringClass,
+ composedAnnotationType, name, composedAnnotationType);
}
@SuppressWarnings("unchecked")
private void assertAtComponentOnComposedAnnotationForMultipleCandidateTypes(Class> startClass,
- Class> declaringClass, String name, Class extends Annotation> composedAnnotationType) {
+ Class> rootDeclaringClass, Class> declaringClass, String name,
+ Class extends Annotation> composedAnnotationType) {
Class annotationType = Component.class;
UntypedAnnotationDescriptor descriptor = findAnnotationDescriptorForTypes(startClass, Service.class,
annotationType, Order.class, Transactional.class);
- assertNotNull(descriptor);
- assertEquals(declaringClass, descriptor.getRootDeclaringClass());
- assertEquals(annotationType, descriptor.getAnnotationType());
- assertEquals(name, ((Component) descriptor.getAnnotation()).value());
- assertNotNull(descriptor.getComposedAnnotation());
- assertEquals(composedAnnotationType, descriptor.getComposedAnnotationType());
+ assertNotNull("UntypedAnnotationDescriptor should not be null", descriptor);
+ assertEquals("rootDeclaringClass", rootDeclaringClass, descriptor.getRootDeclaringClass());
+ assertEquals("declaringClass", declaringClass, descriptor.getDeclaringClass());
+ assertEquals("annotationType", annotationType, descriptor.getAnnotationType());
+ assertEquals("component name", name, ((Component) descriptor.getAnnotation()).value());
+ assertNotNull("composedAnnotation should not be null", descriptor.getComposedAnnotation());
+ assertEquals("composedAnnotationType", composedAnnotationType, descriptor.getComposedAnnotationType());
}
@Test
@@ -150,6 +170,45 @@ public class MetaAnnotationUtilsTests {
ClassWithLocalMetaAnnotationAndMetaAnnotatedInterface.class, "meta2", Meta2.class);
}
+ /**
+ * @since 4.0.3
+ */
+ @Test
+ public void findAnnotationDescriptorOnMetaMetaAnnotatedClass() {
+ Class startClass = MetaMetaAnnotatedClass.class;
+ assertAtComponentOnComposedAnnotation(startClass, startClass, Meta2.class, "meta2", MetaMeta.class);
+ }
+
+ /**
+ * @since 4.0.3
+ */
+ @Test
+ public void findAnnotationDescriptorOnMetaMetaMetaAnnotatedClass() {
+ Class startClass = MetaMetaMetaAnnotatedClass.class;
+ assertAtComponentOnComposedAnnotation(startClass, startClass, Meta2.class, "meta2", MetaMetaMeta.class);
+ }
+
+ /**
+ * @since 4.0.3
+ */
+ @Test
+ public void findAnnotationDescriptorOnAnnotatedClassWithMissingTargetMetaAnnotation() {
+ // InheritedAnnotationClass is NOT annotated or meta-annotated with @Component
+ AnnotationDescriptor descriptor = findAnnotationDescriptor(InheritedAnnotationClass.class,
+ Component.class);
+ assertNull("Should not find @Component on InheritedAnnotationClass", descriptor);
+ }
+
+ /**
+ * @since 4.0.3
+ */
+ @Test
+ public void findAnnotationDescriptorOnMetaCycleAnnotatedClassWithMissingTargetMetaAnnotation() {
+ AnnotationDescriptor descriptor = findAnnotationDescriptor(MetaCycleAnnotatedClass.class,
+ Component.class);
+ assertNull("Should not find @Component on MetaCycleAnnotatedClass", descriptor);
+ }
+
// -------------------------------------------------------------------------
@Test
@@ -217,7 +276,7 @@ public class MetaAnnotationUtilsTests {
@Test
public void findAnnotationDescriptorForTypesWithMetaComponentAnnotation() throws Exception {
Class startClass = HasMetaComponentAnnotation.class;
- assertAtComponentOnComposedAnnotationForMultipleCandidateTypes(startClass, startClass, "meta1", Meta1.class);
+ assertAtComponentOnComposedAnnotationForMultipleCandidateTypes(startClass, "meta1", Meta1.class);
}
@Test
@@ -261,7 +320,7 @@ public class MetaAnnotationUtilsTests {
@Test
public void findAnnotationDescriptorForTypesForInterfaceWithMetaAnnotation() {
Class startClass = InterfaceWithMetaAnnotation.class;
- assertAtComponentOnComposedAnnotationForMultipleCandidateTypes(startClass, startClass, "meta1", Meta1.class);
+ assertAtComponentOnComposedAnnotationForMultipleCandidateTypes(startClass, "meta1", Meta1.class);
}
@Test
@@ -274,7 +333,7 @@ public class MetaAnnotationUtilsTests {
@Test
public void findAnnotationDescriptorForTypesForClassWithLocalMetaAnnotationAndMetaAnnotatedInterface() {
Class startClass = ClassWithLocalMetaAnnotationAndMetaAnnotatedInterface.class;
- assertAtComponentOnComposedAnnotationForMultipleCandidateTypes(startClass, startClass, "meta2", Meta2.class);
+ assertAtComponentOnComposedAnnotationForMultipleCandidateTypes(startClass, "meta2", Meta2.class);
}
@Test
@@ -284,6 +343,50 @@ public class MetaAnnotationUtilsTests {
ClassWithLocalMetaAnnotationAndMetaAnnotatedInterface.class, "meta2", Meta2.class);
}
+ /**
+ * @since 4.0.3
+ */
+ @Test
+ public void findAnnotationDescriptorForTypesOnMetaMetaAnnotatedClass() {
+ Class startClass = MetaMetaAnnotatedClass.class;
+ assertAtComponentOnComposedAnnotationForMultipleCandidateTypes(startClass, startClass, Meta2.class, "meta2",
+ MetaMeta.class);
+ }
+
+ /**
+ * @since 4.0.3
+ */
+ @Test
+ public void findAnnotationDescriptorForTypesOnMetaMetaMetaAnnotatedClass() {
+ Class startClass = MetaMetaMetaAnnotatedClass.class;
+ assertAtComponentOnComposedAnnotationForMultipleCandidateTypes(startClass, startClass, Meta2.class, "meta2",
+ MetaMetaMeta.class);
+ }
+
+ /**
+ * @since 4.0.3
+ */
+ @Test
+ @SuppressWarnings("unchecked")
+ public void findAnnotationDescriptorForTypesOnAnnotatedClassWithMissingTargetMetaAnnotation() {
+ // InheritedAnnotationClass is NOT annotated or meta-annotated with @Component,
+ // @Service, or @Order, but it is annotated with @Transactional.
+ UntypedAnnotationDescriptor descriptor = findAnnotationDescriptorForTypes(InheritedAnnotationClass.class,
+ Service.class, Component.class, Order.class);
+ assertNull("Should not find @Component on InheritedAnnotationClass", descriptor);
+ }
+
+ /**
+ * @since 4.0.3
+ */
+ @Test
+ @SuppressWarnings("unchecked")
+ public void findAnnotationDescriptorForTypesOnMetaCycleAnnotatedClassWithMissingTargetMetaAnnotation() {
+ UntypedAnnotationDescriptor descriptor = findAnnotationDescriptorForTypes(MetaCycleAnnotatedClass.class,
+ Service.class, Component.class, Order.class);
+ assertNull("Should not find @Component on MetaCycleAnnotatedClass", descriptor);
+ }
+
// -------------------------------------------------------------------------
@@ -299,6 +402,31 @@ public class MetaAnnotationUtilsTests {
static @interface Meta2 {
}
+ @Meta2
+ @Retention(RetentionPolicy.RUNTIME)
+ @interface MetaMeta {
+ }
+
+ @MetaMeta
+ @Retention(RetentionPolicy.RUNTIME)
+ @interface MetaMetaMeta {
+ }
+
+ @MetaCycle3
+ @Retention(RetentionPolicy.RUNTIME)
+ @interface MetaCycle1 {
+ }
+
+ @MetaCycle1
+ @Retention(RetentionPolicy.RUNTIME)
+ @interface MetaCycle2 {
+ }
+
+ @MetaCycle2
+ @Retention(RetentionPolicy.RUNTIME)
+ @interface MetaCycle3 {
+ }
+
@ContextConfiguration
@Retention(RetentionPolicy.RUNTIME)
static @interface MetaConfig {
@@ -340,6 +468,18 @@ public class MetaAnnotationUtilsTests {
ClassWithLocalMetaAnnotationAndMetaAnnotatedInterface {
}
+ @MetaMeta
+ static class MetaMetaAnnotatedClass {
+ }
+
+ @MetaMetaMeta
+ static class MetaMetaMetaAnnotatedClass {
+ }
+
+ @MetaCycle3
+ static class MetaCycleAnnotatedClass {
+ }
+
@MetaConfig
public class MetaConfigWithDefaultAttributesTestCase {
}
diff --git a/spring-test/src/test/java/org/springframework/test/context/hierarchies/meta/MetaContextHierarchyConfig.java b/spring-test/src/test/java/org/springframework/test/context/hierarchies/meta/MetaContextHierarchyConfig.java
new file mode 100644
index 0000000000..70482902b5
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/hierarchies/meta/MetaContextHierarchyConfig.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2002-2014 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.test.context.hierarchies.meta;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Profile;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.ContextHierarchy;
+
+/**
+ * Custom context hierarchy configuration annotation.
+ *
+ * @author Sam Brannen
+ * @since 4.0.3
+ */
+@ContextHierarchy(@ContextConfiguration(classes = { DevConfig.class, ProductionConfig.class }))
+@ActiveProfiles("dev")
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+public @interface MetaContextHierarchyConfig {
+}
+
+@Configuration
+@Profile("dev")
+class DevConfig {
+
+ @Bean
+ public String foo() {
+ return "Dev Foo";
+ }
+}
+
+@Configuration
+@Profile("prod")
+class ProductionConfig {
+
+ @Bean
+ public String foo() {
+ return "Production Foo";
+ }
+}
\ No newline at end of file
diff --git a/spring-test/src/test/java/org/springframework/test/context/hierarchies/meta/MetaHierarchyLevelOneTests.java b/spring-test/src/test/java/org/springframework/test/context/hierarchies/meta/MetaHierarchyLevelOneTests.java
new file mode 100644
index 0000000000..b212823da3
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/hierarchies/meta/MetaHierarchyLevelOneTests.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2002-2014 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.test.context.hierarchies.meta;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+
+import static org.junit.Assert.*;
+
+/**
+ * @author Sam Brannen
+ * @since 4.0.3
+ */
+@RunWith(SpringJUnit4ClassRunner.class)
+@MetaMetaContextHierarchyConfig
+public class MetaHierarchyLevelOneTests {
+
+ @Autowired
+ private String foo;
+
+
+ @Test
+ public void foo() {
+ assertEquals("Dev Foo", foo);
+ }
+
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/hierarchies/meta/MetaHierarchyLevelTwoTests.java b/spring-test/src/test/java/org/springframework/test/context/hierarchies/meta/MetaHierarchyLevelTwoTests.java
new file mode 100644
index 0000000000..38214d46d3
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/hierarchies/meta/MetaHierarchyLevelTwoTests.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2002-2014 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.test.context.hierarchies.meta;
+
+import org.junit.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Profile;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.ContextConfiguration;
+
+import static org.junit.Assert.*;
+
+/**
+ * @author Sam Brannen
+ * @since 4.0.3
+ */
+@ContextConfiguration
+@ActiveProfiles("prod")
+public class MetaHierarchyLevelTwoTests extends MetaHierarchyLevelOneTests {
+
+ @Configuration
+ @Profile("prod")
+ static class Config {
+
+ @Bean
+ public String bar() {
+ return "Prod Bar";
+ }
+ }
+
+
+ @Autowired
+ protected ApplicationContext context;
+
+ @Autowired
+ private String bar;
+
+
+ @Test
+ public void bar() {
+ assertEquals("Prod Bar", bar);
+ }
+
+ @Test
+ public void contextHierarchy() {
+ assertNotNull("child ApplicationContext", context);
+ assertNotNull("parent ApplicationContext", context.getParent());
+ assertNull("grandparent ApplicationContext", context.getParent().getParent());
+ }
+
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/hierarchies/meta/MetaMetaContextHierarchyConfig.java b/spring-test/src/test/java/org/springframework/test/context/hierarchies/meta/MetaMetaContextHierarchyConfig.java
new file mode 100644
index 0000000000..4e7b012af3
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/hierarchies/meta/MetaMetaContextHierarchyConfig.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2002-2014 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.test.context.hierarchies.meta;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Custom context hierarchy configuration annotation that is itself meta-annotated
+ * with {@link MetaContextHierarchyConfig @MetaContextHierarchyConfig}.
+ *
+ * @author Sam Brannen
+ * @since 4.0.3
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+@MetaContextHierarchyConfig
+public @interface MetaMetaContextHierarchyConfig {
+
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/meta/MetaConfigDefaultsTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/meta/MetaConfigDefaultsTests.java
index 8349aa846a..0e42192f6e 100644
--- a/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/meta/MetaConfigDefaultsTests.java
+++ b/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/meta/MetaConfigDefaultsTests.java
@@ -25,7 +25,7 @@ import static org.junit.Assert.*;
/**
* Integration tests for meta-annotation attribute override support, relying on
- * default attribute values defined in {@link MetaConfig}.
+ * default attribute values defined in {@link MetaConfig @MetaConfig}.
*
* @author Sam Brannen
* @since 4.0
diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/meta/MetaMetaConfig.java b/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/meta/MetaMetaConfig.java
new file mode 100644
index 0000000000..d468addee0
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/meta/MetaMetaConfig.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2002-2014 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.test.context.junit4.annotation.meta;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import org.springframework.test.context.ActiveProfiles;
+
+/**
+ * Custom configuration annotation that is itself meta-annotated with
+ * {@link MetaConfig @MetaConfig} and {@link ActiveProfiles @ActiveProfiles}.
+ *
+ * @author Sam Brannen
+ * @since 4.0.3
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+@MetaConfig
+// Override default "dev" profile from @MetaConfig:
+@ActiveProfiles("prod")
+public @interface MetaMetaConfig {
+
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/meta/MetaMetaConfigDefaultsTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/meta/MetaMetaConfigDefaultsTests.java
new file mode 100644
index 0000000000..87277690e0
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/junit4/annotation/meta/MetaMetaConfigDefaultsTests.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2002-2014 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.test.context.junit4.annotation.meta;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+
+import static org.junit.Assert.*;
+
+/**
+ * Integration tests for meta-meta-annotation support, relying on
+ * default attribute values defined in {@link MetaConfig @MetaConfig} and
+ * overrides in {@link MetaMetaConfig @MetaMetaConfig}.
+ *
+ * @author Sam Brannen
+ * @since 4.0.3
+ */
+@RunWith(SpringJUnit4ClassRunner.class)
+@MetaMetaConfig
+public class MetaMetaConfigDefaultsTests {
+
+ @Autowired
+ private String foo;
+
+
+ @Test
+ public void foo() {
+ assertEquals("Production Foo", foo);
+ }
+}