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 a1701c9db13..e177856e821 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 @@ -20,6 +20,7 @@ import java.lang.annotation.Annotation; import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Method; +import java.util.List; import java.util.Map; import java.util.WeakHashMap; @@ -240,14 +241,58 @@ public abstract class AnnotationUtils { * if not found * @see Class#isAnnotationPresent(Class) * @see Class#getDeclaredAnnotations() + * @see #findAnnotationDeclaringClassForTypes(List, Class) + * @see #isAnnotationDeclaredLocally(Class, Class) */ public static Class findAnnotationDeclaringClass(Class annotationType, Class clazz) { Assert.notNull(annotationType, "Annotation type must not be null"); if (clazz == null || clazz.equals(Object.class)) { return null; } - return (isAnnotationDeclaredLocally(annotationType, clazz)) ? clazz : - findAnnotationDeclaringClass(annotationType, clazz.getSuperclass()); + return (isAnnotationDeclaredLocally(annotationType, clazz)) ? clazz : findAnnotationDeclaringClass( + annotationType, clazz.getSuperclass()); + } + + /** + * Find 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. + *

If the supplied {@code clazz} is {@code null}, {@code null} will be + * returned. + *

If the supplied {@code clazz} is an interface, only the interface itself + * will be checked; the inheritance hierarchy for interfaces will not be traversed. + *

The standard {@link Class} API does not provide a mechanism for determining + * which class in an inheritance hierarchy actually declares one of several + * candidate {@linkplain Annotation annotations}, so we need to handle this + * explicitly. + * @param annotationTypes the list of Class objects corresponding to the + * annotation types + * @param clazz the Class object corresponding to the class on which to check + * for the annotations, or {@code null} + * @return the first {@link Class} in the inheritance hierarchy of the specified + * {@code clazz} which declares an annotation of at least one of the specified + * {@code annotationTypes}, or {@code null} if not found + * @see Class#isAnnotationPresent(Class) + * @see Class#getDeclaredAnnotations() + * @see #findAnnotationDeclaringClass(Class, Class) + * @see #isAnnotationDeclaredLocally(Class, Class) + * @since 3.2.2 + */ + public static Class findAnnotationDeclaringClassForTypes(List> annotationTypes, + Class clazz) { + Assert.notEmpty(annotationTypes, "The list of annotation types must not be empty"); + if (clazz == null || clazz.equals(Object.class)) { + return null; + } + + for (Class annotationType : annotationTypes) { + if (isAnnotationDeclaredLocally(annotationType, clazz)) { + return clazz; + } + } + + return findAnnotationDeclaringClassForTypes(annotationTypes, clazz.getSuperclass()); } /** @@ -348,8 +393,8 @@ public abstract class AnnotationUtils { * and corresponding attribute values as values * @since 3.1.1 */ - public static AnnotationAttributes getAnnotationAttributes( - Annotation annotation, boolean classValuesAsString, boolean nestedAnnotationsAsMap) { + public static AnnotationAttributes getAnnotationAttributes(Annotation annotation, boolean classValuesAsString, + boolean nestedAnnotationsAsMap) { AnnotationAttributes attrs = new AnnotationAttributes(); Method[] methods = annotation.annotationType().getDeclaredMethods(); @@ -371,15 +416,15 @@ public abstract class AnnotationUtils { } } if (nestedAnnotationsAsMap && value instanceof Annotation) { - attrs.put(method.getName(), getAnnotationAttributes( - (Annotation)value, classValuesAsString, nestedAnnotationsAsMap)); + attrs.put(method.getName(), + getAnnotationAttributes((Annotation) value, classValuesAsString, nestedAnnotationsAsMap)); } else if (nestedAnnotationsAsMap && value instanceof Annotation[]) { - Annotation[] realAnnotations = (Annotation[])value; + Annotation[] realAnnotations = (Annotation[]) value; AnnotationAttributes[] mappedAnnotations = new AnnotationAttributes[realAnnotations.length]; for (int i = 0; i < realAnnotations.length; i++) { - mappedAnnotations[i] = getAnnotationAttributes( - realAnnotations[i], classValuesAsString, nestedAnnotationsAsMap); + mappedAnnotations[i] = getAnnotationAttributes(realAnnotations[i], classValuesAsString, + nestedAnnotationsAsMap); } attrs.put(method.getName(), mappedAnnotations); } 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 0c5d88f3343..9f7e76b0c92 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-2012 the original author or authors. + * Copyright 2002-2013 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,21 +16,26 @@ package org.springframework.core.annotation; +import static org.junit.Assert.*; +import static org.springframework.core.annotation.AnnotationUtils.*; + +import java.lang.annotation.Annotation; import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.List; + import org.junit.Test; import org.springframework.core.Ordered; import org.springframework.stereotype.Component; -import static org.junit.Assert.*; - -import static org.springframework.core.annotation.AnnotationUtils.*; - /** + * Unit tests for {@link AnnotationUtils}. + * * @author Rod Johnson * @author Juergen Hoeller * @author Sam Brannen @@ -102,23 +107,91 @@ public class AnnotationUtilsTests { assertNull(findAnnotationDeclaringClass(Transactional.class, NonAnnotatedClass.class)); // inherited class-level annotation; note: @Transactional is inherited - assertEquals(InheritedAnnotationInterface.class, findAnnotationDeclaringClass(Transactional.class, - InheritedAnnotationInterface.class)); + assertEquals(InheritedAnnotationInterface.class, + findAnnotationDeclaringClass(Transactional.class, InheritedAnnotationInterface.class)); assertNull(findAnnotationDeclaringClass(Transactional.class, SubInheritedAnnotationInterface.class)); - assertEquals(InheritedAnnotationClass.class, findAnnotationDeclaringClass(Transactional.class, - InheritedAnnotationClass.class)); - assertEquals(InheritedAnnotationClass.class, findAnnotationDeclaringClass(Transactional.class, - SubInheritedAnnotationClass.class)); + assertEquals(InheritedAnnotationClass.class, + findAnnotationDeclaringClass(Transactional.class, InheritedAnnotationClass.class)); + assertEquals(InheritedAnnotationClass.class, + findAnnotationDeclaringClass(Transactional.class, SubInheritedAnnotationClass.class)); // non-inherited class-level annotation; note: @Order is not inherited, - // but findAnnotationDeclaringClass() should still find it. - assertEquals(NonInheritedAnnotationInterface.class, findAnnotationDeclaringClass(Order.class, - NonInheritedAnnotationInterface.class)); + // but findAnnotationDeclaringClass() should still find it on classes. + assertEquals(NonInheritedAnnotationInterface.class, + findAnnotationDeclaringClass(Order.class, NonInheritedAnnotationInterface.class)); assertNull(findAnnotationDeclaringClass(Order.class, SubNonInheritedAnnotationInterface.class)); - assertEquals(NonInheritedAnnotationClass.class, findAnnotationDeclaringClass(Order.class, - NonInheritedAnnotationClass.class)); - assertEquals(NonInheritedAnnotationClass.class, findAnnotationDeclaringClass(Order.class, - SubNonInheritedAnnotationClass.class)); + assertEquals(NonInheritedAnnotationClass.class, + findAnnotationDeclaringClass(Order.class, NonInheritedAnnotationClass.class)); + assertEquals(NonInheritedAnnotationClass.class, + findAnnotationDeclaringClass(Order.class, SubNonInheritedAnnotationClass.class)); + } + + @Test + public void findAnnotationDeclaringClassForTypesWithSingleCandidateType() { + + // no class-level annotation + List> transactionalCandidateList = Arrays.> asList(Transactional.class); + assertNull(findAnnotationDeclaringClassForTypes(transactionalCandidateList, NonAnnotatedInterface.class)); + assertNull(findAnnotationDeclaringClassForTypes(transactionalCandidateList, NonAnnotatedClass.class)); + + // inherited class-level annotation; note: @Transactional is inherited + assertEquals(InheritedAnnotationInterface.class, + findAnnotationDeclaringClassForTypes(transactionalCandidateList, InheritedAnnotationInterface.class)); + assertNull(findAnnotationDeclaringClassForTypes(transactionalCandidateList, + SubInheritedAnnotationInterface.class)); + assertEquals(InheritedAnnotationClass.class, + findAnnotationDeclaringClassForTypes(transactionalCandidateList, InheritedAnnotationClass.class)); + assertEquals(InheritedAnnotationClass.class, + findAnnotationDeclaringClassForTypes(transactionalCandidateList, SubInheritedAnnotationClass.class)); + + // non-inherited class-level annotation; note: @Order is not inherited, + // but findAnnotationDeclaringClassForTypes() should still find it on classes. + List> orderCandidateList = Arrays.> asList(Order.class); + assertEquals(NonInheritedAnnotationInterface.class, + findAnnotationDeclaringClassForTypes(orderCandidateList, NonInheritedAnnotationInterface.class)); + assertNull(findAnnotationDeclaringClassForTypes(orderCandidateList, SubNonInheritedAnnotationInterface.class)); + assertEquals(NonInheritedAnnotationClass.class, + findAnnotationDeclaringClassForTypes(orderCandidateList, NonInheritedAnnotationClass.class)); + assertEquals(NonInheritedAnnotationClass.class, + findAnnotationDeclaringClassForTypes(orderCandidateList, SubNonInheritedAnnotationClass.class)); + } + + @Test + public void findAnnotationDeclaringClassForTypesWithMultipleCandidateTypes() { + + List> candidates = Arrays.> asList(Transactional.class, + Order.class); + + // no class-level annotation + assertNull(findAnnotationDeclaringClassForTypes(candidates, NonAnnotatedInterface.class)); + assertNull(findAnnotationDeclaringClassForTypes(candidates, NonAnnotatedClass.class)); + + // inherited class-level annotation; note: @Transactional is inherited + assertEquals(InheritedAnnotationInterface.class, + findAnnotationDeclaringClassForTypes(candidates, InheritedAnnotationInterface.class)); + assertNull(findAnnotationDeclaringClassForTypes(candidates, SubInheritedAnnotationInterface.class)); + assertEquals(InheritedAnnotationClass.class, + findAnnotationDeclaringClassForTypes(candidates, InheritedAnnotationClass.class)); + assertEquals(InheritedAnnotationClass.class, + findAnnotationDeclaringClassForTypes(candidates, SubInheritedAnnotationClass.class)); + + // non-inherited class-level annotation; note: @Order is not inherited, + // but findAnnotationDeclaringClassForTypes() should still find it on classes. + assertEquals(NonInheritedAnnotationInterface.class, + findAnnotationDeclaringClassForTypes(candidates, NonInheritedAnnotationInterface.class)); + assertNull(findAnnotationDeclaringClassForTypes(candidates, SubNonInheritedAnnotationInterface.class)); + assertEquals(NonInheritedAnnotationClass.class, + findAnnotationDeclaringClassForTypes(candidates, NonInheritedAnnotationClass.class)); + assertEquals(NonInheritedAnnotationClass.class, + findAnnotationDeclaringClassForTypes(candidates, SubNonInheritedAnnotationClass.class)); + + // class hierarchy mixed with @Transactional and @Order declarations + assertEquals(TransactionalClass.class, + findAnnotationDeclaringClassForTypes(candidates, TransactionalClass.class)); + assertEquals(TransactionalAndOrderedClass.class, + findAnnotationDeclaringClassForTypes(candidates, TransactionalAndOrderedClass.class)); + assertEquals(TransactionalAndOrderedClass.class, + findAnnotationDeclaringClassForTypes(candidates, SubTransactionalAndOrderedClass.class)); } @Test @@ -216,18 +289,18 @@ public class AnnotationUtilsTests { } - @Component(value="meta1") + @Component(value = "meta1") @Retention(RetentionPolicy.RUNTIME) @interface Meta1 { } - @Component(value="meta2") + @Component(value = "meta2") @Retention(RetentionPolicy.RUNTIME) @interface Meta2 { } @Meta1 - @Component(value="local") + @Component(value = "local") @Meta2 static class HasLocalAndMetaComponentAnnotation { } @@ -332,6 +405,16 @@ public class AnnotationUtilsTests { public static class SubNonInheritedAnnotationClass extends NonInheritedAnnotationClass { } + @Transactional + public static class TransactionalClass { + } + + @Order + public static class TransactionalAndOrderedClass { + } + + public static class SubTransactionalAndOrderedClass extends TransactionalAndOrderedClass { + } public static interface InterfaceWithAnnotatedMethod { @@ -353,10 +436,12 @@ public class AnnotationUtilsTests { } } - public abstract static class AbstractDoesNotImplementInterfaceWithAnnotatedMethod implements InterfaceWithAnnotatedMethod { + public abstract static class AbstractDoesNotImplementInterfaceWithAnnotatedMethod implements + InterfaceWithAnnotatedMethod { } - public static class SubOfAbstractImplementsInterfaceWithAnnotatedMethod extends AbstractDoesNotImplementInterfaceWithAnnotatedMethod { + public static class SubOfAbstractImplementsInterfaceWithAnnotatedMethod extends + AbstractDoesNotImplementInterfaceWithAnnotatedMethod { @Override public void foo() { diff --git a/spring-test/.springBeans b/spring-test/.springBeans index 656622651c9..aa53e0e6588 100644 --- a/spring-test/.springBeans +++ b/spring-test/.springBeans @@ -10,6 +10,7 @@ src/test/java/org/springframework/test/context/junit4/profile/xml/DefaultProfileXmlConfigTests-context.xml src/test/java/org/springframework/test/context/junit4/aci/xml/MultipleInitializersXmlConfigTests-context.xml src/test/resources/org/springframework/test/context/web/RequestAndSessionScopedBeansWacTests-context.xml + src/test/resources/org/springframework/test/context/hierarchies/web/DispatcherWacRootWacEarTests-context.xml diff --git a/spring-test/src/main/java/org/springframework/mock/jndi/SimpleNamingContextBuilder.java b/spring-test/src/main/java/org/springframework/mock/jndi/SimpleNamingContextBuilder.java index 449c45facaf..5d2f70d5198 100644 --- a/spring-test/src/main/java/org/springframework/mock/jndi/SimpleNamingContextBuilder.java +++ b/spring-test/src/main/java/org/springframework/mock/jndi/SimpleNamingContextBuilder.java @@ -40,9 +40,9 @@ import org.springframework.util.ClassUtils; * *

There are various choices for DataSource implementations: *

* *

Typical usage in bootstrap code: @@ -77,7 +77,6 @@ import org.springframework.util.ClassUtils; * @see SimpleNamingContext * @see org.springframework.jdbc.datasource.SingleConnectionDataSource * @see org.springframework.jdbc.datasource.DriverManagerDataSource - * @see org.apache.commons.dbcp.BasicDataSource */ public class SimpleNamingContextBuilder implements InitialContextFactoryBuilder { diff --git a/spring-test/src/main/java/org/springframework/mock/web/MockServletContext.java b/spring-test/src/main/java/org/springframework/mock/web/MockServletContext.java index 40fbabd1a17..76b65c3904c 100644 --- a/spring-test/src/main/java/org/springframework/mock/web/MockServletContext.java +++ b/spring-test/src/main/java/org/springframework/mock/web/MockServletContext.java @@ -163,6 +163,7 @@ public class MockServletContext implements ServletContext { * @param resourceLoader the ResourceLoader to use (or null for the default) * @see #registerNamedDispatcher */ + @SuppressWarnings("javadoc") public MockServletContext(String resourceBasePath, ResourceLoader resourceLoader) { this.resourceLoader = (resourceLoader != null ? resourceLoader : new DefaultResourceLoader()); this.resourceBasePath = (resourceBasePath != null ? resourceBasePath : ""); @@ -344,6 +345,7 @@ public class MockServletContext implements ServletContext { *

Defaults to {@linkplain #COMMON_DEFAULT_SERVLET_NAME "default"}. * @see #setDefaultServletName */ + @SuppressWarnings("javadoc") public String getDefaultServletName() { return this.defaultServletName; } diff --git a/spring-test/src/main/java/org/springframework/test/AbstractDependencyInjectionSpringContextTests.java b/spring-test/src/main/java/org/springframework/test/AbstractDependencyInjectionSpringContextTests.java index 0ef706abe11..6229a8d3841 100644 --- a/spring-test/src/main/java/org/springframework/test/AbstractDependencyInjectionSpringContextTests.java +++ b/spring-test/src/main/java/org/springframework/test/AbstractDependencyInjectionSpringContextTests.java @@ -197,6 +197,7 @@ public abstract class AbstractDependencyInjectionSpringContextTests extends Abst * test instance has not been configured * @see #populateProtectedVariables() */ + @SuppressWarnings("javadoc") protected void injectDependencies() throws Exception { Assert.state(getApplicationContext() != null, "injectDependencies() called without first configuring an ApplicationContext"); diff --git a/spring-test/src/main/java/org/springframework/test/annotation/DirtiesContext.java b/spring-test/src/main/java/org/springframework/test/annotation/DirtiesContext.java index 53a9b877e64..aa6ed211370 100644 --- a/spring-test/src/main/java/org/springframework/test/annotation/DirtiesContext.java +++ b/spring-test/src/main/java/org/springframework/test/annotation/DirtiesContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2013 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,13 +36,13 @@ import java.lang.annotation.Target; * mode set to {@link ClassMode#AFTER_CLASS AFTER_CLASS} * *

- * Use this annotation if a test has modified the context (for example, by - * replacing a bean definition). Subsequent tests will be supplied a new - * context. + * Use this annotation if a test has modified the context — for example, by + * replacing a bean definition or changing the state of a singleton bean. + * Subsequent tests will be supplied a new context. *

*

- * {@code @DirtiesContext} may be used as a class-level and - * method-level annotation within the same class. In such scenarios, the + * {@code @DirtiesContext} may be used as a class-level and method-level + * annotation within the same class. In such scenarios, the * {@code ApplicationContext} will be marked as dirty after any * such annotated method as well as after the entire class. If the * {@link ClassMode} is set to {@link ClassMode#AFTER_EACH_TEST_METHOD @@ -53,16 +53,19 @@ import java.lang.annotation.Target; * @author Sam Brannen * @author Rod Johnson * @since 2.0 + * @see org.springframework.test.context.ContextConfiguration */ @Documented @Inherited @Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.TYPE, ElementType.METHOD}) +@Target({ ElementType.TYPE, ElementType.METHOD }) public @interface DirtiesContext { /** - * Defines modes which determine how {@code @DirtiesContext} - * is interpreted when used to annotate a test class. + * Defines modes which determine how {@code @DirtiesContext} is + * interpreted when used to annotate a test class. + * + * @since 3.0 */ static enum ClassMode { @@ -76,18 +79,64 @@ public @interface DirtiesContext { * The associated {@code ApplicationContext} will be marked as * dirty after each test method in the class. */ - AFTER_EACH_TEST_METHOD + AFTER_EACH_TEST_METHOD; + } + + /** + * Defines modes which determine how the context cache is cleared + * when {@code @DirtiesContext} is used in a test whose context is + * configured as part of a hierarchy via + * {@link org.springframework.test.context.ContextHierarchy @ContextHierarchy}. + * + * @since 3.2.2 + */ + static enum HierarchyMode { + + /** + * The context cache will be cleared using an exhaustive algorithm + * that includes not only the {@linkplain HierarchyMode#CURRENT_LEVEL current level} + * but also all other context hierarchies that share an ancestor context + * common to the current test. + * + *

All {@code ApplicationContexts} that reside in a subhierarchy of + * the common ancestor context will be removed from the context cache and + * closed. + */ + EXHAUSTIVE, + + /** + * The {@code ApplicationContext} for the current level in the + * context hierarchy and all contexts in subhierarchies of the current + * level will be removed from the context cache and closed. + * + *

The current level refers to the {@code ApplicationContext} + * at the lowest level in the context hierarchy that is visible from the + * current test. + */ + CURRENT_LEVEL; } /** * The mode to use when a test class is annotated with - * {@code @DirtiesContext}. + * {@code @DirtiesContext}. *

Defaults to {@link ClassMode#AFTER_CLASS AFTER_CLASS}. *

Note: Setting the class mode on an annotated test method has no meaning, - * since the mere presence of the {@code @DirtiesContext} - * annotation on a test method is sufficient. + * since the mere presence of the {@code @DirtiesContext} annotation on a + * test method is sufficient. + * + * @since 3.0 */ ClassMode classMode() default ClassMode.AFTER_CLASS; + /** + * The context cache clearing mode to use when a context is + * configured as part of a hierarchy via + * {@link org.springframework.test.context.ContextHierarchy @ContextHierarchy}. + *

Defaults to {@link HierarchyMode#EXHAUSTIVE EXHAUSTIVE}. + * + * @since 3.2.2 + */ + HierarchyMode hierarchyMode() default HierarchyMode.EXHAUSTIVE; + } diff --git a/spring-test/src/main/java/org/springframework/test/context/CacheAwareContextLoaderDelegate.java b/spring-test/src/main/java/org/springframework/test/context/CacheAwareContextLoaderDelegate.java new file mode 100644 index 00000000000..f7375def035 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/CacheAwareContextLoaderDelegate.java @@ -0,0 +1,112 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.context; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.util.Assert; + +/** + * {@code CacheAwareContextLoaderDelegate} loads application contexts from + * {@link MergedContextConfiguration} by delegating to the + * {@link ContextLoader} configured in the {@code MergedContextConfiguration} + * and interacting transparently with the {@link ContextCache} behind the scenes. + * + *

Note: {@code CacheAwareContextLoaderDelegate} does not implement the + * {@link ContextLoader} or {@link SmartContextLoader} interface. + * + * @author Sam Brannen + * @since 3.2.2 + */ +public class CacheAwareContextLoaderDelegate { + + private static final Log logger = LogFactory.getLog(CacheAwareContextLoaderDelegate.class); + + private final ContextCache contextCache; + + + CacheAwareContextLoaderDelegate(ContextCache contextCache) { + Assert.notNull(contextCache, "ContextCache must not be null"); + this.contextCache = contextCache; + } + + /** + * Load the {@code ApplicationContext} for the supplied merged context + * configuration. Supports both the {@link SmartContextLoader} and + * {@link ContextLoader} SPIs. + * @throws Exception if an error occurs while loading the application context + */ + private ApplicationContext loadContextInternal(MergedContextConfiguration mergedContextConfiguration) + throws Exception { + ContextLoader contextLoader = mergedContextConfiguration.getContextLoader(); + Assert.notNull(contextLoader, "Cannot load an ApplicationContext with a NULL 'contextLoader'. " + + "Consider annotating your test class with @ContextConfiguration or @ContextHierarchy."); + + ApplicationContext applicationContext; + + if (contextLoader instanceof SmartContextLoader) { + SmartContextLoader smartContextLoader = (SmartContextLoader) contextLoader; + applicationContext = smartContextLoader.loadContext(mergedContextConfiguration); + } + else { + String[] locations = mergedContextConfiguration.getLocations(); + Assert.notNull(locations, "Cannot load an ApplicationContext with a NULL 'locations' array. " + + "Consider annotating your test class with @ContextConfiguration or @ContextHierarchy."); + applicationContext = contextLoader.loadContext(locations); + } + + return applicationContext; + } + + /** + * Load the {@link ApplicationContext application context} for the supplied + * merged context configuration. + * + *

If the context is present in the cache it will simply be returned; + * otherwise, it will be loaded, stored in the cache, and returned. + * @return the application context + * @throws IllegalStateException if an error occurs while retrieving or + * loading the application context + */ + public ApplicationContext loadContext(MergedContextConfiguration mergedContextConfiguration) { + synchronized (contextCache) { + ApplicationContext context = contextCache.get(mergedContextConfiguration); + if (context == null) { + try { + context = loadContextInternal(mergedContextConfiguration); + if (logger.isDebugEnabled()) { + logger.debug(String.format("Storing ApplicationContext in cache under key [%s].", + mergedContextConfiguration)); + } + contextCache.put(mergedContextConfiguration, context); + } + catch (Exception ex) { + throw new IllegalStateException("Failed to load ApplicationContext", ex); + } + } + else { + if (logger.isDebugEnabled()) { + logger.debug(String.format("Retrieved ApplicationContext from cache with key [%s].", + mergedContextConfiguration)); + } + } + return context; + } + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/ContextCache.java b/spring-test/src/main/java/org/springframework/test/context/ContextCache.java index 32851cf5ed3..3a5a5b1adbd 100644 --- a/spring-test/src/main/java/org/springframework/test/context/ContextCache.java +++ b/spring-test/src/main/java/org/springframework/test/context/ContextCache.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2013 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,24 +16,30 @@ package org.springframework.test.context; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import org.springframework.context.ApplicationContext; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.core.style.ToStringCreator; +import org.springframework.test.annotation.DirtiesContext.HierarchyMode; import org.springframework.util.Assert; /** * Cache for Spring {@link ApplicationContext ApplicationContexts} in a test environment. * - *

Maintains a cache of {@link ApplicationContext contexts} keyed by - * {@link MergedContextConfiguration} instances. This has significant performance - * benefits if initializing the context would take time. While initializing a - * Spring context itself is very quick, some beans in a context, such as a - * {@code LocalSessionFactoryBean} for working with Hibernate, may take some time - * to initialize. Hence it often makes sense to perform that initialization only - * once per test suite. + *

Maintains a cache of {@code ApplicationContexts} keyed by + * {@link MergedContextConfiguration} instances. + * + *

This has significant performance benefits if initializing the context would take time. + * While initializing a Spring context itself is very quick, some beans in a context, such + * as a {@code LocalSessionFactoryBean} for working with Hibernate, may take some time to + * initialize. Hence it often makes sense to perform that initialization only once per + * test suite. * * @author Sam Brannen * @author Juergen Hoeller @@ -41,11 +47,22 @@ import org.springframework.util.Assert; */ class ContextCache { + private final Object monitor = new Object(); + + /** + * Map of context keys to Spring {@code ApplicationContext} instances. + */ + private final Map contextMap = new ConcurrentHashMap( + 64); + /** - * Map of context keys to Spring ApplicationContext instances. + * Map of parent keys to sets of children keys, representing a top-down tree + * of context hierarchies. This information is used for determining which subtrees + * need to be recursively removed and closed when removing a context that is a parent + * of other contexts. */ - private final Map contextMap = - new ConcurrentHashMap(64); + private final Map> hierarchyMap = new ConcurrentHashMap>( + 64); private int hitCount; @@ -53,15 +70,18 @@ class ContextCache { /** - * Clears all contexts from the cache. + * Clears all contexts from the cache and clears context hierarchy information as + * well. */ void clear() { - this.contextMap.clear(); + synchronized (monitor) { + this.contextMap.clear(); + this.hierarchyMap.clear(); + } } /** - * Clears hit and miss count statistics for the cache (i.e., resets counters - * to zero). + * Clears hit and miss count statistics for the cache (i.e., resets counters to zero). */ void clearStatistics() { this.hitCount = 0; @@ -70,124 +90,210 @@ class ContextCache { /** * Return whether there is a cached context for the given key. + * * @param key the context key (never {@code null}) */ boolean contains(MergedContextConfiguration key) { Assert.notNull(key, "Key must not be null"); - return this.contextMap.containsKey(key); + synchronized (monitor) { + return this.contextMap.containsKey(key); + } } /** - * Obtain a cached ApplicationContext for the given key. - *

The {@link #getHitCount() hit} and {@link #getMissCount() miss} - * counts will be updated accordingly. + * Obtain a cached {@code ApplicationContext} for the given key. + * + *

The {@link #getHitCount() hit} and {@link #getMissCount() miss} counts will be + * updated accordingly. + * * @param key the context key (never {@code null}) - * @return the corresponding ApplicationContext instance, - * or {@code null} if not found in the cache. + * @return the corresponding {@code ApplicationContext} instance, or {@code null} if + * not found in the cache. * @see #remove */ ApplicationContext get(MergedContextConfiguration key) { Assert.notNull(key, "Key must not be null"); - ApplicationContext context = this.contextMap.get(key); - if (context == null) { - incrementMissCount(); + synchronized (monitor) { + ApplicationContext context = this.contextMap.get(key); + if (context == null) { + incrementMissCount(); + } + else { + incrementHitCount(); + } + return context; } - else { - incrementHitCount(); - } - return context; } /** - * Increment the hit count by one. A hit is an access to the - * cache, which returned a non-null context for a queried key. + * Increment the hit count by one. A hit is an access to the cache, which + * returned a non-null context for a queried key. */ private void incrementHitCount() { this.hitCount++; } /** - * Increment the miss count by one. A miss is an access to the - * cache, which returned a {@code null} context for a queried key. + * Increment the miss count by one. A miss is an access to the cache, which + * returned a {@code null} context for a queried key. */ private void incrementMissCount() { this.missCount++; } /** - * Get the overall hit count for this cache. A hit is an access - * to the cache, which returned a non-null context for a queried key. + * Get the overall hit count for this cache. A hit is an access to the cache, + * which returned a non-null context for a queried key. */ int getHitCount() { return this.hitCount; } /** - * Get the overall miss count for this cache. A miss is an - * access to the cache, which returned a {@code null} context for a - * queried key. + * Get the overall miss count for this cache. A miss is an access to the + * cache, which returned a {@code null} context for a queried key. */ int getMissCount() { return this.missCount; } /** - * Explicitly add an ApplicationContext instance to the cache under the given key. + * Explicitly add an {@code ApplicationContext} instance to the cache under the given + * key. + * * @param key the context key (never {@code null}) - * @param context the ApplicationContext instance (never {@code null}) + * @param context the {@code ApplicationContext} instance (never {@code null}) */ void put(MergedContextConfiguration key, ApplicationContext context) { Assert.notNull(key, "Key must not be null"); Assert.notNull(context, "ApplicationContext must not be null"); - this.contextMap.put(key, context); - } - /** - * Remove the context with the given key. - * @param key the context key (never {@code null}) - * @return the corresponding ApplicationContext instance, or {@code null} - * if not found in the cache. - * @see #setDirty - */ - ApplicationContext remove(MergedContextConfiguration key) { - return this.contextMap.remove(key); + synchronized (monitor) { + this.contextMap.put(key, context); + + MergedContextConfiguration child = key; + MergedContextConfiguration parent = child.getParent(); + while (parent != null) { + Set list = hierarchyMap.get(parent); + if (list == null) { + list = new HashSet(); + hierarchyMap.put(parent, list); + } + list.add(child); + child = parent; + parent = child.getParent(); + } + } } /** - * Mark the context with the given key as dirty, effectively - * {@link #remove removing} the context from the cache and explicitly - * {@link ConfigurableApplicationContext#close() closing} it if it is an + * Remove the context with the given key from the cache and explicitly + * {@linkplain ConfigurableApplicationContext#close() close} it if it is an * instance of {@link ConfigurableApplicationContext}. + * *

Generally speaking, you would only call this method if you change the * state of a singleton bean, potentially affecting future interaction with * the context. - * @param key the context key (never {@code null}) - * @see #remove + * + *

In addition, the semantics of the supplied {@code HierarchyMode} will + * be honored. See the Javadoc for {@link HierarchyMode} for details. + * + * @param key the context key; never {@code null} + * @param hierarchyMode the hierarchy mode; may be {@code null} if the context + * is not part of a hierarchy */ - void setDirty(MergedContextConfiguration key) { + void remove(MergedContextConfiguration key, HierarchyMode hierarchyMode) { Assert.notNull(key, "Key must not be null"); - ApplicationContext context = remove(key); - if (context instanceof ConfigurableApplicationContext) { - ((ConfigurableApplicationContext) context).close(); + + // startKey is the level at which to begin clearing the cache, depending + // on the configured hierarchy mode. + MergedContextConfiguration startKey = key; + if (hierarchyMode == HierarchyMode.EXHAUSTIVE) { + while (startKey.getParent() != null) { + startKey = startKey.getParent(); + } + } + + synchronized (monitor) { + final List removedContexts = new ArrayList(); + + remove(removedContexts, startKey); + + // Remove all remaining references to any removed contexts from the + // hierarchy map. + for (MergedContextConfiguration currentKey : removedContexts) { + for (Set children : hierarchyMap.values()) { + children.remove(currentKey); + } + } + + // Remove empty entries from the hierarchy map. + for (MergedContextConfiguration currentKey : hierarchyMap.keySet()) { + if (hierarchyMap.get(currentKey).isEmpty()) { + hierarchyMap.remove(currentKey); + } + } + } + } + + private void remove(List removedContexts, MergedContextConfiguration key) { + Assert.notNull(key, "Key must not be null"); + + synchronized (monitor) { + Set children = hierarchyMap.get(key); + if (children != null) { + for (MergedContextConfiguration child : children) { + // Recurse through lower levels + remove(removedContexts, child); + } + // Remove the set of children for the current context from the + // hierarchy map. + hierarchyMap.remove(key); + } + + // Physically remove and close leaf nodes first (i.e., on the way back up the + // stack as opposed to prior to the recursive call). + ApplicationContext context = contextMap.remove(key); + if (context instanceof ConfigurableApplicationContext) { + ((ConfigurableApplicationContext) context).close(); + } + removedContexts.add(key); } } /** - * Determine the number of contexts currently stored in the cache. If the - * cache contains more than Integer.MAX_VALUE elements, returns + * Determine the number of contexts currently stored in the cache. If the cache + * contains more than Integer.MAX_VALUE elements, returns * Integer.MAX_VALUE. */ int size() { - return this.contextMap.size(); + synchronized (monitor) { + return this.contextMap.size(); + } + } + + /** + * Determine the number of parent contexts currently tracked within the cache. + */ + int getParentContextCount() { + synchronized (monitor) { + return this.hierarchyMap.size(); + } } /** - * Generates a text string, which contains the {@link #size() size} as well - * as the {@link #hitCount hit} and {@link #missCount miss} counts. + * Generates a text string, which contains the {@linkplain #size() size} as well + * as the {@linkplain #getHitCount() hit}, {@linkplain #getMissCount() miss}, and + * {@linkplain #getParentContextCount() parent context} counts. */ + @Override public String toString() { - return new ToStringCreator(this).append("size", size()).append("hitCount", getHitCount()). - append("missCount", getMissCount()).toString(); + return new ToStringCreator(this)// + .append("size", size())// + .append("hitCount", getHitCount())// + .append("missCount", getMissCount())// + .append("parentContextCount", getParentContextCount())// + .toString(); } } 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 a23aed849c7..b86032a7d24 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2013 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -66,11 +66,12 @@ import org.springframework.context.ConfigurableApplicationContext; * * @author Sam Brannen * @since 2.5 + * @see ContextHierarchy + * @see ActiveProfiles * @see ContextLoader * @see SmartContextLoader * @see ContextConfigurationAttributes * @see MergedContextConfiguration - * @see ActiveProfiles * @see org.springframework.context.ApplicationContext */ @Documented @@ -283,4 +284,19 @@ public @interface ContextConfiguration { */ Class loader() default ContextLoader.class; + /** + * The name of the context hierarchy level represented by this configuration. + * + *

If not specified the name will be inferred based on the numerical level within all + * declared contexts within the hierarchy. + * + *

This attribute is only applicable when used within a test class hierarchy that is + * configured using {@link ContextHierarchy @ContextHierarchy}, in which case the name + * can be used for merging or overriding this configuration with configuration of the + * same name in hierarchy levels defined in superclasses. + * + * @since 3.2.2 + */ + String name() 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 5a481c190de..035483a71e0 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-2012 the original author or authors. + * Copyright 2002-2013 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,8 @@ package org.springframework.test.context; +import java.util.Arrays; + import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -24,6 +26,7 @@ import org.springframework.context.ConfigurableApplicationContext; import org.springframework.core.style.ToStringCreator; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; /** * {@code ContextConfigurationAttributes} encapsulates the context @@ -54,6 +57,8 @@ public class ContextConfigurationAttributes { private final boolean inheritInitializers; + private final String name; + /** * Resolve resource locations from the {@link ContextConfiguration#locations() locations} @@ -75,7 +80,8 @@ public class ContextConfigurationAttributes { ObjectUtils.nullSafeToString(valueLocations), ObjectUtils.nullSafeToString(locations)); logger.error(msg); throw new IllegalStateException(msg); - } else if (!ObjectUtils.isEmpty(valueLocations)) { + } + else if (!ObjectUtils.isEmpty(valueLocations)) { locations = valueLocations; } @@ -92,7 +98,7 @@ public class ContextConfigurationAttributes { public ContextConfigurationAttributes(Class declaringClass, ContextConfiguration contextConfiguration) { this(declaringClass, resolveLocations(declaringClass, contextConfiguration), contextConfiguration.classes(), contextConfiguration.inheritLocations(), contextConfiguration.initializers(), - contextConfiguration.inheritInitializers(), contextConfiguration.loader()); + contextConfiguration.inheritInitializers(), contextConfiguration.name(), contextConfiguration.loader()); } /** @@ -109,13 +115,13 @@ public class ContextConfigurationAttributes { * @throws IllegalArgumentException if the {@code declaringClass} or {@code contextLoaderClass} is * {@code null}, or if the {@code locations} and {@code classes} are both non-empty * @deprecated as of Spring 3.2, use - * {@link #ContextConfigurationAttributes(Class, String[], Class[], boolean, Class[], boolean, Class)} + * {@link #ContextConfigurationAttributes(Class, String[], Class[], boolean, Class[], boolean, String, Class)} * instead */ @Deprecated public ContextConfigurationAttributes(Class declaringClass, String[] locations, Class[] classes, boolean inheritLocations, Class contextLoaderClass) { - this(declaringClass, locations, classes, inheritLocations, null, true, contextLoaderClass); + this(declaringClass, locations, classes, inheritLocations, null, true, null, contextLoaderClass); } /** @@ -138,6 +144,31 @@ public class ContextConfigurationAttributes { boolean inheritLocations, Class>[] initializers, boolean inheritInitializers, Class contextLoaderClass) { + this(declaringClass, locations, classes, inheritLocations, initializers, inheritInitializers, null, + contextLoaderClass); + } + + /** + * Construct a new {@link ContextConfigurationAttributes} instance for the + * {@linkplain Class test class} that declared the + * {@link ContextConfiguration @ContextConfiguration} annotation and its + * corresponding attributes. + * + * @param declaringClass the test class that declared {@code @ContextConfiguration} + * @param locations the resource locations declared via {@code @ContextConfiguration} + * @param classes the annotated classes declared via {@code @ContextConfiguration} + * @param inheritLocations the {@code inheritLocations} flag declared via {@code @ContextConfiguration} + * @param initializers the context initializers declared via {@code @ContextConfiguration} + * @param inheritInitializers the {@code inheritInitializers} flag declared via {@code @ContextConfiguration} + * @param name the name of level in the context hierarchy, or {@code null} if not applicable + * @param contextLoaderClass the {@code ContextLoader} class declared via {@code @ContextConfiguration} + * @throws IllegalArgumentException if the {@code declaringClass} or {@code contextLoaderClass} is + * {@code null}, or if the {@code locations} and {@code classes} are both non-empty + */ + public ContextConfigurationAttributes(Class declaringClass, String[] locations, Class[] classes, + boolean inheritLocations, + Class>[] initializers, + boolean inheritInitializers, String name, Class contextLoaderClass) { Assert.notNull(declaringClass, "declaringClass must not be null"); Assert.notNull(contextLoaderClass, "contextLoaderClass must not be null"); @@ -158,6 +189,7 @@ public class ContextConfigurationAttributes { this.inheritLocations = inheritLocations; this.initializers = initializers; this.inheritInitializers = inheritInitializers; + this.name = StringUtils.hasText(name) ? name : null; this.contextLoaderClass = contextLoaderClass; } @@ -305,6 +337,101 @@ public class ContextConfigurationAttributes { return contextLoaderClass; } + /** + * Get the name of the context hierarchy level that was declared via + * {@link ContextConfiguration @ContextConfiguration}. + * + * @return the name of the context hierarchy level or {@code null} if not applicable + * @see ContextConfiguration#name() + * @since 3.2.2 + */ + public String getName() { + return this.name; + } + + /** + * Generate a unique hash code for all properties of this + * {@code ContextConfigurationAttributes} instance excluding the + * {@linkplain #getName() name}. + */ + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + declaringClass.hashCode(); + result = prime * result + Arrays.hashCode(locations); + result = prime * result + Arrays.hashCode(classes); + result = prime * result + (inheritLocations ? 1231 : 1237); + result = prime * result + Arrays.hashCode(initializers); + result = prime * result + (inheritInitializers ? 1231 : 1237); + result = prime * result + contextLoaderClass.hashCode(); + return result; + } + + /** + * Determine if the supplied object is equal to this + * {@code ContextConfigurationAttributes} instance by comparing both object's + * {@linkplain #getDeclaringClass() declaring class}, + * {@linkplain #getLocations() locations}, + * {@linkplain #getClasses() annotated classes}, + * {@linkplain #isInheritLocations() inheritLocations flag}, + * {@linkplain #getInitializers() context initializer classes}, + * {@linkplain #isInheritInitializers() inheritInitializers flag}, and the + * {@link #getContextLoaderClass() ContextLoader class}. + */ + @Override + public boolean equals(Object obj) { + + if (this == obj) { + return true; + } + if (!(obj instanceof ContextConfigurationAttributes)) { + return false; + } + + final ContextConfigurationAttributes that = (ContextConfigurationAttributes) obj; + + if (this.declaringClass == null) { + if (that.declaringClass != null) { + return false; + } + } + else if (!this.declaringClass.equals(that.declaringClass)) { + return false; + } + + if (!Arrays.equals(this.locations, that.locations)) { + return false; + } + + if (!Arrays.equals(this.classes, that.classes)) { + return false; + } + + if (this.inheritLocations != that.inheritLocations) { + return false; + } + + if (!Arrays.equals(this.initializers, that.initializers)) { + return false; + } + + if (this.inheritInitializers != that.inheritInitializers) { + return false; + } + + if (this.contextLoaderClass == null) { + if (that.contextLoaderClass != null) { + return false; + } + } + else if (!this.contextLoaderClass.equals(that.contextLoaderClass)) { + return false; + } + + return true; + } + /** * Provide a String representation of the context configuration attributes * and declaring class. @@ -318,6 +445,7 @@ public class ContextConfigurationAttributes { .append("inheritLocations", inheritLocations)// .append("initializers", ObjectUtils.nullSafeToString(initializers))// .append("inheritInitializers", inheritInitializers)// + .append("name", name)// .append("contextLoaderClass", contextLoaderClass.getName())// .toString(); } diff --git a/spring-test/src/main/java/org/springframework/test/context/ContextHierarchy.java b/spring-test/src/main/java/org/springframework/test/context/ContextHierarchy.java new file mode 100644 index 00000000000..b7846b7a0d8 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/ContextHierarchy.java @@ -0,0 +1,54 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.context; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * {@code @ContextHierarchy} is a class-level annotation that is used to define + * a hierarchy of {@link org.springframework.context.ApplicationContext + * ApplicationContexts} for integration tests. + * + * @author Sam Brannen + * @since 3.2.2 + * @see ContextConfiguration + * @see org.springframework.context.ApplicationContext + */ +@Documented +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface ContextHierarchy { + + /** + * A list of {@link ContextConfiguration @ContextConfiguration} instances, + * each of which defines a level in the context hierarchy. + * + *

If you need to merge or override the configuration for a given level + * of the context hierarchy within a test class hierarchy, you must explicitly + * name that level by supplying the same value to the {@link ContextConfiguration#name + * name} attribute in {@code @ContextConfiguration} at each level in the + * class hierarchy. + */ + ContextConfiguration[] value(); + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/ContextLoaderUtils.java b/spring-test/src/main/java/org/springframework/test/context/ContextLoaderUtils.java index 8016c5d7c77..a43897eb0b6 100644 --- a/spring-test/src/main/java/org/springframework/test/context/ContextLoaderUtils.java +++ b/spring-test/src/main/java/org/springframework/test/context/ContextLoaderUtils.java @@ -16,20 +16,24 @@ package org.springframework.test.context; -import static org.springframework.beans.BeanUtils.*; -import static org.springframework.core.annotation.AnnotationUtils.*; +import static org.springframework.beans.BeanUtils.instantiateClass; +import static org.springframework.core.annotation.AnnotationUtils.findAnnotationDeclaringClass; +import static org.springframework.core.annotation.AnnotationUtils.findAnnotationDeclaringClassForTypes; +import static org.springframework.core.annotation.AnnotationUtils.isAnnotationDeclaredLocally; import java.lang.annotation.Annotation; import java.lang.reflect.Constructor; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.Set; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; - import org.springframework.context.ApplicationContextInitializer; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.core.annotation.AnnotationUtils; @@ -52,10 +56,13 @@ import org.springframework.util.StringUtils; * @see ContextConfigurationAttributes * @see ActiveProfiles * @see ApplicationContextInitializer + * @see ContextHierarchy * @see MergedContextConfiguration */ abstract class ContextLoaderUtils { + static final String GENERATED_CONTEXT_HIERARCHY_LEVEL_PREFIX = "ContextHierarchyLevel#"; + private static final Log logger = LogFactory.getLog(ContextLoaderUtils.class); private static final String DEFAULT_CONTEXT_LOADER_CLASS_NAME = "org.springframework.test.context.support.DelegatingSmartContextLoader"; @@ -70,30 +77,29 @@ abstract class ContextLoaderUtils { } /** - * Resolve the {@link ContextLoader} {@linkplain Class class} to use for the - * supplied list of {@link ContextConfigurationAttributes} and then - * instantiate and return that {@code ContextLoader}. + * Resolve the {@link ContextLoader} {@linkplain Class class} to use for the supplied + * list of {@link ContextConfigurationAttributes} and then instantiate and return that + * {@code ContextLoader}. * - *

If the supplied {@code defaultContextLoaderClassName} is - * {@code null} or empty, depending on the absence or presence - * of {@link org.springframework.test.context.web.WebAppConfiguration @WebAppConfiguration} - * either {@value #DEFAULT_CONTEXT_LOADER_CLASS_NAME} - * or {@value #DEFAULT_WEB_CONTEXT_LOADER_CLASS_NAME} will be used as the - * default context loader class name. For details on the class resolution - * process, see {@link #resolveContextLoaderClass}. + *

If the supplied {@code defaultContextLoaderClassName} is {@code null} or + * empty, depending on the absence or presence of + * {@link org.springframework.test.context.web.WebAppConfiguration @WebAppConfiguration} either + * {@code "org.springframework.test.context.support.DelegatingSmartContextLoader"} or + * {@code "org.springframework.test.context.web.WebDelegatingSmartContextLoader"} will + * be used as the default context loader class name. For details on the class + * resolution process, see {@link #resolveContextLoaderClass}. * - * @param testClass the test class for which the {@code ContextLoader} - * should be resolved; must not be {@code null} - * @param configAttributesList the list of configuration attributes to process; - * must not be {@code null} or empty; must be ordered bottom-up + * @param testClass the test class for which the {@code ContextLoader} should be + * resolved; must not be {@code null} + * @param configAttributesList the list of configuration attributes to process; must + * not be {@code null} or empty; must be ordered bottom-up * (i.e., as if we were traversing up the class hierarchy) - * @param defaultContextLoaderClassName the name of the default - * {@code ContextLoader} class to use; may be {@code null} or empty - * @return the resolved {@code ContextLoader} for the supplied - * {@code testClass} (never {@code null}) + * @param defaultContextLoaderClassName the name of the default {@code ContextLoader} + * class to use; may be {@code null} or empty + * @return the resolved {@code ContextLoader} for the supplied {@code testClass} + * (never {@code null}) * @see #resolveContextLoaderClass */ - @SuppressWarnings("javadoc") static ContextLoader resolveContextLoader(Class testClass, List configAttributesList, String defaultContextLoaderClassName) { Assert.notNull(testClass, "Class must not be null"); @@ -113,36 +119,35 @@ abstract class ContextLoaderUtils { } /** - * Resolve the {@link ContextLoader} {@linkplain Class class} to use for the - * supplied list of {@link ContextConfigurationAttributes}. + * Resolve the {@link ContextLoader} {@linkplain Class class} to use for the supplied + * list of {@link ContextConfigurationAttributes}. * - *

Beginning with the first level in the context configuration attributes - * hierarchy: + *

Beginning with the first level in the context configuration attributes hierarchy: * *

    *
  1. If the {@link ContextConfigurationAttributes#getContextLoaderClass() * contextLoaderClass} property of {@link ContextConfigurationAttributes} is * configured with an explicit class, that class will be returned.
  2. - *
  3. If an explicit {@code ContextLoader} class is not specified at the - * current level in the hierarchy, traverse to the next level in the hierarchy - * and return to step #1.
  4. - *
  5. If no explicit {@code ContextLoader} class is found after traversing - * the hierarchy, an attempt will be made to load and return the class - * with the supplied {@code defaultContextLoaderClassName}.
  6. + *
  7. If an explicit {@code ContextLoader} class is not specified at the current + * level in the hierarchy, traverse to the next level in the hierarchy and return to + * step #1.
  8. + *
  9. If no explicit {@code ContextLoader} class is found after traversing the + * hierarchy, an attempt will be made to load and return the class with the supplied + * {@code defaultContextLoaderClassName}.
  10. *
* - * @param testClass the class for which to resolve the {@code ContextLoader} - * class; must not be {@code null}; only used for logging purposes - * @param configAttributesList the list of configuration attributes to process; - * must not be {@code null} or empty; must be ordered bottom-up + * @param testClass the class for which to resolve the {@code ContextLoader} class; + * must not be {@code null}; only used for logging purposes + * @param configAttributesList the list of configuration attributes to process; must + * not be {@code null} or empty; must be ordered bottom-up * (i.e., as if we were traversing up the class hierarchy) - * @param defaultContextLoaderClassName the name of the default - * {@code ContextLoader} class to use; must not be {@code null} or empty + * @param defaultContextLoaderClassName the name of the default {@code ContextLoader} + * class to use; must not be {@code null} or empty * @return the {@code ContextLoader} class to use for the supplied test class * @throws IllegalArgumentException if {@code @ContextConfiguration} is not * present on the supplied test class - * @throws IllegalStateException if the default {@code ContextLoader} class - * could not be loaded + * @throws IllegalStateException if the default {@code ContextLoader} class could not + * be loaded */ @SuppressWarnings("unchecked") static Class resolveContextLoaderClass(Class testClass, @@ -184,22 +189,201 @@ abstract class ContextLoaderUtils { } /** - * Resolve the list of {@link ContextConfigurationAttributes configuration - * attributes} for the supplied {@link Class class} and its superclasses. + * Convenience method for creating a {@link ContextConfigurationAttributes} instance + * from the supplied {@link ContextConfiguration} and declaring class and then adding + * the attributes to the supplied list. + */ + private static void convertContextConfigToConfigAttributesAndAddToList(ContextConfiguration contextConfiguration, + Class declaringClass, final List attributesList) { + if (logger.isTraceEnabled()) { + logger.trace(String.format("Retrieved @ContextConfiguration [%s] for declaring class [%s].", + contextConfiguration, declaringClass.getName())); + } + + ContextConfigurationAttributes attributes = new ContextConfigurationAttributes(declaringClass, + contextConfiguration); + if (logger.isTraceEnabled()) { + logger.trace("Resolved context configuration attributes: " + attributes); + } + attributesList.add(attributes); + } + + /** + * Resolve the list of lists of {@linkplain ContextConfigurationAttributes context + * configuration attributes} for the supplied {@linkplain Class test class} and its + * superclasses, taking into account context hierarchies declared via + * {@link ContextHierarchy @ContextHierarchy} and + * {@link ContextConfiguration @ContextConfiguration}. + * + *

The outer list represents a top-down ordering of context configuration + * attributes, where each element in the list represents the context configuration + * declared on a given test class in the class hierarchy. Each nested list + * contains the context configuration attributes declared either via a single + * instance of {@code @ContextConfiguration} on the particular class or via + * multiple instances of {@code @ContextConfiguration} declared within a + * single {@code @ContextHierarchy} instance on the particular class. + * Furthermore, each nested list maintains the order in which + * {@code @ContextConfiguration} instances are declared. + * + *

Note that the {@link ContextConfiguration#inheritLocations inheritLocations} and + * {@link ContextConfiguration#inheritInitializers() inheritInitializers} flags of + * {@link ContextConfiguration @ContextConfiguration} will not + * be taken into consideration. If these flags need to be honored, that must be + * handled manually when traversing the nested lists returned by this method. + * + * @param testClass the class for which to resolve the context hierarchy attributes + * (must not be {@code null}) + * @return the list of lists of configuration attributes for the specified class; + * never {@code null} + * @throws IllegalArgumentException if the supplied class is {@code null}; if + * neither {@code @ContextConfiguration} nor {@code @ContextHierarchy} is + * present on the supplied class; if a given class in the class hierarchy + * declares both {@code @ContextConfiguration} and {@code @ContextHierarchy} as + * top-level annotations; or if individual {@code @ContextConfiguration} + * elements within a {@code @ContextHierarchy} declaration on a given class + * in the class hierarchy do not define unique context configuration. + * + * @since 3.2.2 + * @see #buildContextHierarchyMap(Class) + * @see #resolveContextConfigurationAttributes(Class) + */ + static List> resolveContextHierarchyAttributes(Class testClass) { + Assert.notNull(testClass, "Class must not be null"); + + final Class contextConfigType = ContextConfiguration.class; + final Class contextHierarchyType = ContextHierarchy.class; + final List> annotationTypes = Arrays.asList(contextConfigType, contextHierarchyType); + + final List> hierarchyAttributes = new ArrayList>(); + + Class declaringClass = findAnnotationDeclaringClassForTypes(annotationTypes, testClass); + Assert.notNull(declaringClass, String.format( + "Could not find an 'annotation declaring class' for annotation type [%s] or [%s] and test class [%s]", + contextConfigType.getName(), contextHierarchyType.getName(), testClass.getName())); + + while (declaringClass != null) { + + boolean contextConfigDeclaredLocally = isAnnotationDeclaredLocally(contextConfigType, declaringClass); + boolean contextHierarchyDeclaredLocally = isAnnotationDeclaredLocally(contextHierarchyType, declaringClass); + + if (contextConfigDeclaredLocally && contextHierarchyDeclaredLocally) { + String msg = String.format("Test class [%s] has been configured with both @ContextConfiguration " + + "and @ContextHierarchy as class-level annotations. Only one of these annotations may " + + "be declared as a top-level annotation per test class.", declaringClass.getName()); + logger.error(msg); + throw new IllegalStateException(msg); + } + + final List configAttributesList = new ArrayList(); + + if (contextConfigDeclaredLocally) { + ContextConfiguration contextConfiguration = declaringClass.getAnnotation(contextConfigType); + convertContextConfigToConfigAttributesAndAddToList(contextConfiguration, declaringClass, + configAttributesList); + } + else if (contextHierarchyDeclaredLocally) { + ContextHierarchy contextHierarchy = declaringClass.getAnnotation(contextHierarchyType); + for (ContextConfiguration contextConfiguration : contextHierarchy.value()) { + convertContextConfigToConfigAttributesAndAddToList(contextConfiguration, declaringClass, + configAttributesList); + } + + // Check for uniqueness + Set configAttributesSet = new HashSet( + configAttributesList); + if (configAttributesSet.size() != configAttributesList.size()) { + String msg = String.format("The @ContextConfiguration elements configured via " + + "@ContextHierarchy in test class [%s] must define unique contexts to load.", + declaringClass.getName()); + logger.error(msg); + throw new IllegalStateException(msg); + } + } + else { + // This should theoretically actually never happen... + String msg = String.format("Test class [%s] has been configured with neither @ContextConfiguration " + + "nor @ContextHierarchy as a class-level annotation.", declaringClass.getName()); + logger.error(msg); + throw new IllegalStateException(msg); + } + + hierarchyAttributes.add(0, configAttributesList); + + declaringClass = findAnnotationDeclaringClassForTypes(annotationTypes, declaringClass.getSuperclass()); + } + + return hierarchyAttributes; + } + + /** + * Build a context hierarchy map for the supplied {@linkplain Class + * test class} and its superclasses, taking into account context hierarchies + * declared via {@link ContextHierarchy @ContextHierarchy} and + * {@link ContextConfiguration @ContextConfiguration}. + * + *

Each value in the map represents the consolidated list of {@linkplain + * ContextConfigurationAttributes context configuration attributes} for a + * given level in the context hierarchy (potentially across the test class + * hierarchy), keyed by the {@link ContextConfiguration#name() name} of the + * context hierarchy level. + * + *

If a given level in the context hierarchy does not have an explicit + * name (i.e., configured via {@link ContextConfiguration#name}), a name will + * be generated for that hierarchy level by appending the numerical level to + * the {@link #GENERATED_CONTEXT_HIERARCHY_LEVEL_PREFIX}. + * + * @param testClass the class for which to resolve the context hierarchy map + * (must not be {@code null}) + * @return a map of context configuration attributes for the context hierarchy, + * keyed by context hierarchy level name; never {@code null} + * + * @since 3.2.2 + * @see #resolveContextHierarchyAttributes(Class) + */ + static Map> buildContextHierarchyMap(Class testClass) { + final Map> map = new LinkedHashMap>(); + int hierarchyLevel = 1; + + for (List configAttributesList : resolveContextHierarchyAttributes(testClass)) { + for (ContextConfigurationAttributes configAttributes : configAttributesList) { + String name = configAttributes.getName(); + + // Assign a generated name? + if (!StringUtils.hasText(name)) { + name = GENERATED_CONTEXT_HIERARCHY_LEVEL_PREFIX + hierarchyLevel; + } + + // Encountered a new context hierarchy level? + if (!map.containsKey(name)) { + hierarchyLevel++; + map.put(name, new ArrayList()); + } + + map.get(name).add(configAttributes); + } + } + + return map; + } + + /** + * Resolve the list of {@linkplain ContextConfigurationAttributes context + * configuration attributes} for the supplied {@linkplain Class test class} and its + * superclasses. * - *

Note that the {@link ContextConfiguration#inheritLocations - * inheritLocations} and {@link ContextConfiguration#inheritInitializers() - * inheritInitializers} flags of {@link ContextConfiguration - * @ContextConfiguration} will not be taken into - * consideration. If these flags need to be honored, that must be handled - * manually when traversing the list returned by this method. + *

Note that the {@link ContextConfiguration#inheritLocations inheritLocations} and + * {@link ContextConfiguration#inheritInitializers() inheritInitializers} flags of + * {@link ContextConfiguration @ContextConfiguration} will not + * be taken into consideration. If these flags need to be honored, that must be + * handled manually when traversing the list returned by this method. * * @param testClass the class for which to resolve the configuration attributes (must * not be {@code null}) - * @return the list of configuration attributes for the specified class, ordered bottom-up - * (i.e., as if we were traversing up the class hierarchy); never {@code null} - * @throws IllegalArgumentException if the supplied class is {@code null} or - * if {@code @ContextConfiguration} is not present on the supplied class + * @return the list of configuration attributes for the specified class, ordered + * bottom-up (i.e., as if we were traversing up the class hierarchy); + * never {@code null} + * @throws IllegalArgumentException if the supplied class is {@code null} or if + * {@code @ContextConfiguration} is not present on the supplied class */ static List resolveContextConfigurationAttributes(Class testClass) { Assert.notNull(testClass, "Class must not be null"); @@ -214,19 +398,7 @@ abstract class ContextLoaderUtils { while (declaringClass != null) { ContextConfiguration contextConfiguration = declaringClass.getAnnotation(annotationType); - if (logger.isTraceEnabled()) { - logger.trace(String.format("Retrieved @ContextConfiguration [%s] for declaring class [%s].", - contextConfiguration, declaringClass.getName())); - } - - ContextConfigurationAttributes attributes = new ContextConfigurationAttributes(declaringClass, - contextConfiguration); - if (logger.isTraceEnabled()) { - logger.trace("Resolved context configuration attributes: " + attributes); - } - - attributesList.add(attributes); - + convertContextConfigToConfigAttributesAndAddToList(contextConfiguration, declaringClass, attributesList); declaringClass = findAnnotationDeclaringClass(annotationType, declaringClass.getSuperclass()); } @@ -234,22 +406,21 @@ abstract class ContextLoaderUtils { } /** - * Resolve the list of merged {@code ApplicationContextInitializer} classes - * for the supplied list of {@code ContextConfigurationAttributes}. + * Resolve the list of merged {@code ApplicationContextInitializer} classes for the + * supplied list of {@code ContextConfigurationAttributes}. * *

Note that the {@link ContextConfiguration#inheritInitializers inheritInitializers} * flag of {@link ContextConfiguration @ContextConfiguration} will be taken into - * consideration. Specifically, if the {@code inheritInitializers} flag is - * set to {@code true} for a given level in the class hierarchy represented by - * the provided configuration attributes, context initializer classes defined - * at the given level will be merged with those defined in higher levels - * of the class hierarchy. + * consideration. Specifically, if the {@code inheritInitializers} flag is set to + * {@code true} for a given level in the class hierarchy represented by the provided + * configuration attributes, context initializer classes defined at the given level + * will be merged with those defined in higher levels of the class hierarchy. * - * @param configAttributesList the list of configuration attributes to process; - * must not be {@code null} or empty; must be ordered bottom-up + * @param configAttributesList the list of configuration attributes to process; must + * not be {@code null} or empty; must be ordered bottom-up * (i.e., as if we were traversing up the class hierarchy) - * @return the set of merged context initializer classes, including those - * from superclasses if appropriate (never {@code null}) + * @return the set of merged context initializer classes, including those from + * superclasses if appropriate (never {@code null}) * @since 3.2 */ static Set>> resolveInitializerClasses( @@ -278,16 +449,15 @@ abstract class ContextLoaderUtils { /** * Resolve active bean definition profiles for the supplied {@link Class}. * - *

Note that the {@link ActiveProfiles#inheritProfiles inheritProfiles} - * flag of {@link ActiveProfiles @ActiveProfiles} will be taken into - * consideration. Specifically, if the {@code inheritProfiles} flag is - * set to {@code true}, profiles defined in the test class will be - * merged with those defined in superclasses. + *

Note that the {@link ActiveProfiles#inheritProfiles inheritProfiles} flag of + * {@link ActiveProfiles @ActiveProfiles} will be taken into consideration. + * Specifically, if the {@code inheritProfiles} flag is set to {@code true}, profiles + * defined in the test class will be merged with those defined in superclasses. * - * @param testClass the class for which to resolve the active profiles (must - * not be {@code null}) - * @return the set of active profiles for the specified class, including - * active profiles from superclasses if appropriate (never {@code null}) + * @param testClass the class for which to resolve the active profiles (must not be + * {@code null}) + * @return the set of active profiles for the specified class, including active + * profiles from superclasses if appropriate (never {@code null}) * @see ActiveProfiles * @see org.springframework.context.annotation.Profile */ @@ -342,14 +512,72 @@ abstract class ContextLoaderUtils { } /** - * Build the {@link MergedContextConfiguration merged context configuration} - * for the supplied {@link Class testClass} and - * {@code defaultContextLoaderClassName}. + * Build the {@link MergedContextConfiguration merged context configuration} for + * the supplied {@link Class testClass} and {@code defaultContextLoaderClassName}, + * taking into account context hierarchies declared via + * {@link ContextHierarchy @ContextHierarchy} and + * {@link ContextConfiguration @ContextConfiguration}. + * + * @param testClass the test class for which the {@code MergedContextConfiguration} + * should be built (must not be {@code null}) + * @param defaultContextLoaderClassName the name of the default {@code ContextLoader} + * class to use (may be {@code null}) + * @param cacheAwareContextLoaderDelegate the cache-aware context loader delegate to + * be passed to the {@code MergedContextConfiguration} constructor + * @return the merged context configuration + * @see #buildContextHierarchyMap(Class) + * @see #buildMergedContextConfiguration(Class, List, String, MergedContextConfiguration, CacheAwareContextLoaderDelegate) + */ + @SuppressWarnings("javadoc") + static MergedContextConfiguration buildMergedContextConfiguration(Class testClass, + String defaultContextLoaderClassName, CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate) { + + if (testClass.isAnnotationPresent(ContextHierarchy.class)) { + Map> hierarchyMap = buildContextHierarchyMap(testClass); + + MergedContextConfiguration parentConfig = null; + MergedContextConfiguration mergedConfig = null; + + for (List list : hierarchyMap.values()) { + List reversedList = new ArrayList(list); + Collections.reverse(reversedList); + + // Don't use the supplied testClass; instead ensure that we are + // building the MCC for the actual test class that declared the + // configuration for the current level in the context hierarchy. + Assert.notEmpty(reversedList, "ContextConfigurationAttributes list must not be empty"); + Class declaringClass = reversedList.get(0).getDeclaringClass(); + + mergedConfig = buildMergedContextConfiguration(declaringClass, reversedList, + defaultContextLoaderClassName, parentConfig, cacheAwareContextLoaderDelegate); + parentConfig = mergedConfig; + } + + // Return the last level in the context hierarchy + return mergedConfig; + } + else { + return buildMergedContextConfiguration(testClass, resolveContextConfigurationAttributes(testClass), + defaultContextLoaderClassName, null, cacheAwareContextLoaderDelegate); + } + } + + /** + * Build the {@link MergedContextConfiguration merged context configuration} for the + * supplied {@link Class testClass}, context configuration attributes, + * {@code defaultContextLoaderClassName}, and parent context configuration. * * @param testClass the test class for which the {@code MergedContextConfiguration} * should be built (must not be {@code null}) - * @param defaultContextLoaderClassName the name of the default - * {@code ContextLoader} class to use (may be {@code null}) + * @param configAttributesList the list of context configuration attributes for the + * specified test class, ordered bottom-up (i.e., as if we were + * traversing up the class hierarchy); never {@code null} or empty + * @param defaultContextLoaderClassName the name of the default {@code ContextLoader} + * class to use (may be {@code null}) + * @param parentConfig the merged context configuration for the parent application + * context in a context hierarchy, or {@code null} if there is no parent + * @param cacheAwareContextLoaderDelegate the cache-aware context loader delegate to + * be passed to the {@code MergedContextConfiguration} constructor * @return the merged context configuration * @see #resolveContextLoader * @see #resolveContextConfigurationAttributes @@ -358,10 +586,11 @@ abstract class ContextLoaderUtils { * @see #resolveActiveProfiles * @see MergedContextConfiguration */ - static MergedContextConfiguration buildMergedContextConfiguration(Class testClass, - String defaultContextLoaderClassName) { + private static MergedContextConfiguration buildMergedContextConfiguration(final Class testClass, + final List configAttributesList, + final String defaultContextLoaderClassName, MergedContextConfiguration parentConfig, + CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate) { - final List configAttributesList = resolveContextConfigurationAttributes(testClass); final ContextLoader contextLoader = resolveContextLoader(testClass, configAttributesList, defaultContextLoaderClassName); final List locationsList = new ArrayList(); @@ -397,22 +626,21 @@ abstract class ContextLoaderUtils { String[] activeProfiles = resolveActiveProfiles(testClass); MergedContextConfiguration mergedConfig = buildWebMergedContextConfiguration(testClass, locations, classes, - initializerClasses, activeProfiles, contextLoader); + initializerClasses, activeProfiles, contextLoader, cacheAwareContextLoaderDelegate, parentConfig); if (mergedConfig == null) { mergedConfig = new MergedContextConfiguration(testClass, locations, classes, initializerClasses, - activeProfiles, contextLoader); + activeProfiles, contextLoader, cacheAwareContextLoaderDelegate, parentConfig); } return mergedConfig; } /** - * Load the {@link org.springframework.test.context.web.WebAppConfiguration @WebAppConfiguration} + * Load the {@link org.springframework.test.context.web.WebAppConfiguration} * class, using reflection in order to avoid package cycles. * - * @return the {@code @WebAppConfiguration} class or {@code null} if it - * cannot be loaded + * @return the {@code @WebAppConfiguration} class or {@code null} if it cannot be loaded * @since 3.2 */ @SuppressWarnings("unchecked") @@ -431,12 +659,10 @@ abstract class ContextLoaderUtils { } /** - * Attempt to build a {@link org.springframework.test.context.web.WebMergedContextConfiguration - * WebMergedContextConfiguration} from the supplied arguments, using reflection - * in order to avoid package cycles. + * Attempt to build a {@link org.springframework.test.context.web.WebMergedContextConfiguration} + * from the supplied arguments, using reflection in order to avoid package cycles. * - * @return the {@code WebMergedContextConfiguration} or {@code null} if - * it could not be built + * @return the {@code WebMergedContextConfiguration} or {@code null} if it could not be built * @since 3.2 */ @SuppressWarnings("unchecked") @@ -445,7 +671,8 @@ abstract class ContextLoaderUtils { String[] locations, Class[] classes, Set>> initializerClasses, - String[] activeProfiles, ContextLoader contextLoader) { + String[] activeProfiles, ContextLoader contextLoader, + CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate, MergedContextConfiguration parentConfig) { Class webAppConfigClass = loadWebAppConfigurationClass(); @@ -459,11 +686,12 @@ abstract class ContextLoaderUtils { Constructor constructor = ClassUtils.getConstructorIfAvailable( webMergedConfigClass, Class.class, String[].class, Class[].class, Set.class, String[].class, - String.class, ContextLoader.class); + String.class, ContextLoader.class, CacheAwareContextLoaderDelegate.class, + MergedContextConfiguration.class); if (constructor != null) { return instantiateClass(constructor, testClass, locations, classes, initializerClasses, - activeProfiles, resourceBasePath, contextLoader); + activeProfiles, resourceBasePath, contextLoader, cacheAwareContextLoaderDelegate, parentConfig); } } catch (Throwable t) { diff --git a/spring-test/src/main/java/org/springframework/test/context/MergedContextConfiguration.java b/spring-test/src/main/java/org/springframework/test/context/MergedContextConfiguration.java index b4c1f683a2c..befe85c7cba 100644 --- a/spring-test/src/main/java/org/springframework/test/context/MergedContextConfiguration.java +++ b/spring-test/src/main/java/org/springframework/test/context/MergedContextConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2013 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,9 +23,11 @@ import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; +import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextInitializer; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.core.style.ToStringCreator; +import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -72,6 +74,8 @@ public class MergedContextConfiguration implements Serializable { private final Set>> contextInitializerClasses; private final String[] activeProfiles; private final ContextLoader contextLoader; + private final CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate; + private final MergedContextConfiguration parent; private static String[] processLocations(String[] locations) { @@ -149,6 +153,7 @@ public class MergedContextConfiguration implements Serializable { * @param contextInitializerClasses the merged context initializer classes * @param activeProfiles the merged active bean definition profiles * @param contextLoader the resolved {@code ContextLoader} + * @see #MergedContextConfiguration(Class, String[], Class[], Set, String[], ContextLoader, CacheAwareContextLoaderDelegate, MergedContextConfiguration) */ public MergedContextConfiguration( Class testClass, @@ -156,12 +161,48 @@ public class MergedContextConfiguration implements Serializable { Class[] classes, Set>> contextInitializerClasses, String[] activeProfiles, ContextLoader contextLoader) { + this(testClass, locations, classes, contextInitializerClasses, activeProfiles, contextLoader, null, null); + } + + /** + * Create a new {@code MergedContextConfiguration} instance for the + * supplied test class, resource locations, annotated classes, context + * initializers, active profiles, {@code ContextLoader}, and parent + * configuration. + * + *

If a {@code null} value is supplied for {@code locations}, + * {@code classes}, or {@code activeProfiles} an empty array will + * be stored instead. If a {@code null} value is supplied for the + * {@code contextInitializerClasses} an empty set will be stored instead. + * Furthermore, active profiles will be sorted, and duplicate profiles will + * be removed. + * + * @param testClass the test class for which the configuration was merged + * @param locations the merged resource locations + * @param classes the merged annotated classes + * @param contextInitializerClasses the merged context initializer classes + * @param activeProfiles the merged active bean definition profiles + * @param contextLoader the resolved {@code ContextLoader} + * @param cacheAwareContextLoaderDelegate a cache-aware context loader + * delegate with which to retrieve the parent context + * @param parent the parent configuration or {@code null} if there is no parent + * @since 3.2.2 + */ + public MergedContextConfiguration( + Class testClass, + String[] locations, + Class[] classes, + Set>> contextInitializerClasses, + String[] activeProfiles, ContextLoader contextLoader, + CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate, MergedContextConfiguration parent) { this.testClass = testClass; this.locations = processLocations(locations); this.classes = processClasses(classes); this.contextInitializerClasses = processContextInitializerClasses(contextInitializerClasses); this.activeProfiles = processActiveProfiles(activeProfiles); this.contextLoader = contextLoader; + this.cacheAwareContextLoaderDelegate = cacheAwareContextLoaderDelegate; + this.parent = parent; } /** @@ -207,6 +248,39 @@ public class MergedContextConfiguration implements Serializable { return contextLoader; } + /** + * Get the {@link MergedContextConfiguration} for the parent application context in a + * context hierarchy. + * + * @return the parent configuration or {@code null} if there is no parent + * @see #getParentApplicationContext() + * @since 3.2.2 + */ + public MergedContextConfiguration getParent() { + return this.parent; + } + + /** + * Get the parent {@link ApplicationContext} for the context defined by this + * {@code MergedContextConfiguration} from the context cache. + *

+ * If the parent context has not yet been loaded, it will be loaded, stored in the + * cache, and then returned. + * + * @return the parent {@code ApplicationContext} or {@code null} if there is no parent + * @see #getParent() + * @since 3.2.2 + */ + public ApplicationContext getParentApplicationContext() { + if (parent == null) { + return null; + } + + Assert.state(cacheAwareContextLoaderDelegate != null, + "Cannot retrieve a parent application context without access to the CacheAwareContextLoaderDelegate."); + return cacheAwareContextLoaderDelegate.loadContext(parent); + } + /** * Generate a unique hash code for all properties of this * {@code MergedContextConfiguration} excluding the @@ -220,6 +294,7 @@ public class MergedContextConfiguration implements Serializable { result = prime * result + Arrays.hashCode(classes); result = prime * result + contextInitializerClasses.hashCode(); result = prime * result + Arrays.hashCode(activeProfiles); + result = prime * result + (parent == null ? 0 : parent.hashCode()); result = prime * result + nullSafeToString(contextLoader).hashCode(); return result; } @@ -229,8 +304,9 @@ public class MergedContextConfiguration implements Serializable { * instance by comparing both object's {@linkplain #getLocations() locations}, * {@linkplain #getClasses() annotated classes}, * {@linkplain #getContextInitializerClasses() context initializer classes}, - * {@linkplain #getActiveProfiles() active profiles}, and the fully qualified - * names of their {@link #getContextLoader() ContextLoaders}. + * {@linkplain #getActiveProfiles() active profiles}, + * {@linkplain #getParent() parents}, and the fully qualified names of their + * {@link #getContextLoader() ContextLoaders}. */ @Override public boolean equals(Object obj) { @@ -247,15 +323,28 @@ public class MergedContextConfiguration implements Serializable { if (!Arrays.equals(this.locations, that.locations)) { return false; } + if (!Arrays.equals(this.classes, that.classes)) { return false; } + if (!this.contextInitializerClasses.equals(that.contextInitializerClasses)) { return false; } + if (!Arrays.equals(this.activeProfiles, that.activeProfiles)) { return false; } + + if (this.parent == null) { + if (that.parent != null) { + return false; + } + } + else if (!this.parent.equals(that.parent)) { + return false; + } + if (!nullSafeToString(this.contextLoader).equals(nullSafeToString(that.contextLoader))) { return false; } @@ -267,8 +356,9 @@ public class MergedContextConfiguration implements Serializable { * Provide a String representation of the {@linkplain #getTestClass() test class}, * {@linkplain #getLocations() locations}, {@linkplain #getClasses() annotated classes}, * {@linkplain #getContextInitializerClasses() context initializer classes}, - * {@linkplain #getActiveProfiles() active profiles}, and the name of the - * {@link #getContextLoader() ContextLoader}. + * {@linkplain #getActiveProfiles() active profiles}, the name of the + * {@link #getContextLoader() ContextLoader}, and the + * {@linkplain #getParent() parent configuration}. */ @Override public String toString() { @@ -279,6 +369,7 @@ public class MergedContextConfiguration implements Serializable { .append("contextInitializerClasses", ObjectUtils.nullSafeToString(contextInitializerClasses))// .append("activeProfiles", ObjectUtils.nullSafeToString(activeProfiles))// .append("contextLoader", nullSafeToString(contextLoader))// + .append("parent", parent)// .toString(); } diff --git a/spring-test/src/main/java/org/springframework/test/context/SmartContextLoader.java b/spring-test/src/main/java/org/springframework/test/context/SmartContextLoader.java index 8d069c85e91..d0495ca010b 100644 --- a/spring-test/src/main/java/org/springframework/test/context/SmartContextLoader.java +++ b/spring-test/src/main/java/org/springframework/test/context/SmartContextLoader.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2013 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,8 +29,7 @@ import org.springframework.context.ApplicationContext; * context that it loads (see {@link MergedContextConfiguration#getActiveProfiles()} * and {@link #loadContext(MergedContextConfiguration)}). * - *

See the Javadoc for - * {@link ContextConfiguration @ContextConfiguration} + *

See the Javadoc for {@link ContextConfiguration @ContextConfiguration} * for a definition of annotated class. * *

Clients of a {@code SmartContextLoader} should call @@ -48,8 +47,8 @@ import org.springframework.context.ApplicationContext; *

Even though {@code SmartContextLoader} extends {@code ContextLoader}, * clients should favor {@code SmartContextLoader}-specific methods over those * defined in {@code ContextLoader}, particularly because a - * {@code SmartContextLoader} may choose not to support methods defined in - * the {@code ContextLoader} SPI. + * {@code SmartContextLoader} may choose not to support methods defined in the + * {@code ContextLoader} SPI. * *

Concrete implementations must provide a {@code public} no-args constructor. * @@ -59,6 +58,9 @@ import org.springframework.context.ApplicationContext; *

  • {@link org.springframework.test.context.support.AnnotationConfigContextLoader AnnotationConfigContextLoader}
  • *
  • {@link org.springframework.test.context.support.GenericXmlContextLoader GenericXmlContextLoader}
  • *
  • {@link org.springframework.test.context.support.GenericPropertiesContextLoader GenericPropertiesContextLoader}
  • + *
  • {@link org.springframework.test.context.web.WebDelegatingSmartContextLoader WebDelegatingSmartContextLoader}
  • + *
  • {@link org.springframework.test.context.web.AnnotationConfigWebContextLoader AnnotationConfigWebContextLoader}
  • + *
  • {@link org.springframework.test.context.web.GenericXmlWebContextLoader GenericXmlWebContextLoader}
  • * * * @author Sam Brannen diff --git a/spring-test/src/main/java/org/springframework/test/context/TestContext.java b/spring-test/src/main/java/org/springframework/test/context/TestContext.java index 27100be94c1..d2719b50a0a 100644 --- a/spring-test/src/main/java/org/springframework/test/context/TestContext.java +++ b/spring-test/src/main/java/org/springframework/test/context/TestContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2013 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,9 +20,11 @@ import java.lang.reflect.Method; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; + import org.springframework.context.ApplicationContext; import org.springframework.core.AttributeAccessorSupport; import org.springframework.core.style.ToStringCreator; +import org.springframework.test.annotation.DirtiesContext.HierarchyMode; import org.springframework.util.Assert; /** @@ -41,6 +43,8 @@ public class TestContext extends AttributeAccessorSupport { private final ContextCache contextCache; + private final CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate; + private final MergedContextConfiguration mergedContextConfiguration; private final Class testClass; @@ -61,16 +65,17 @@ public class TestContext extends AttributeAccessorSupport { } /** - * Construct a new test context for the supplied {@link Class test class} - * and {@link ContextCache context cache} and parse the corresponding - * {@link ContextConfiguration @ContextConfiguration} annotation, if - * present. + * Construct a new test context for the supplied {@linkplain Class test class} + * and {@linkplain ContextCache context cache} and parse the corresponding + * {@link ContextConfiguration @ContextConfiguration} or + * {@link ContextHierarchy @ContextHierarchy} annotation, if present. *

    If the supplied class name for the default {@code ContextLoader} * is {@code null} or empty and no concrete {@code ContextLoader} - * class is explicitly supplied via the {@code @ContextConfiguration} - * annotation, a + * class is explicitly supplied via {@code @ContextConfiguration}, a * {@link org.springframework.test.context.support.DelegatingSmartContextLoader - * DelegatingSmartContextLoader} will be used instead. + * DelegatingSmartContextLoader} or + * {@link org.springframework.test.context.web.WebDelegatingSmartContextLoader + * WebDelegatingSmartContextLoader} will be used instead. * @param testClass the test class for which the test context should be * constructed (must not be {@code null}) * @param contextCache the context cache from which the constructed test @@ -83,54 +88,27 @@ public class TestContext extends AttributeAccessorSupport { Assert.notNull(testClass, "Test class must not be null"); Assert.notNull(contextCache, "ContextCache must not be null"); + this.testClass = testClass; + this.contextCache = contextCache; + this.cacheAwareContextLoaderDelegate = new CacheAwareContextLoaderDelegate(contextCache); + MergedContextConfiguration mergedContextConfiguration; - ContextConfiguration contextConfiguration = testClass.getAnnotation(ContextConfiguration.class); - if (contextConfiguration == null) { - if (logger.isInfoEnabled()) { - logger.info(String.format("@ContextConfiguration not found for class [%s]", testClass)); - } - mergedContextConfiguration = new MergedContextConfiguration(testClass, null, null, null, null); + if (testClass.isAnnotationPresent(ContextConfiguration.class) + || testClass.isAnnotationPresent(ContextHierarchy.class)) { + mergedContextConfiguration = ContextLoaderUtils.buildMergedContextConfiguration(testClass, + defaultContextLoaderClassName, cacheAwareContextLoaderDelegate); } else { - if (logger.isTraceEnabled()) { - logger.trace(String.format("Retrieved @ContextConfiguration [%s] for class [%s]", contextConfiguration, - testClass)); + if (logger.isInfoEnabled()) { + logger.info(String.format( + "Neither @ContextConfiguration nor @ContextHierarchy found for test class [%s]", + testClass.getName())); } - mergedContextConfiguration = ContextLoaderUtils.buildMergedContextConfiguration(testClass, - defaultContextLoaderClassName); + mergedContextConfiguration = new MergedContextConfiguration(testClass, null, null, null, null); } - this.contextCache = contextCache; this.mergedContextConfiguration = mergedContextConfiguration; - this.testClass = testClass; - } - - /** - * Load an {@code ApplicationContext} for this test context using the - * configured {@code ContextLoader} and merged context configuration. Supports - * both the {@link SmartContextLoader} and {@link ContextLoader} SPIs. - * @throws Exception if an error occurs while loading the application context - */ - private ApplicationContext loadApplicationContext() throws Exception { - ContextLoader contextLoader = mergedContextConfiguration.getContextLoader(); - Assert.notNull(contextLoader, "Cannot load an ApplicationContext with a NULL 'contextLoader'. " - + "Consider annotating your test class with @ContextConfiguration."); - - ApplicationContext applicationContext; - - if (contextLoader instanceof SmartContextLoader) { - SmartContextLoader smartContextLoader = (SmartContextLoader) contextLoader; - applicationContext = smartContextLoader.loadContext(mergedContextConfiguration); - } - else { - String[] locations = mergedContextConfiguration.getLocations(); - Assert.notNull(locations, "Cannot load an ApplicationContext with a NULL 'locations' array. " - + "Consider annotating your test class with @ContextConfiguration."); - applicationContext = contextLoader.loadContext(locations); - } - - return applicationContext; } /** @@ -141,31 +119,7 @@ public class TestContext extends AttributeAccessorSupport { * application context */ public ApplicationContext getApplicationContext() { - synchronized (contextCache) { - ApplicationContext context = contextCache.get(mergedContextConfiguration); - if (context == null) { - try { - context = loadApplicationContext(); - if (logger.isDebugEnabled()) { - logger.debug(String.format( - "Storing ApplicationContext for test class [%s] in cache under key [%s].", testClass, - mergedContextConfiguration)); - } - contextCache.put(mergedContextConfiguration, context); - } - catch (Exception ex) { - throw new IllegalStateException("Failed to load ApplicationContext", ex); - } - } - else { - if (logger.isDebugEnabled()) { - logger.debug(String.format( - "Retrieved ApplicationContext for test class [%s] from cache with key [%s].", testClass, - mergedContextConfiguration)); - } - } - return context; - } + return cacheAwareContextLoaderDelegate.loadContext(mergedContextConfiguration); } /** @@ -209,15 +163,27 @@ public class TestContext extends AttributeAccessorSupport { } /** - * Call this method to signal that the {@link ApplicationContext application - * context} associated with this test context is dirty and should - * be reloaded. Do this if a test has modified the context (for example, by - * replacing a bean definition). + * Call this method to signal that the {@linkplain ApplicationContext application + * context} associated with this test context is dirty and should be + * discarded. Do this if a test has modified the context — for example, + * by replacing a bean definition or modifying the state of a singleton bean. + * @deprecated As of Spring 3.2.2, use {@link #markApplicationContextDirty(HierarchyMode)} instead. */ + @Deprecated public void markApplicationContextDirty() { - synchronized (contextCache) { - contextCache.setDirty(mergedContextConfiguration); - } + markApplicationContextDirty((HierarchyMode) null); + } + + /** + * Call this method to signal that the {@linkplain ApplicationContext application + * context} associated with this test context is dirty and should be + * discarded. Do this if a test has modified the context — for example, + * by replacing a bean definition or modifying the state of a singleton bean. + * @param hierarchyMode the context cache clearing mode to be applied if the + * context is part of a hierarchy (may be {@code null}) + */ + public void markApplicationContextDirty(HierarchyMode hierarchyMode) { + contextCache.remove(mergedContextConfiguration, hierarchyMode); } /** diff --git a/spring-test/src/main/java/org/springframework/test/context/TestContextManager.java b/spring-test/src/main/java/org/springframework/test/context/TestContextManager.java index c67f0c72d75..aabdf33dfb7 100644 --- a/spring-test/src/main/java/org/springframework/test/context/TestContextManager.java +++ b/spring-test/src/main/java/org/springframework/test/context/TestContextManager.java @@ -104,15 +104,14 @@ public class TestContextManager { } /** - * Constructs a new {@code TestContextManager} for the specified {@link Class test class} - * and automatically {@link #registerTestExecutionListeners registers} the + * Constructs a new {@code TestContextManager} for the specified {@linkplain Class + * test class} and automatically {@link #registerTestExecutionListeners registers} the * {@link TestExecutionListener TestExecutionListeners} configured for the test class * via the {@link TestExecutionListeners @TestExecutionListeners} annotation. * @param testClass the test class to be managed - * @param defaultContextLoaderClassName the name of the default - * {@code ContextLoader} class to use (may be {@code null}) + * @param defaultContextLoaderClassName the name of the default {@code ContextLoader} + * class to use (may be {@code null}) * @see #registerTestExecutionListeners(TestExecutionListener...) - * @see #retrieveTestExecutionListeners(Class) */ public TestContextManager(Class testClass, String defaultContextLoaderClassName) { this.testContext = new TestContext(testClass, contextCache, defaultContextLoaderClassName); diff --git a/spring-test/src/main/java/org/springframework/test/context/TestExecutionListener.java b/spring-test/src/main/java/org/springframework/test/context/TestExecutionListener.java index 0f2ceb73a48..a052523fc90 100644 --- a/spring-test/src/main/java/org/springframework/test/context/TestExecutionListener.java +++ b/spring-test/src/main/java/org/springframework/test/context/TestExecutionListener.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2013 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,29 +17,24 @@ package org.springframework.test.context; /** + * {@code TestExecutionListener} defines a listener API for reacting to + * test execution events published by the {@link TestContextManager} with which + * the listener is registered. *

    - * {@code TestExecutionListener} defines a listener API for - * reacting to test execution events published by the {@link TestContextManager} - * with which the listener is registered. - *

    - *

    - * Concrete implementations must provide a {@code public} no-args - * constructor, so that listeners can be instantiated transparently by tools and - * configuration mechanisms. - *

    + * Concrete implementations must provide a {@code public} no-args constructor, + * so that listeners can be instantiated transparently by tools and configuration + * mechanisms. *

    * Spring provides the following out-of-the-box implementations: - *

    * * * @author Sam Brannen 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 50cfa74474e..388f66b5c8e 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-2012 the original author or authors. + * Copyright 2002-2013 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,9 +26,10 @@ import java.lang.annotation.Target; /** * {@code TestExecutionListeners} defines class-level metadata for * configuring which {@link TestExecutionListener TestExecutionListeners} should - * be registered with a {@link TestContextManager}. Typically, - * {@code @TestExecutionListeners} will be used in conjunction with - * {@link ContextConfiguration @ContextConfiguration}. + * be registered with a {@link TestContextManager}. + * + *

    Typically, {@code @TestExecutionListeners} will be used in conjunction with + * {@link ContextConfiguration @ContextConfiguration}. * * @author Sam Brannen * @since 2.5 @@ -43,11 +44,10 @@ import java.lang.annotation.Target; public @interface TestExecutionListeners { /** - *

    * The {@link TestExecutionListener TestExecutionListeners} to register with * a {@link TestContextManager}. - *

    * + * @see org.springframework.test.context.web.ServletTestExecutionListener * @see org.springframework.test.context.support.DependencyInjectionTestExecutionListener * @see org.springframework.test.context.support.DirtiesContextTestExecutionListener * @see org.springframework.test.context.transaction.TransactionalTestExecutionListener @@ -60,10 +60,8 @@ public @interface TestExecutionListeners { Class[] value() default {}; /** - *

    * Whether or not {@link #value() TestExecutionListeners} from superclasses * should be inherited. - *

    *

    * The default value is {@code true}, which means that an annotated * class will inherit the listeners defined by an annotated @@ -77,11 +75,12 @@ public @interface TestExecutionListeners { * {@code DependencyInjectionTestExecutionListener}, * {@code DirtiesContextTestExecutionListener}, and * {@code TransactionalTestExecutionListener}, in that order. - *

    * *
    -	 * @TestExecutionListeners({ DependencyInjectionTestExecutionListener.class,
    -	 *    DirtiesContextTestExecutionListener.class })
    +	 * @TestExecutionListeners({
    +	 *    DependencyInjectionTestExecutionListener.class,
    +	 *    DirtiesContextTestExecutionListener.class
    +	 * })
     	 * public abstract class AbstractBaseTest {
     	 * 	// ...
     	 * }
    @@ -89,14 +88,12 @@ public @interface TestExecutionListeners {
     	 * @TestExecutionListeners(TransactionalTestExecutionListener.class)
     	 * public class TransactionalTest extends AbstractBaseTest {
     	 * 	// ...
    -	 * }
    -	 * 
    + * } * *

    - * If {@code inheritListeners} is set to {@code false}, the - * listeners for the annotated class will shadow and effectively - * replace any listeners defined by a superclass. - *

    + * If {@code inheritListeners} is set to {@code false}, the listeners for the + * annotated class will shadow and effectively replace any listeners + * defined by a superclass. */ boolean inheritListeners() default true; diff --git a/spring-test/src/main/java/org/springframework/test/context/support/AbstractGenericContextLoader.java b/spring-test/src/main/java/org/springframework/test/context/support/AbstractGenericContextLoader.java index c4b9b4dca7e..bd84068ac78 100644 --- a/spring-test/src/main/java/org/springframework/test/context/support/AbstractGenericContextLoader.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/AbstractGenericContextLoader.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2013 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,10 +16,13 @@ package org.springframework.test.context.support; + import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; + import org.springframework.beans.factory.support.BeanDefinitionReader; import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.context.ApplicationContext; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.AnnotationConfigUtils; import org.springframework.context.support.GenericApplicationContext; @@ -66,6 +69,12 @@ public abstract class AbstractGenericContextLoader extends AbstractContextLoader * *