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 c1a98a81e850..2980ea3fdf94 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 @@ -731,7 +731,7 @@ private static A findAnnotation(Class clazz, Class * @see Class#isAnnotationPresent(Class) * @see Class#getDeclaredAnnotations() * @see #findAnnotationDeclaringClassForTypes(List, Class) - * @see #isAnnotationDeclaredLocally(Class, Class) + * @see #isAnnotationDeclaredLocally(Class, AnnotatedElement) */ public static Class findAnnotationDeclaringClass(Class annotationType, Class clazz) { Assert.notNull(annotationType, "Annotation type must not be null"); @@ -766,7 +766,7 @@ public static Class findAnnotationDeclaringClass(Class * @see Class#isAnnotationPresent(Class) * @see Class#getDeclaredAnnotations() * @see #findAnnotationDeclaringClass(Class, Class) - * @see #isAnnotationDeclaredLocally(Class, Class) + * @see #isAnnotationDeclaredLocally(Class, AnnotatedElement) */ public static Class findAnnotationDeclaringClassForTypes(List> annotationTypes, Class clazz) { Assert.notEmpty(annotationTypes, "List of annotation types must not be empty"); @@ -784,33 +784,34 @@ public static Class findAnnotationDeclaringClassForTypes(Listdirectly present) on the supplied - * {@code clazz}. - *

The supplied {@link Class} may represent any type. + * {@code annotatedElement}. + *

The supplied {@link AnnotatedElement} may represents any annotated element. *

Meta-annotations will not be searched. *

Note: This method does not determine if the annotation * is {@linkplain java.lang.annotation.Inherited inherited}. For greater * clarity regarding inherited annotations, consider using * {@link #isAnnotationInherited(Class, Class)} instead. * @param annotationType the annotation type to look for - * @param clazz the class to check for the annotation on + * @param annotatedElement the element to check for the annotation on * @return {@code true} if an annotation of the specified {@code annotationType} * is directly present * @see java.lang.Class#getDeclaredAnnotations() * @see java.lang.Class#getDeclaredAnnotation(Class) * @see #isAnnotationInherited(Class, Class) */ - public static boolean isAnnotationDeclaredLocally(Class annotationType, Class clazz) { + public static boolean isAnnotationDeclaredLocally(Class annotationType, + AnnotatedElement annotatedElement) { Assert.notNull(annotationType, "Annotation type must not be null"); - Assert.notNull(clazz, "Class must not be null"); + Assert.notNull(annotatedElement, "Annotated element must not be null"); try { - for (Annotation ann : clazz.getDeclaredAnnotations()) { + for (Annotation ann : annotatedElement.getDeclaredAnnotations()) { if (ann.annotationType() == annotationType) { return true; } } } catch (Throwable ex) { - handleIntrospectionFailure(clazz, ex); + handleIntrospectionFailure(annotatedElement, ex); } return false; } @@ -832,7 +833,7 @@ public static boolean isAnnotationDeclaredLocally(Class an * @return {@code true} if an annotation of the specified {@code annotationType} * is present and inherited * @see Class#isAnnotationPresent(Class) - * @see #isAnnotationDeclaredLocally(Class, Class) + * @see #isAnnotationDeclaredLocally(Class, AnnotatedElement) */ public static boolean isAnnotationInherited(Class annotationType, Class clazz) { Assert.notNull(annotationType, "Annotation type must not be null"); 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 81e795295d35..c511df26f2ae 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 @@ -407,6 +407,14 @@ public void isAnnotationDeclaredLocallyForAllScenarios() throws Exception { assertFalse(isAnnotationDeclaredLocally(Order.class, SubNonInheritedAnnotationInterface.class)); assertTrue(isAnnotationDeclaredLocally(Order.class, NonInheritedAnnotationClass.class)); assertFalse(isAnnotationDeclaredLocally(Order.class, SubNonInheritedAnnotationClass.class)); + + // inherited method-level annotation; note: @Transactional is inherited + assertTrue(isAnnotationDeclaredLocally(Transactional.class, InheritedAnnotationClass.class.getMethod("something"))); + assertFalse(isAnnotationDeclaredLocally(Transactional.class, SubInheritedAnnotationClass.class.getMethod("something"))); + + // non-inherited method-level annotation; note: @Order is not inherited + assertTrue(isAnnotationDeclaredLocally(Order.class, NonInheritedAnnotationInterface.class.getMethod("something"))); + assertFalse(isAnnotationDeclaredLocally(Order.class, SubNonInheritedAnnotationInterface.class.getMethod("something"))); } @Test @@ -1719,9 +1727,13 @@ public interface SubSubInheritedAnnotationInterface extends SubInheritedAnnotati @Order public interface NonInheritedAnnotationInterface { + @Order + void something(); } public interface SubNonInheritedAnnotationInterface extends NonInheritedAnnotationInterface { + @Override + void something(); } public interface SubSubNonInheritedAnnotationInterface extends SubNonInheritedAnnotationInterface { @@ -1735,9 +1747,17 @@ public interface NonAnnotatedInterface { @Transactional public static class InheritedAnnotationClass { + @Transactional + public void something() { + // for test purposes + } } public static class SubInheritedAnnotationClass extends InheritedAnnotationClass { + @Override + public void something() { + // for test purposes + } } @Order diff --git a/spring-test/src/main/java/org/springframework/test/context/BootstrapUtils.java b/spring-test/src/main/java/org/springframework/test/context/BootstrapUtils.java index 5b81650b7dcc..4aabf396ba95 100644 --- a/spring-test/src/main/java/org/springframework/test/context/BootstrapUtils.java +++ b/spring-test/src/main/java/org/springframework/test/context/BootstrapUtils.java @@ -17,6 +17,7 @@ package org.springframework.test.context; import java.lang.reflect.Constructor; +import java.lang.reflect.Method; import java.util.Set; import org.apache.commons.logging.Log; @@ -50,6 +51,9 @@ abstract class BootstrapUtils { private static final String DEFAULT_TEST_CONTEXT_BOOTSTRAPPER_CLASS_NAME = "org.springframework.test.context.support.DefaultTestContextBootstrapper"; + private static final String DEFAULT_TEST_METHOD_CONTEXT_BOOTSTRAPPER_CLASS_NAME = + "org.springframework.test.context.support.DefaultTestMethodContextBootstrapper"; + private static final String DEFAULT_WEB_TEST_CONTEXT_BOOTSTRAPPER_CLASS_NAME = "org.springframework.test.context.web.WebTestContextBootstrapper"; @@ -147,6 +151,42 @@ static TestContextBootstrapper resolveTestContextBootstrapper(BootstrapContext b } } + /** + * Resolve the {@link TestContextBootstrapper} type for the supplied test method. + *

Will use + * {@link org.springframework.test.context.support.DefaultTestMethodContextBootstrapper + * DefaultTestMethodContextBootstrapper}. + * @param testMethod the method for which to create context bootstrapper + * @return a fully configured {@code TestContextBootstrapper} for particular method + */ + @SuppressWarnings("unchecked") + static TestContextBootstrapper resolveTestContextBootstrapper(Method testMethod) { + final BootstrapContext bootstrapContext = createBootstrapContext(testMethod.getDeclaringClass()); + Class testClass = bootstrapContext.getTestClass(); + + Class clazz = null; + try { + clazz = ClassUtils.forName(DEFAULT_TEST_METHOD_CONTEXT_BOOTSTRAPPER_CLASS_NAME, + BootstrapUtils.class.getClassLoader()); + if (logger.isDebugEnabled()) { + logger.debug(String.format("Instantiating TestContextBootstrapper for test class [%s] from class [%s]", + testClass.getName(), clazz.getName())); + } + TestContextBootstrapper testContextBootstrapper = BeanUtils.instantiateClass( + (Constructor) clazz.getConstructor(Method.class), testMethod); + testContextBootstrapper.setBootstrapContext(bootstrapContext); + return testContextBootstrapper; + } + catch (IllegalStateException ex) { + throw ex; + } + catch (Throwable ex) { + throw new IllegalStateException("Could not load TestContextBootstrapper [" + clazz + + "]. Specify @BootstrapWith's 'value' attribute or make the default bootstrapper class available.", + ex); + } + } + private static Class resolveExplicitTestContextBootstrapper(Class testClass) { Set annotations = AnnotatedElementUtils.findAllMergedAnnotations(testClass, BootstrapWith.class); if (annotations.size() < 1) { 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 669adbd65c74..7b407f7b4eac 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 @@ -83,7 +83,7 @@ * @see MergedContextConfiguration * @see org.springframework.context.ApplicationContext */ -@Target(ElementType.TYPE) +@Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited 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 f32731dfa5b6..9ffc829598f1 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 @@ -16,6 +16,7 @@ package org.springframework.test.context; +import java.lang.reflect.Method; import java.util.Arrays; import org.apache.commons.logging.Log; @@ -51,6 +52,8 @@ public class ContextConfigurationAttributes { private final Class declaringClass; + private final Method declaringMethod; + private Class[] classes; private String[] locations; @@ -149,6 +152,14 @@ public ContextConfigurationAttributes( Class declaringClass, String[] locations, Class[] classes, boolean inheritLocations, Class>[] initializers, boolean inheritInitializers, String name, Class contextLoaderClass) { + this(declaringClass, null, locations, classes, inheritLocations, initializers, inheritInitializers, + name, contextLoaderClass); + } + + public ContextConfigurationAttributes( + Class declaringClass, Method declaringMethod, 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"); @@ -156,13 +167,14 @@ public ContextConfigurationAttributes( if (!ObjectUtils.isEmpty(locations) && !ObjectUtils.isEmpty(classes) && logger.isDebugEnabled()) { logger.debug(String.format( "Test class [%s] has been configured with @ContextConfiguration's 'locations' (or 'value') %s " + - "and 'classes' %s attributes. Most SmartContextLoader implementations support " + - "only one declaration of resources per @ContextConfiguration annotation.", + "and 'classes' %s attributes. Most SmartContextLoader implementations support " + + "only one declaration of resources per @ContextConfiguration annotation.", declaringClass.getName(), ObjectUtils.nullSafeToString(locations), ObjectUtils.nullSafeToString(classes))); } this.declaringClass = declaringClass; + this.declaringMethod = declaringMethod; this.locations = locations; this.classes = classes; this.inheritLocations = inheritLocations; @@ -183,6 +195,16 @@ public Class getDeclaringClass() { return this.declaringClass; } + /** + * Get the {@linkplain Method method} that declared the + * {@link ContextConfiguration @ContextConfiguration} annotation, either explicitly + * or implicitly. + * @return the declaring method (may be {@code null}) + */ + public Method getDeclaringMethod() { + return this.declaringMethod; + } + /** * Set the processed annotated classes, effectively overriding the * original value declared via {@link ContextConfiguration @ContextConfiguration}. 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 index b22dc18ad599..7c168b4dc02f 100644 --- a/spring-test/src/main/java/org/springframework/test/context/ContextHierarchy.java +++ b/spring-test/src/main/java/org/springframework/test/context/ContextHierarchy.java @@ -139,7 +139,7 @@ * @see ContextConfiguration * @see org.springframework.context.ApplicationContext */ -@Target(ElementType.TYPE) +@Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited 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 1d625d4a935a..6857066d0cd1 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 @@ -25,6 +25,8 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; +import org.springframework.test.util.MetaAnnotationUtils; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ReflectionUtils; @@ -282,10 +284,11 @@ public void prepareTestInstance(Object testInstance) throws Exception { public void beforeTestMethod(Object testInstance, Method testMethod) throws Exception { String callbackName = "beforeTestMethod"; prepareForBeforeCallback(callbackName, testInstance, testMethod); + TestContext preparedContext = prepareTestContextBeforeMethod(getTestContext()); for (TestExecutionListener testExecutionListener : getTestExecutionListeners()) { try { - testExecutionListener.beforeTestMethod(getTestContext()); + testExecutionListener.beforeTestMethod(preparedContext); } catch (Throwable ex) { handleBeforeException(ex, callbackName, testExecutionListener, testInstance, testMethod); @@ -294,6 +297,46 @@ public void beforeTestMethod(Object testInstance, Method testMethod) throws Exce } /** + * Prepares {@linkplain TestContext test context} in the sense + * that if current test method annotated with context configuration + * annotation e.g. {@link ContextConfiguration}, {@link ContextHierarchy} + * then emits throw-away context, constructed specifically for current + * test method, otherwise returns original test context without modifications. + * + * @param originalTestContext test class test context + * @return throw-away context if current method has context annotation or + * originalTestContext otherwise. + * + * @since 5.0 + */ + private TestContext prepareTestContextBeforeMethod(TestContext originalTestContext) { + Method testMethod = originalTestContext.getTestMethod(); + if (testMethod != null && MetaAnnotationUtils.findAnnotationDescriptorForTypes(testMethod, + ContextConfiguration.class, ContextHierarchy.class) != null) { + return createThrowAwayContext(testMethod, originalTestContext); + } + return originalTestContext; + } + + /** + * Creates throw-away context for supplied {@linkplain Method testMethod} + * and copies the state of original context in the created one. + * Sets an attribute {@link DependencyInjectionTestExecutionListener#REINJECT_DEPENDENCIES_ATTRIBUTE} + * of the created context to true to make reinjection. + * @param testMethod test method annotated with context configuration annotation + * @param originalContext original test context to copy state into created one + * @return throw-away context for specified test method + * @since 5.0 + */ + private TestContext createThrowAwayContext(Method testMethod, TestContext originalContext) { + TestContext throwAwayContext = BootstrapUtils.resolveTestContextBootstrapper(testMethod).buildTestContext(); + throwAwayContext.updateState(originalContext.getTestInstance(), testMethod, null); + throwAwayContext.setAttribute(DependencyInjectionTestExecutionListener.REINJECT_DEPENDENCIES_ATTRIBUTE, + Boolean.TRUE); + return throwAwayContext; + } + + /** * Hook for pre-processing a test immediately before execution of * the {@linkplain java.lang.reflect.Method test method} in the supplied * {@linkplain TestContext test context} — for example, for timing diff --git a/spring-test/src/main/java/org/springframework/test/context/support/AbstractTestContextBootstrapper.java b/spring-test/src/main/java/org/springframework/test/context/support/AbstractTestContextBootstrapper.java index 376cdaa332c8..722f6c1f99bf 100644 --- a/spring-test/src/main/java/org/springframework/test/context/support/AbstractTestContextBootstrapper.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/AbstractTestContextBootstrapper.java @@ -25,6 +25,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.function.Supplier; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -278,36 +279,45 @@ public final MergedContextConfiguration buildMergedContextConfiguration() { } if (AnnotationUtils.findAnnotation(testClass, ContextHierarchy.class) != null) { - Map> hierarchyMap = - ContextLoaderUtils.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, parentConfig, cacheAwareContextLoaderDelegate, true); - parentConfig = mergedConfig; - } - - // Return the last level in the context hierarchy - return mergedConfig; - } - else { + return buildMergedConfigFromHierarchyMap(() -> ContextLoaderUtils.buildContextHierarchyMap(testClass)); + } else { return buildMergedContextConfiguration(testClass, ContextLoaderUtils.resolveContextConfigurationAttributes(testClass), null, cacheAwareContextLoaderDelegate, true); } } + /** + * Convenience method to build {@linkplain MergedContextConfiguration merged configuration} + * for the supplied context hierarchy map. + * @param hierarchyMapSupplier supplies process with context hierarchy map + * @return the merged context configuration + */ + protected MergedContextConfiguration buildMergedConfigFromHierarchyMap( + Supplier>> hierarchyMapSupplier) { + Map> hierarchyMap = hierarchyMapSupplier.get(); + 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, parentConfig, + getCacheAwareContextLoaderDelegate(), true); + parentConfig = mergedConfig; + } + + // Return the last level in the context hierarchy + return mergedConfig; + } + private MergedContextConfiguration buildDefaultMergedContextConfiguration(Class testClass, CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate) { @@ -349,7 +359,7 @@ private MergedContextConfiguration buildDefaultMergedContextConfiguration(Class< * @see ApplicationContextInitializerUtils#resolveInitializerClasses * @see MergedContextConfiguration */ - private MergedContextConfiguration buildMergedContextConfiguration(Class testClass, + protected MergedContextConfiguration buildMergedContextConfiguration(Class testClass, List configAttributesList, MergedContextConfiguration parentConfig, CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate, boolean requireLocationsClassesOrInitializers) { diff --git a/spring-test/src/main/java/org/springframework/test/context/support/ContextLoaderUtils.java b/spring-test/src/main/java/org/springframework/test/context/support/ContextLoaderUtils.java index 6754a5bbe86b..7ef4a18de988 100644 --- a/spring-test/src/main/java/org/springframework/test/context/support/ContextLoaderUtils.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/ContextLoaderUtils.java @@ -16,26 +16,36 @@ package org.springframework.test.context.support; +import java.lang.reflect.Method; import java.util.ArrayList; +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 java.util.function.Supplier; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.springframework.core.annotation.AnnotationUtils; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.ContextConfigurationAttributes; import org.springframework.test.context.ContextHierarchy; import org.springframework.test.context.SmartContextLoader; +import org.springframework.test.util.MetaAnnotationUtils.AnnotationOnMethodDescriptor; +import org.springframework.test.util.MetaAnnotationUtils.UntypedAnnotationOnMethodDescriptor; import org.springframework.util.Assert; +import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; -import static org.springframework.core.annotation.AnnotationUtils.*; -import static org.springframework.test.util.MetaAnnotationUtils.*; +import static org.springframework.core.annotation.AnnotationUtils.getAnnotation; +import static org.springframework.core.annotation.AnnotationUtils.isAnnotationDeclaredLocally; +import static org.springframework.core.annotation.AnnotationUtils.synthesizeAnnotation; +import static org.springframework.test.util.MetaAnnotationUtils.AnnotationDescriptor; +import static org.springframework.test.util.MetaAnnotationUtils.UntypedAnnotationDescriptor; +import static org.springframework.test.util.MetaAnnotationUtils.findAnnotationDescriptor; +import static org.springframework.test.util.MetaAnnotationUtils.findAnnotationDescriptorForTypes; /** * Utility methods for resolving {@link ContextConfigurationAttributes} from the @@ -90,6 +100,7 @@ abstract class ContextLoaderUtils { * @since 3.2.2 * @see #buildContextHierarchyMap(Class) * @see #resolveContextConfigurationAttributes(Class) + * @see #resolveContextHierarchyAttributes(Method) */ @SuppressWarnings("unchecked") static List> resolveContextHierarchyAttributes(Class testClass) { @@ -123,7 +134,7 @@ static List> resolveContextHierarchyAttribu List configAttributesList = new ArrayList<>(); if (contextConfigDeclaredLocally) { - ContextConfiguration contextConfiguration = AnnotationUtils.synthesizeAnnotation( + ContextConfiguration contextConfiguration = synthesizeAnnotation( desc.getAnnotationAttributes(), ContextConfiguration.class, desc.getRootDeclaringClass()); convertContextConfigToConfigAttributesAndAddToList( contextConfiguration, rootDeclaringClass, configAttributesList); @@ -151,6 +162,114 @@ else if (contextHierarchyDeclaredLocally) { return hierarchyAttributes; } + /** + * Resolve the list of lists of {@linkplain ContextConfigurationAttributes context + * configuration attributes} for the supplied {@linkplain Method testMethod} and its + * supermethods, 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 method in the methods hierarchy. Each nested list + * contains the context configuration attributes declared either via a single + * instance of {@code @ContextConfiguration} on the particular method or via + * multiple instances of {@code @ContextConfiguration} declared within a + * single {@code @ContextHierarchy} instance on the particular method. + * 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 testMethod. + *

Note that this method does not throw an exception when method in + * hierarchy annotated with neither {@link ContextHierarchy @ContextHierarchy} nor + * {@link ContextConfiguration @ContextConfiguration} unlikely + * {@link #resolveContextHierarchyAttributes(Class)}. + * @param testMethod the method 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 method is {@code null}; or if neither {@code + * @ContextConfiguration} nor {@code @ContextHierarchy} is present on the supplied class + * @throws IllegalStateException if a test method or composed annotation in the method hierarchy declares both + * {@code @ContextConfiguration} and {@code @ContextHierarchy} as top-level annotations. + * @see #buildContextHierarchyMap(Class) + * @see #resolveContextConfigurationAttributes(Class) + * @see #resolveContextHierarchyAttributes(Class) + * @since 5.0 + */ + static List> resolveContextHierarchyAttributes(Method testMethod) { + Assert.notNull(testMethod, "Method must not be null"); + + List> hierarchyAttributes = new ArrayList<>(); + Method superMethod = testMethod; + + while (superMethod != null) { + hierarchyAttributes.add(buildContextAttributes(superMethod)); + for (Class ifc : superMethod.getDeclaringClass().getInterfaces()) { + hierarchyAttributes.addAll( + buildContextHierarchyForInterface(superMethod.getName(), superMethod.getParameterTypes(), ifc, + new ArrayList<>(), new HashSet<>())); + } + superMethod = ReflectionUtils.findMethod(superMethod.getDeclaringClass().getSuperclass(), + superMethod.getName(), superMethod.getParameterTypes()); + } + + Collections.reverse(hierarchyAttributes); + return hierarchyAttributes; + } + + private static List> buildContextHierarchyForInterface(String name, + Class[] parameterTypes, Class interfaceType, List> attributes, + Set visited) { + Method found = ReflectionUtils.findMethod(interfaceType, name, parameterTypes); + if (found != null && visited.add(found)) { + List attr = buildContextAttributes(found); + if (!attr.isEmpty()) { + attributes.add(attr); + } + } + for (Class ifc : interfaceType.getInterfaces()) { + buildContextHierarchyForInterface(name, parameterTypes, ifc, attributes, visited); + } + return attributes; + } + + private static List buildContextAttributes(Method method) { + Class contextConfigType = ContextConfiguration.class; + Class contextHierarchyType = ContextHierarchy.class; + + boolean contextConfigDeclaredLocally = getAnnotation(method, contextConfigType) != null; + boolean contextHierarchyDeclaredLocally = getAnnotation(method, contextHierarchyType) != null; + + if (contextConfigDeclaredLocally && contextHierarchyDeclaredLocally) { + String msg = String.format("Method [%s] has been configured with both @ContextConfiguration " + + "and @ContextHierarchy. Only one of these annotations may be declared on a test class " + + "or composed annotation.", method.getName()); + logger.error(msg); + throw new IllegalStateException(msg); + } + + UntypedAnnotationOnMethodDescriptor desc = findAnnotationDescriptorForTypes(method, contextConfigType, + contextHierarchyType); + + List configAttributesList = new ArrayList<>(); + if (contextConfigDeclaredLocally) { + ContextConfiguration contextConfiguration = synthesizeAnnotation(desc.getAnnotationAttributes(), + ContextConfiguration.class, desc.getRootDeclaringClass()); + convertContextConfigToConfigAttributesAndAddToList(contextConfiguration, method.getDeclaringClass(), + method, configAttributesList); + + } else if (contextHierarchyDeclaredLocally) { + ContextHierarchy contextHierarchy = getAnnotation(method, contextHierarchyType); + for (ContextConfiguration contextConfiguration : contextHierarchy.value()) { + convertContextConfigToConfigAttributesAndAddToList(contextConfiguration, + method.getDeclaringClass(), method, configAttributesList); + } + } + + return configAttributesList; + } + /** * Build a context hierarchy map for the supplied {@linkplain Class * test class} and its superclasses, taking into account context hierarchies @@ -174,12 +293,73 @@ else if (contextHierarchyDeclaredLocally) { * unique context configuration within the overall hierarchy. * @since 3.2.2 * @see #resolveContextHierarchyAttributes(Class) + * @see #buildContextHierarchyMap(Method) + */ + static Map> buildContextHierarchyMap(final Class testClass) { + Map> map = buildContextHierarchyMap( + () -> resolveContextHierarchyAttributes(testClass)); + + Set> set = new HashSet<>(map.values()); + if (set.size() != map.size()) { + String msg = String.format("The @ContextConfiguration elements configured via @ContextHierarchy in " + + "test class [%s] and its superclasses must define unique contexts per hierarchy level.", + testClass.getName()); + logger.error(msg); + throw new IllegalStateException(msg); + } + + return map; + } + + /** + * Build a context hierarchy map for the supplied {@linkplain Method + * test testMethod} and its supermethods, 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 methods + * 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 testMethod the method 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} + * + * @throws IllegalArgumentException if the lists of context configuration + * attributes for each level in the {@code @ContextHierarchy} do not define + * unique context configuration within the overall hierarchy. + * @see #resolveContextHierarchyAttributes(Class) + * @see #buildContextHierarchyMap(Class) + * @since 5.0 */ - static Map> buildContextHierarchyMap(Class testClass) { + static Map> buildContextHierarchyMap(final Method testMethod) { + Map> map + = buildContextHierarchyMap(() -> resolveContextHierarchyAttributes(testMethod)); + + Set> set = new HashSet<>(map.values()); + if (set.size() != map.size()) { + String msg = String.format("The @ContextConfiguration elements configured via @ContextHierarchy on " + + "test method [%s] and its supermethods must define unique contexts per hierarchy level.", + testMethod.getName()); + logger.error(msg); + throw new IllegalStateException(msg); + } + + return map; + } + + private static Map> buildContextHierarchyMap + (Supplier>> contextHierarchyResolver) { final Map> map = new LinkedHashMap<>(); int hierarchyLevel = 1; - for (List configAttributesList : resolveContextHierarchyAttributes(testClass)) { + for (List configAttributesList : contextHierarchyResolver.get()) { for (ContextConfigurationAttributes configAttributes : configAttributesList) { String name = configAttributes.getName(); @@ -198,16 +378,6 @@ static Map> buildContextHierarchyMa } } - // Check for uniqueness - Set> set = new HashSet<>(map.values()); - if (set.size() != map.size()) { - String msg = String.format("The @ContextConfiguration elements configured via @ContextHierarchy in " + - "test class [%s] and its superclasses must define unique contexts per hierarchy level.", - testClass.getName()); - logger.error(msg); - throw new IllegalStateException(msg); - } - return map; } @@ -242,7 +412,59 @@ static List resolveContextConfigurationAttribute while (descriptor != null) { convertContextConfigToConfigAttributesAndAddToList(descriptor.synthesizeAnnotation(), descriptor.getRootDeclaringClass(), attributesList); - descriptor = findAnnotationDescriptor(descriptor.getRootDeclaringClass().getSuperclass(), annotationType); + descriptor = findAnnotationDescriptor( + descriptor.getRootDeclaringClass().getSuperclass(), annotationType); + } + + return attributesList; + } + + /** + * Resolve the list of {@linkplain ContextConfigurationAttributes context + * configuration attributes} for the supplied {@linkplain Method test testMethod} and its + * supermethods. + *

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 testMethod. + * + * @param testMethod 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 method is {@code null} or if + * {@code @ContextConfiguration} is not present on the supplied method + * @see #resolveContextConfigurationAttributes(Class) + * @since 5.0 + */ + static List resolveContextConfigurationAttributes(Method testMethod) { + Assert.notNull(testMethod, "Method must not be null"); + + List attributesList = new ArrayList<>(); + Class annotationType = ContextConfiguration.class; + + AnnotationOnMethodDescriptor descriptor = findAnnotationDescriptor(testMethod, + annotationType); + Assert.notNull(descriptor, () -> + String.format("Could not find an 'annotation declaring testMethod' for annotation type [%s] and testMethod [%s]", + annotationType.getName(), testMethod.getName())); + + Method superMethod = ReflectionUtils.findMethod(testMethod.getDeclaringClass().getSuperclass(), + testMethod.getName(), testMethod.getParameterTypes()); + while (descriptor != null) { + convertContextConfigToConfigAttributesAndAddToList(descriptor.synthesizeAnnotation(), + descriptor.getRootDeclaringClass(), descriptor.getDeclaringMethod(), attributesList); + if (superMethod == null) { + break; + } + + descriptor = findAnnotationDescriptor(superMethod, annotationType); + superMethod = ReflectionUtils.findMethod(superMethod.getDeclaringClass().getSuperclass(), + superMethod.getName(), superMethod.getParameterTypes()); } return attributesList; @@ -268,4 +490,27 @@ private static void convertContextConfigToConfigAttributesAndAddToList(ContextCo attributesList.add(attributes); } + /** + * Convenience method for creating a {@link ContextConfigurationAttributes} + * instance from the supplied {@link ContextConfiguration} annotation, + * declaring class and declaring method and then adding the attributes to the supplied list. + */ + private static void convertContextConfigToConfigAttributesAndAddToList( + ContextConfiguration contextConfiguration, Class declaringClass, Method declaringMethod, + List attributesList) { + if (logger.isTraceEnabled()) { + logger.trace(String.format("Retrieved @ContextConfiguration [%s] for declaring method [%s].", + contextConfiguration, + declaringMethod.getName())); + } + ContextConfigurationAttributes attributes = new ContextConfigurationAttributes(declaringClass, + declaringMethod, contextConfiguration.locations(), contextConfiguration.classes(), + contextConfiguration.inheritLocations(), contextConfiguration.initializers(), + contextConfiguration.inheritInitializers(), contextConfiguration.name(), contextConfiguration.loader()); + if (logger.isTraceEnabled()) { + logger.trace("Resolved context configuration attributes: " + attributes); + } + attributesList.add(attributes); + } + } diff --git a/spring-test/src/main/java/org/springframework/test/context/support/DefaultTestMethodContextBootstrapper.java b/spring-test/src/main/java/org/springframework/test/context/support/DefaultTestMethodContextBootstrapper.java new file mode 100644 index 000000000000..f64928d30fc8 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/support/DefaultTestMethodContextBootstrapper.java @@ -0,0 +1,93 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.support; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.test.context.CacheAwareContextLoaderDelegate; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextHierarchy; +import org.springframework.test.context.ContextLoader; +import org.springframework.test.context.MergedContextConfiguration; +import org.springframework.test.context.TestContext; +import org.springframework.test.context.TestContextBootstrapper; +import org.springframework.test.util.MetaAnnotationUtils; + +import java.lang.reflect.Method; + +import static java.lang.String.format; +import static org.springframework.test.context.support.ContextLoaderUtils.buildContextHierarchyMap; +import static org.springframework.test.context.support.ContextLoaderUtils.resolveContextConfigurationAttributes; + +/** + * Default implementation of the {@link TestContextBootstrapper} SPI for supplied test method. + *

Uses {@link DelegatingSmartContextLoader} as the default {@link ContextLoader}. + * + * @author Sergei Ustimenko + * @since 5.0 + */ +public class DefaultTestMethodContextBootstrapper extends DefaultTestContextBootstrapper { + private final Log logger = LogFactory.getLog(getClass()); + + private final Method testMethod; + + public DefaultTestMethodContextBootstrapper(Method testMethod) { + this.testMethod = testMethod; + } + + /** + * Build a new {@link DefaultTestContext} using the {@linkplain Method test method}. + * {@link #getCacheAwareContextLoaderDelegate()}. + */ + @Override + public TestContext buildTestContext() { + return new DefaultTestContext(getBootstrapContext().getTestClass(), + buildMergedContextConfiguration(testMethod), + getCacheAwareContextLoaderDelegate()); + } + + /** + * Build the {@linkplain MergedContextConfiguration merged context configuration} + * for the supplied test testMethod. + * + * @param testMethod supplied testMethod for which to create merged context configuration + * + * @return the merged context configuration, never {@code null} + * + * @see #buildMergedContextConfiguration() + * @see #buildTestContext() + */ + protected MergedContextConfiguration buildMergedContextConfiguration(Method testMethod) { + CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate = getCacheAwareContextLoaderDelegate(); + + if (MetaAnnotationUtils.findAnnotationDescriptorForTypes( + testMethod, ContextConfiguration.class, ContextHierarchy.class) == null) { + String msg = format("Neither @ContextConfiguration nor @ContextHierarchy found for test method [%s]", + testMethod.getName()); + logger.error(msg); + throw new IllegalStateException(msg); + } + + if (AnnotationUtils.findAnnotation(testMethod, ContextHierarchy.class) != null) { + return buildMergedConfigFromHierarchyMap(() -> buildContextHierarchyMap(testMethod)); + } else { + return buildMergedContextConfiguration(testMethod.getDeclaringClass(), + resolveContextConfigurationAttributes(testMethod), null, cacheAwareContextLoaderDelegate, true); + } + } +} diff --git a/spring-test/src/main/java/org/springframework/test/util/MetaAnnotationUtils.java b/spring-test/src/main/java/org/springframework/test/util/MetaAnnotationUtils.java index df203284ef06..77bc41216cb0 100644 --- a/spring-test/src/main/java/org/springframework/test/util/MetaAnnotationUtils.java +++ b/spring-test/src/main/java/org/springframework/test/util/MetaAnnotationUtils.java @@ -17,6 +17,7 @@ package org.springframework.test.util; import java.lang.annotation.Annotation; +import java.lang.reflect.Method; import java.util.HashSet; import java.util.Set; @@ -26,6 +27,7 @@ import org.springframework.core.style.ToStringCreator; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; +import org.springframework.util.ReflectionUtils; /** * {@code MetaAnnotationUtils} is a collection of utility methods that complements @@ -134,6 +136,199 @@ private static AnnotationDescriptor findAnnotationDesc return findAnnotationDescriptor(clazz.getSuperclass(), visited, annotationType); } + /** + * Find the {@link AnnotationOnMethodDescriptor} for the supplied {@code annotationType} + * on the supplied {@link Method}, traversing its annotations and + * supermethods if no annotation can be found on the given class itself. + *

This method explicitly handles method-level annotations which are not + * declared as {@linkplain java.lang.annotation.Inherited inherited} as + * well as meta-annotations. + *

The algorithm operates as follows: + *

    + *
  1. Search for the annotation on the given method and return a corresponding + * {@code AnnotationOnMethodDescriptor} if found. + *
  2. Recursively search through all annotations that the given method declares. + *
  3. Recursively search through all interfaces's methods implemented by the given method's class. + *
  4. Recursively search through the supermethods hierarchy of the given method's class. + *
+ *

In this context, the term recursively means that the search + * process continues by returning to step #1 with the current annotation, + * interface, or superclass as the class to look for annotations on. + * @param method the method to look for annotations on + * @param annotationType the type of annotation to look for + * @return the corresponding annotation descriptor if the annotation was found; + * otherwise {@code null} + * @see #findAnnotationDescriptor(Class, Class) + * @see AnnotationUtils#findAnnotationDeclaringClass(Class, Class) + * @see #findAnnotationDescriptorForTypes(Class, Class...) + * + * @since 5.0 + */ + public static AnnotationOnMethodDescriptor findAnnotationDescriptor( + Method method, Class annotationType) { + return findAnnotationDescriptor(method, method.getDeclaringClass(), method.getDeclaringClass(), + new HashSet<>(), annotationType); + } + + /** + * Perform the search algorithm for {@link #findAnnotationDescriptor(Method, Class)}, + * avoiding endless recursion by tracking which annotations have already been + * visited. + * @param method the method to look for annotations on + * @param declaringClass the class to look for method in + * @param rootDeclaringClass the root method's declaring class + * @param visited the set of annotations that have already been visited + * @param annotationType the type of annotation to look for + * @return the corresponding annotation descriptor if the annotation was found; + * otherwise {@code null} + */ + private static AnnotationOnMethodDescriptor findAnnotationDescriptor(Method method, + Class declaringClass, Class rootDeclaringClass, Set visited, Class annotationType) { + Assert.notNull(annotationType, "Annotation type must not be null"); + if (declaringClass == null || Object.class == declaringClass) { + return null; + } + Method foundMethod = ReflectionUtils.findMethod(declaringClass, method.getName(), method.getParameterTypes()); + if (foundMethod == null) { + return null; + } + + // Declared locally? + if (AnnotationUtils.isAnnotationDeclaredLocally(annotationType, foundMethod)) { + return new AnnotationOnMethodDescriptor<>(foundMethod, declaringClass, rootDeclaringClass, null, + AnnotationUtils.getAnnotation(foundMethod, annotationType)); + } + + // Declared on a composed annotation (i.e., as a meta-annotation)? + for (Annotation composedAnnotation : foundMethod.getDeclaredAnnotations()) { + if (!AnnotationUtils.isInJavaLangAnnotationPackage(composedAnnotation) && visited.add(composedAnnotation)) { + AnnotationDescriptor descriptor = findAnnotationDescriptor(composedAnnotation.annotationType(), + annotationType); + if (descriptor != null) { + return new AnnotationOnMethodDescriptor<>(foundMethod, descriptor.getDeclaringClass(), + rootDeclaringClass, composedAnnotation, descriptor.getAnnotation()); + } + } + } + + for (Class ifc : foundMethod.getDeclaringClass().getInterfaces()) { + AnnotationOnMethodDescriptor descriptor = findAnnotationDescriptor(foundMethod, ifc, rootDeclaringClass, + visited, annotationType); + if (descriptor != null) { + return new AnnotationOnMethodDescriptor<>(descriptor.getDeclaringMethod(), + descriptor.getDeclaringClass(), descriptor.getRootDeclaringClass(), + descriptor.getComposedAnnotation(), descriptor.getAnnotation()); + } + } + + // Declared on a superclass? + return findAnnotationDescriptor(foundMethod, foundMethod.getDeclaringClass().getSuperclass(), + rootDeclaringClass, visited, annotationType); + } + + /** + * Find the {@link UntypedAnnotationOnMethodDescriptor} for the first {@link Method} + * in the inheritance hierarchy of the specified {@code method} (including + * the specified {@code method} itself) which declares at least one of the + * specified {@code annotationTypes}. + *

This method traverses the annotations, interfaces's methods, and supermethods + * of the specified {@code method} if no annotation can be found on the given + * method itself. + *

This method explicitly handles both class-level and method-level annotations which are not + * declared as {@linkplain java.lang.annotation.Inherited inherited} as + * well as meta-annotations. + *

The algorithm operates as follows: + *

    + *
  1. Search for a local declaration of one of the annotation types on + * the given class and return a corresponding {@code UntypedAnnotationOnMethodDescriptor} + * if found. + *
  2. Recursively search through all annotations that the given method declares. + *
  3. Recursively search through all interfaces's methods implemented by the given method's class. + *
  4. Recursively search through the supermethods hierarchy of the given method's class. + *
+ *

In this context, the term recursively means that the search + * process continues by returning to step #1 with the current annotation, + * interface, or superclass as the class to look for annotations on. + * @param method the method to look for annotations on + * @param annotationTypes the types of annotations to look for + * @return the corresponding annotation descriptor if one of the annotations + * was found; otherwise {@code null} + * @see #findAnnotationDescriptorForTypes(Class, Class...) + * @see AnnotationUtils#findAnnotationDeclaringClassForTypes(java.util.List, Class) + * @see #findAnnotationDescriptor(Class, Class) + * + * @since 5.0 + */ + @SafeVarargs + public static UntypedAnnotationOnMethodDescriptor findAnnotationDescriptorForTypes( + Method method, Class... annotationTypes) { + return findAnnotationDescriptorForTypes(method, method.getDeclaringClass(), method.getDeclaringClass(), + new HashSet<>(), annotationTypes); + } + + /** + * Perform the search algorithm for {@link #findAnnotationDescriptorForTypes(Method, Class...)}, + * avoiding endless recursion by tracking which annotations have already been + * visited. + * @param method the method to look for annotations on + * @param declaringClass the class to look for method in + * @param rootDeclaringClass the root method's declaring class + * @param visited the set of annotations that have already been visited + * @param annotationTypes the types of annotations to look for + * @return the corresponding annotation descriptor if one of the annotations + * was found; otherwise {@code null} + */ + @SuppressWarnings("unchecked") + private static UntypedAnnotationOnMethodDescriptor findAnnotationDescriptorForTypes( + Method method, Class declaringClass, Class rootDeclaringClass, Set visited, + Class... annotationTypes) { + + Assert.notNull(method, "Method must not be null"); + assertNonEmptyAnnotationTypeArray(annotationTypes, "The list of annotation types must not be empty"); + if (declaringClass == null || Object.class == declaringClass) { + return null; + } + + Method foundMethod = ReflectionUtils.findMethod(declaringClass, method.getName(), method.getParameterTypes()); + if (foundMethod == null) { + return null; + } + + // Declared locally? + for (Class annotationType : annotationTypes) { + if (AnnotationUtils.isAnnotationDeclaredLocally(annotationType, foundMethod)) { + return new UntypedAnnotationOnMethodDescriptor(foundMethod, declaringClass, rootDeclaringClass, + null, AnnotationUtils.getAnnotation(foundMethod, annotationType)); + } + } + + // Declared on a composed annotation (i.e., as a meta-annotation)? + for (Annotation composedAnnotation : foundMethod.getDeclaredAnnotations()) { + if (!AnnotationUtils.isInJavaLangAnnotationPackage(composedAnnotation) && visited.add(composedAnnotation)) { + UntypedAnnotationDescriptor descriptor = findAnnotationDescriptorForTypes( + composedAnnotation.annotationType(), annotationTypes); + if (descriptor != null) { + return new UntypedAnnotationOnMethodDescriptor(foundMethod, descriptor.getDeclaringClass(), + rootDeclaringClass, composedAnnotation, descriptor.getAnnotation()); + } + } + } + + for (Class ifc : foundMethod.getDeclaringClass().getInterfaces()) { + UntypedAnnotationOnMethodDescriptor descriptor = findAnnotationDescriptorForTypes(foundMethod, + ifc, rootDeclaringClass, visited, annotationTypes); + if (descriptor != null) { + return new UntypedAnnotationOnMethodDescriptor(descriptor.getDeclaringMethod(), + descriptor.getDeclaringClass(), descriptor.getRootDeclaringClass(), + descriptor.getComposedAnnotation(), descriptor.getAnnotation()); + } + } + + // Declared on a superclass? + return findAnnotationDescriptorForTypes(foundMethod, foundMethod.getDeclaringClass().getSuperclass(), + rootDeclaringClass, visited, annotationTypes); + } + /** * Find the {@link UntypedAnnotationDescriptor} for the first {@link Class} * in the inheritance hierarchy of the specified {@code clazz} (including @@ -292,8 +487,16 @@ public AnnotationDescriptor(Class rootDeclaringClass, T annotation) { this(rootDeclaringClass, rootDeclaringClass, null, annotation); } - public AnnotationDescriptor(Class rootDeclaringClass, Class declaringClass, - Annotation composedAnnotation, T annotation) { + public AnnotationDescriptor(Class rootDeclaringClass, Class declaringClass, Annotation composedAnnotation, + T annotation) { + this(rootDeclaringClass, declaringClass, composedAnnotation, annotation, + AnnotatedElementUtils.findMergedAnnotationAttributes(rootDeclaringClass, + annotation.annotationType().getName(), false,false)); + } + + protected AnnotationDescriptor( + Class rootDeclaringClass, Class declaringClass, Annotation composedAnnotation, T annotation, + AnnotationAttributes annotationAttributes) { Assert.notNull(rootDeclaringClass, "rootDeclaringClass must not be null"); Assert.notNull(annotation, "annotation must not be null"); @@ -301,8 +504,7 @@ public AnnotationDescriptor(Class rootDeclaringClass, Class declaringClass this.declaringClass = declaringClass; this.composedAnnotation = composedAnnotation; this.annotation = annotation; - this.annotationAttributes = AnnotatedElementUtils.findMergedAnnotationAttributes( - rootDeclaringClass, annotation.annotationType().getName(), false, false); + this.annotationAttributes = annotationAttributes; } public Class getRootDeclaringClass() { @@ -362,6 +564,37 @@ public String toString() { } } + /** + * Descriptor for an {@link Annotation}, including the {@linkplain + * #getDeclaringMethod()} method} on which the annotation is declared + * as well as the actual {@linkplain #getAnnotation() annotation} instance. + * + * @see AnnotationDescriptor + */ + public static class AnnotationOnMethodDescriptor extends AnnotationDescriptor { + private final Method declaringMethod; + + public AnnotationOnMethodDescriptor( + Method declaringMethod, Class declaringClass, Class rootDeclaringClass, + Annotation composedAnnotation, T annotation) { + super(rootDeclaringClass, declaringClass, composedAnnotation, annotation, + AnnotatedElementUtils.findMergedAnnotationAttributes(declaringMethod, + annotation.annotationType().getName(), false,false)); + this.declaringMethod = declaringMethod; + } + + @Override + @SuppressWarnings("unchecked") + public T synthesizeAnnotation() { + return AnnotationUtils.synthesizeAnnotation(getAnnotationAttributes(), + (Class) getAnnotationType(), declaringMethod); + } + + public Method getDeclaringMethod() { + return declaringMethod; + } + + } /** * Untyped extension of {@code AnnotationDescriptor} that is used @@ -380,6 +613,11 @@ public UntypedAnnotationDescriptor(Class rootDeclaringClass, Class declari super(rootDeclaringClass, declaringClass, composedAnnotation, annotation); } + protected UntypedAnnotationDescriptor(Class rootDeclaringClass, Class declaringClass, + Annotation composedAnnotation, Annotation annotation, AnnotationAttributes annotationAttributes) { + super(rootDeclaringClass, declaringClass, composedAnnotation, annotation, annotationAttributes); + } + /** * Throws an {@link UnsupportedOperationException} since the type of annotation * represented by the {@link #getAnnotationAttributes AnnotationAttributes} in @@ -393,4 +631,34 @@ public Annotation synthesizeAnnotation() { } } + /** + * Extension of {@code UntypedAnnotationDescriptor} that is used + * to describe the declaration of one of several candidate annotation types + * where the actual annotation type cannot be predetermined on the target method. + * + * @see UntypedAnnotationDescriptor + */ + public static class UntypedAnnotationOnMethodDescriptor extends UntypedAnnotationDescriptor { + private final Method declaringMethod; + + public UntypedAnnotationOnMethodDescriptor( + Method declaringMethod, Class declaringClass, Class rootDeclaringClass, Annotation composedAnnotation, + Annotation annotation) { + this(declaringMethod, rootDeclaringClass, declaringClass, composedAnnotation, annotation, + AnnotatedElementUtils.findMergedAnnotationAttributes(declaringMethod, + annotation.annotationType().getName(), false, false)); + } + + private UntypedAnnotationOnMethodDescriptor( + Method declaringMethod, Class rootDeclaringClass, Class declaringClass, Annotation composedAnnotation, + Annotation annotation, AnnotationAttributes annotationAttributes) { + super(rootDeclaringClass, declaringClass, composedAnnotation, annotation, annotationAttributes); + this.declaringMethod = declaringMethod; + } + + public Method getDeclaringMethod() { + return declaringMethod; + } + } + } diff --git a/spring-test/src/test/java/org/springframework/test/context/configuration/method/ContextConfigurationAppliedOnSuperMethodTest.java b/spring-test/src/test/java/org/springframework/test/context/configuration/method/ContextConfigurationAppliedOnSuperMethodTest.java new file mode 100644 index 000000000000..34088d70bbf4 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/configuration/method/ContextConfigurationAppliedOnSuperMethodTest.java @@ -0,0 +1,78 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.configuration.method; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringRunner; + +import static org.junit.Assert.assertEquals; + +/** + * Unit tests for {@link ContextConfiguration} annotation presented on inherited methods. + * @author Sergei Ustimenko + * @since 5.0 + */ +@RunWith(SpringRunner.class) +@ContextConfiguration(classes = ContextConfigurationOnMethodTest.TypeContext.class) +public class ContextConfigurationAppliedOnSuperMethodTest + extends ContextConfigurationOnMethodTest.ContextConfigurationOnMethodParent + implements ContextConfigurationOnMethodTest.ContextConfigurationInterface, + ContextConfigurationOnMethodTest.ContextConfigurationMetaInterface { + + @Autowired + protected String bean; + + @Test + @Override + public void shouldPickUpParentBean() { + assertEquals("parent", bean); + } + + @Test + @Override + public void shouldPickUpParentParentBean() { + assertEquals("parentParent", bean); + } + + @Test + @Override + public void shouldPickUpMetaParent() { + assertEquals("metaOnParent", bean); + } + + @Test + @Override + public void shouldPickUpMetaParentParent() { + assertEquals("metaOnParentParent", bean); + } + + @Test + @Override + public void shouldPickUpConfigurationFromParentInterface() { + assertEquals("parentInterface", bean); + } + + @Test + @Override + public void shouldPickUpMetaFromParentInterface() { + assertEquals("metaOnParentInterface", bean); + } +} diff --git a/spring-test/src/test/java/org/springframework/test/context/configuration/method/ContextConfigurationOnMethodTest.java b/spring-test/src/test/java/org/springframework/test/context/configuration/method/ContextConfigurationOnMethodTest.java new file mode 100644 index 000000000000..6c09f2e3ddaf --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/configuration/method/ContextConfigurationOnMethodTest.java @@ -0,0 +1,168 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.configuration.method; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringRunner; + +import static org.junit.Assert.assertEquals; + +/** + * Unit tests for {@link ContextConfiguration} annotation presented on the method. + * @author Sergei Ustimenko + * @since 5.0 + */ +@RunWith(SpringRunner.class) +@ContextConfiguration(classes = ContextConfigurationOnMethodTest.TypeContext.class) +public class ContextConfigurationOnMethodTest { + + @Autowired + protected String bean; + + @Test + @ContextConfiguration(classes = MethodContext.class) + public void methodLevelAnnotationShouldOverrideTypeAnnotation() { + assertEquals("method", bean); + } + + @Test + public void typeLevelAnnotationShouldInjectRightValue() { + assertEquals("type", bean); + } + + // --------------------------------------------------------------- + + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) + @ContextConfiguration(classes = MetaParentConfig.class) + public @interface MetaOnParent {} + + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) + @ContextConfiguration(classes = MetaParentParentConfig.class) + public @interface MetaOnParentParent {} + + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) + @ContextConfiguration(classes = MetaParentInterfaceConfig.class) + public @interface MetaOnParentInterface {} + + public interface ParentContextConfigurationInterface { + @ContextConfiguration(classes = ContextConfigurationOnMethodTest.ParentInterfaceConfig.class) + void shouldPickUpConfigurationFromParentInterface(); + } + + public interface ContextConfigurationInterface extends ParentContextConfigurationInterface {} + + public interface ParentContextConfigurationMetaInterface { + @ContextConfigurationOnMethodTest.MetaOnParentInterface + void shouldPickUpMetaFromParentInterface(); + } + + public interface ContextConfigurationMetaInterface extends ParentContextConfigurationMetaInterface {} + + public static class TypeContext { + @Bean + public String testBean() { + return "type"; + } + } + + public static class MethodContext { + @Bean + public String testBean() { + return "method"; + } + } + + public static class ParentConfig { + @Bean + public String testBean() { + return "parent"; + } + } + + public static class ParentParentConfig { + @Bean + public String testBean() { + return "parentParent"; + } + } + + public static class MetaParentConfig { + @Bean + public String testBean() { + return "metaOnParent"; + } + } + + public static class MetaParentParentConfig { + @Bean + public String testBean() { + return "metaOnParentParent"; + } + } + + public static class ParentInterfaceConfig { + @Bean + public String testBean() { + return "parentInterface"; + } + } + + public static class MetaParentInterfaceConfig { + @Bean + public String testBean() { + return "metaOnParentInterface"; + } + } + + public static class ContextConfigurationOnMethodParentParent { + @ContextConfiguration(classes = ContextConfigurationOnMethodTest.ParentConfig.class) + public void shouldPickUpParentBean() { + // for test purposes + } + + @ContextConfigurationOnMethodTest.MetaOnParent + public void shouldPickUpMetaParent() { + // for test purposes + } + } + + public static class ContextConfigurationOnMethodParent extends ContextConfigurationOnMethodParentParent { + @ContextConfiguration(classes = ContextConfigurationOnMethodTest.ParentParentConfig.class) + public void shouldPickUpParentParentBean() { + // for test purposes + } + + @ContextConfigurationOnMethodTest.MetaOnParentParent + public void shouldPickUpMetaParentParent() { + // for test purposes + } + } + +} \ No newline at end of file diff --git a/spring-test/src/test/java/org/springframework/test/context/configuration/method/ContextHierarchyAppliedOnSuperMethodTest.java b/spring-test/src/test/java/org/springframework/test/context/configuration/method/ContextHierarchyAppliedOnSuperMethodTest.java new file mode 100644 index 000000000000..6dcf2cbab982 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/configuration/method/ContextHierarchyAppliedOnSuperMethodTest.java @@ -0,0 +1,81 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.configuration.method; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextHierarchy; +import org.springframework.test.context.junit4.SpringRunner; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +/** + * Unit tests for {@link ContextHierarchy} presented on inherited methods + * @author Sergei Ustimenko + * @since 5.0 + */ +@RunWith(SpringRunner.class) +@ContextConfiguration(classes = ContextHierarchyOnMethodTest.MainContext.class) +public class ContextHierarchyAppliedOnSuperMethodTest + extends ContextHierarchyOnMethodTest.ContextHierarchyOnMethodParent + implements ContextHierarchyOnMethodTest.ContextHierarchyInterface, + ContextHierarchyOnMethodTest.ContextHierarchyMetaInterface { + + @Autowired + private ApplicationContext applicationContext; + + @Autowired + private String testBean; + + @Test + @Override + public void shouldLoadContextsInReverseOrderFromParentParent() { + assertNotNull("child ApplicationContext", applicationContext); + assertNotNull("parent ApplicationContext", applicationContext.getParent()); + assertNull("grandparent ApplicationContext", applicationContext.getParent().getParent()); + assertEquals("overridenParent", testBean); + } + + + @Test + @Override + public void shouldLoadContextsForMetaOnParentParent() { + assertNotNull("child ApplicationContext", applicationContext); + assertNotNull("parent ApplicationContext", applicationContext.getParent()); + assertNull("grandparent ApplicationContext", applicationContext.getParent().getParent()); + assertEquals("overridenMetaOnParent", testBean); + } + + @Test + @Override + public void shouldPickUpHierarchyFromParentInterface() { + assertEquals("parentInterface", testBean); + } + + @Test + @Override + public void shouldPickUpMetaHierarchyFromParentInterface() { + assertEquals("metaOnParentInterface", testBean); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/configuration/method/ContextHierarchyOnMethodTest.java b/spring-test/src/test/java/org/springframework/test/context/configuration/method/ContextHierarchyOnMethodTest.java new file mode 100644 index 000000000000..4d8cc60ea0cc --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/configuration/method/ContextHierarchyOnMethodTest.java @@ -0,0 +1,197 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.configuration.method; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextHierarchy; +import org.springframework.test.context.junit4.SpringRunner; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +/** + * Unit tests for {@link ContextHierarchy} presented on the method-level + * @author Sergei Ustimenko + * @since 5.0 + */ +@RunWith(SpringRunner.class) +@ContextConfiguration(classes = ContextHierarchyOnMethodTest.MainContext.class) +public class ContextHierarchyOnMethodTest { + + @Autowired + private ApplicationContext applicationContext; + + @Autowired + private String testBean; + + @Test + @ContextHierarchy({ + @ContextConfiguration(classes = FirstContext.class), @ContextConfiguration(classes = SecondContext.class)}) + public void shouldLoadContextsInStraightOrder() { + assertEquals("second", testBean); + } + + @Test + @ContextHierarchy({ + @ContextConfiguration(classes = SecondContext.class), @ContextConfiguration(classes = FirstContext.class)}) + public void shouldLoadContextsInReverseOrder() { + assertEquals("first", testBean); + } + + + @Test + @ContextHierarchy({ + @ContextConfiguration(classes = FirstContext.class), @ContextConfiguration(classes = SecondContext.class)}) + public void shouldCreateProperHierarchy() { + assertNotNull("child ApplicationContext", applicationContext); + assertNotNull("parent ApplicationContext", applicationContext.getParent()); + assertNull("grandparent ApplicationContext", applicationContext.getParent().getParent()); + } + + // -------------------------------------------------------------------------- + + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) + @ContextHierarchy({@ContextConfiguration(classes = MetaOnParentContext.class)}) + public @interface MetaOnParent {} + + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) + @ContextHierarchy({@ContextConfiguration(classes = MetaOnParentParentContext.class)}) + public @interface MetaOnParentParent {} + + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) + @ContextHierarchy({@ContextConfiguration(classes = MetaOnParentInterfaceContext.class)}) + public @interface MetaOnParentInterface {} + + public interface ParentContextHierarchyInterface { + @ContextHierarchy(@ContextConfiguration(classes = ContextHierarchyOnMethodTest.ParentInterfaceContext.class)) + void shouldPickUpHierarchyFromParentInterface(); + } + + public interface ContextHierarchyInterface extends ParentContextHierarchyInterface {} + + public interface ParentContextHierarchyMetaInterface { + @ContextHierarchyOnMethodTest.MetaOnParentInterface + void shouldPickUpMetaHierarchyFromParentInterface(); + } + + public interface ContextHierarchyMetaInterface extends ParentContextHierarchyMetaInterface {} + + public static class FirstContext { + @Bean + public String testBean() { + return "first"; + } + } + + public static class SecondContext { + @Bean + public String testBean() { + return "second"; + } + } + + public static class ParentLastContext { + @Bean + public String testBean() { + return "overridenParent"; + } + } + + public static class ParentParentLastContext { + @Bean + public String testBean() { + return "parentParent"; + } + } + + public static class ParentInterfaceContext { + @Bean + public String testBean() { + return "parentInterface"; + } + } + + public static class MainContext { + @Bean + public String testBean() { + return "main"; + } + } + + public static class MetaOnParentContext { + @Bean + public String testBean() { + return "overridenMetaOnParent"; + } + } + + public static class MetaOnParentParentContext { + @Bean + public String testBean() { + return "metaOnParentParent"; + } + } + + public static class MetaOnParentInterfaceContext { + @Bean + public String testBean() { + return "metaOnParentInterface"; + } + } + + public static class ContextHierarchyOnMethodParent extends ContextHierarchyOnMethodParentParent { + @Override + @ContextHierarchy({@ContextConfiguration(classes = ContextHierarchyOnMethodTest.ParentLastContext.class)}) + public void shouldLoadContextsInReverseOrderFromParentParent() { + // for tes purposes + } + + @Override + @ContextHierarchyOnMethodTest.MetaOnParent + public void shouldLoadContextsForMetaOnParentParent() { + // for tes purposes + } + } + + public static class ContextHierarchyOnMethodParentParent { + @ContextHierarchy({@ContextConfiguration(classes = ContextHierarchyOnMethodTest.ParentParentLastContext.class)}) + public void shouldLoadContextsInReverseOrderFromParentParent() { + // for tes purposes + } + + @ContextHierarchyOnMethodTest.MetaOnParentParent + public void shouldLoadContextsForMetaOnParentParent() { + // for tes purposes + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/support/AbstractContextConfigurationUtilsTests.java b/spring-test/src/test/java/org/springframework/test/context/support/AbstractContextConfigurationUtilsTests.java index 578273912603..7f558a630fc9 100644 --- a/spring-test/src/test/java/org/springframework/test/context/support/AbstractContextConfigurationUtilsTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/support/AbstractContextConfigurationUtilsTests.java @@ -20,6 +20,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import java.lang.reflect.Method; import java.util.Collections; import java.util.Set; @@ -39,7 +40,10 @@ import org.springframework.test.context.TestContextBootstrapper; import org.springframework.test.context.web.WebAppConfiguration; -import static org.junit.Assert.*; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; /** * Abstract base class for tests involving {@link ContextLoaderUtils}, @@ -76,6 +80,18 @@ void assertAttributes(ContextConfigurationAttributes attributes, Class expect assertEquals("context loader", expectedContextLoaderClass, attributes.getContextLoaderClass()); } + void assertAttributes(ContextConfigurationAttributes attributes, Class expectedDeclaringClass, + Method expectedDeclaringMethod, String[] expectedLocations, Class[] expectedClasses, + Class expectedContextLoaderClass, boolean expectedInheritLocations) { + + assertEquals("declaring class", expectedDeclaringClass, attributes.getDeclaringClass()); + assertEquals("declaring method", expectedDeclaringMethod, attributes.getDeclaringMethod()); + assertArrayEquals("locations", expectedLocations, attributes.getLocations()); + assertArrayEquals("classes", expectedClasses, attributes.getClasses()); + assertEquals("inherit locations", expectedInheritLocations, attributes.isInheritLocations()); + assertEquals("context loader", expectedContextLoaderClass, attributes.getContextLoaderClass()); + } + void assertMergedConfig(MergedContextConfiguration mergedConfig, Class expectedTestClass, String[] expectedLocations, Class[] expectedClasses, Class expectedContextLoaderClass) { @@ -129,14 +145,14 @@ static class BarConfig { @ContextConfiguration("/foo.xml") @ActiveProfiles(profiles = "foo") @Retention(RetentionPolicy.RUNTIME) - @Target(ElementType.TYPE) + @Target({ElementType.TYPE, ElementType.METHOD}) public static @interface MetaLocationsFooConfig { } @ContextConfiguration @ActiveProfiles @Retention(RetentionPolicy.RUNTIME) - @Target(ElementType.TYPE) + @Target({ElementType.TYPE, ElementType.METHOD}) public static @interface MetaLocationsFooConfigWithOverrides { String[] locations() default "/foo.xml"; @@ -147,7 +163,7 @@ static class BarConfig { @ContextConfiguration("/bar.xml") @ActiveProfiles(profiles = "bar") @Retention(RetentionPolicy.RUNTIME) - @Target(ElementType.TYPE) + @Target({ElementType.TYPE, ElementType.METHOD}) public static @interface MetaLocationsBarConfig { } @@ -213,4 +229,68 @@ static class PropertiesLocationsFoo { static class PropertiesClassesFoo { } + + static class BareMethodAnnotations { + @ContextConfiguration + public void something() { + // for test purposes + } + } + + static class LocationsMethodFoo { + @ContextConfiguration(locations = "/foo.xml", inheritLocations = false) + public void something() { + // for test purposes + } + } + + static class ClassesMethodFoo { + @ContextConfiguration(classes = FooConfig.class, inheritLocations = false) + public void something() { + // for test purposes + } + } + + static class LocationsMethodBar extends LocationsMethodFoo { + @ContextConfiguration(locations = "/bar.xml", inheritLocations = true, loader = AnnotationConfigContextLoader.class) + public void something() { + // for test purposes + } + } + + static class ClassesMethodBar extends ClassesMethodFoo { + @ContextConfiguration(classes = BarConfig.class, inheritLocations = true, loader = AnnotationConfigContextLoader.class) + public void something() { + // for test purposes + } + } + + static class MetaLocationsMethodFoo { + @MetaLocationsFooConfig + public void something() { + // for test purposes + } + } + + static class MetaLocationsMethodBar extends MetaLocationsMethodFoo { + @MetaLocationsBarConfig + public void something() { + // for test purposes + } + } + + static class MetaLocationsMethodFooWithOverrides { + @MetaLocationsFooConfigWithOverrides + public void something() { + // for test purposes + } + } + + static class MetaLocationsMethodFooWithOverriddenAttributes { + @MetaLocationsFooConfigWithOverrides(locations = {"foo1.xml", "foo2.xml"}, profiles = {"foo1", "foo2"}) + public void something() { + // for test purposes + } + } + } diff --git a/spring-test/src/test/java/org/springframework/test/context/support/ContextLoaderUtilsConfigurationAttributesOnMethodTests.java b/spring-test/src/test/java/org/springframework/test/context/support/ContextLoaderUtilsConfigurationAttributesOnMethodTests.java new file mode 100644 index 000000000000..3776f0978463 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/support/ContextLoaderUtilsConfigurationAttributesOnMethodTests.java @@ -0,0 +1,261 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.support; + +import java.lang.reflect.Method; +import java.util.List; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import org.springframework.core.annotation.AnnotationConfigurationException; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextConfigurationAttributes; +import org.springframework.test.context.ContextLoader; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.core.CombinableMatcher.either; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.springframework.test.context.support.ContextLoaderUtils.resolveContextConfigurationAttributes; + +/** + * Unit tests for {@link ContextLoaderUtils} involving {@link ContextConfigurationAttributes} parsed from method-level + * annotations. + * + * @author Sergei Ustimenko + * @since 5.0 + */ +public class ContextLoaderUtilsConfigurationAttributesOnMethodTests extends AbstractContextConfigurationUtilsTests { + @Rule + public final ExpectedException exception = ExpectedException.none(); + + + private void assertLocationsMethodFooAttributes(ContextConfigurationAttributes attributes) throws Exception { + assertAttributes(attributes, + LocationsMethodFoo.class, + LocationsMethodFoo.class.getDeclaredMethod("something"), + new String[] {"/foo.xml"}, + EMPTY_CLASS_ARRAY, + ContextLoader.class, + false); + } + + private void assertClassesMethodFooAttributes(ContextConfigurationAttributes attributes) throws Exception { + assertAttributes(attributes, + ClassesMethodFoo.class, + ClassesMethodFoo.class.getDeclaredMethod("something"), + EMPTY_STRING_ARRAY, + new Class[] {FooConfig.class}, + ContextLoader.class, + false); + } + + private void assertLocationsMethodBarAttributes(ContextConfigurationAttributes attributes) throws Exception { + assertAttributes(attributes, + LocationsMethodBar.class, + LocationsMethodBar.class.getDeclaredMethod("something"), + new String[] {"/bar.xml"}, + EMPTY_CLASS_ARRAY, + AnnotationConfigContextLoader.class, + true); + } + + private void assertClassesMethodBarAttributes(ContextConfigurationAttributes attributes) throws Exception { + assertAttributes(attributes, + ClassesMethodBar.class, + ClassesMethodBar.class.getDeclaredMethod("something"), + EMPTY_STRING_ARRAY, + new Class[] {BarConfig.class}, + AnnotationConfigContextLoader.class, + true); + } + + @Test + public void resolveConfigAttributesWithConflictingLocations() throws Exception { + exception.expect(AnnotationConfigurationException.class); + exception.expectMessage(containsString(ConflictingLocations.class.getName())); + exception.expectMessage(either(containsString("attribute 'value' and its alias 'locations'")) + .or(containsString("attribute 'locations' and its alias 'value'"))); + exception.expectMessage(either(containsString("values of [{x}] and [{y}]")).or(containsString( + "values of [{y}] and [{x}]"))); + exception.expectMessage(containsString("but only one is permitted")); + resolveContextConfigurationAttributes(ConflictingLocations.class.getDeclaredMethod("something")); + } + + @Test + public void resolveConfigAttributesWithBareAnnotations() throws Exception { + Class testClass = BareMethodAnnotations.class; + Method something = testClass.getDeclaredMethod("something"); + List attributesList = resolveContextConfigurationAttributes(something); + + assertNotNull(attributesList); + assertEquals(1, attributesList.size()); + assertAttributes(attributesList.get(0), + testClass, + something, + EMPTY_STRING_ARRAY, + EMPTY_CLASS_ARRAY, + ContextLoader.class, + true); + } + + @Test + public void resolveConfigAttributesWithLocalAnnotationAndLocations() throws Exception { + List attributesList = + resolveContextConfigurationAttributes(LocationsMethodFoo.class.getDeclaredMethod("something")); + + assertNotNull(attributesList); + assertEquals(1, attributesList.size()); + assertLocationsMethodFooAttributes(attributesList.get(0)); + } + + @Test + public void resolveConfigAttributesWithMetaAnnotationAndLocations() throws Exception { + Class testClass = MetaLocationsMethodFoo.class; + Method something = testClass.getDeclaredMethod("something"); + List attributesList = resolveContextConfigurationAttributes(something); + + assertNotNull(attributesList); + assertEquals(1, attributesList.size()); + assertAttributes(attributesList.get(0), + testClass, + something, + new String[] {"/foo.xml"}, + EMPTY_CLASS_ARRAY, + ContextLoader.class, + true); + } + + @Test + public void resolveConfigAttributesWithMetaAnnotationAndLocationsAndOverrides() throws Exception { + Class testClass = MetaLocationsMethodFooWithOverrides.class; + Method something = testClass.getDeclaredMethod("something"); + List attributesList = resolveContextConfigurationAttributes(something); + + assertNotNull(attributesList); + assertEquals(1, attributesList.size()); + assertAttributes(attributesList.get(0), + testClass, + something, + new String[] {"/foo.xml"}, + EMPTY_CLASS_ARRAY, + ContextLoader.class, + true); + } + + @Test + public void resolveConfigAttributesWithMetaAnnotationAndLocationsAndOverriddenAttributes() throws Exception { + Class testClass + = MetaLocationsMethodFooWithOverriddenAttributes.class; + Method something = testClass.getDeclaredMethod("something"); + List attributesList = resolveContextConfigurationAttributes(something); + + assertNotNull(attributesList); + assertEquals(1, attributesList.size()); + assertAttributes(attributesList.get(0), + testClass, + something, + new String[] {"foo1.xml", "foo2.xml"}, + EMPTY_CLASS_ARRAY, + ContextLoader.class, + true); + } + + @Test + public void resolveConfigAttributesWithMetaAnnotationAndLocationsInClassHierarchy() throws Exception { + Class testClass = MetaLocationsMethodBar.class; + Method something = testClass.getDeclaredMethod("something"); + List attributesList = resolveContextConfigurationAttributes(something); + + assertNotNull(attributesList); + assertEquals(2, attributesList.size()); + assertAttributes(attributesList.get(0), + testClass, + something, + new String[] {"/bar.xml"}, + EMPTY_CLASS_ARRAY, + ContextLoader.class, + true); + assertAttributes(attributesList.get(1), + MetaLocationsMethodFoo.class, + MetaLocationsMethodFoo.class.getDeclaredMethod("something"), + new String[] {"/foo.xml"}, + EMPTY_CLASS_ARRAY, + ContextLoader.class, + true); + } + + @Test + public void resolveConfigAttributesWithLocalAnnotationAndClasses() throws Exception { + List attributesList = + resolveContextConfigurationAttributes(ClassesMethodFoo.class.getDeclaredMethod("something")); + + assertNotNull(attributesList); + assertEquals(1, attributesList.size()); + assertClassesMethodFooAttributes(attributesList.get(0)); + } + + @Test + public void resolveConfigAttributesWithLocalAndInheritedAnnotationsAndLocations() throws Exception { + List attributesList = + resolveContextConfigurationAttributes(LocationsMethodBar.class.getDeclaredMethod("something")); + + assertNotNull(attributesList); + assertEquals(2, attributesList.size()); + assertLocationsMethodBarAttributes(attributesList.get(0)); + assertLocationsMethodFooAttributes(attributesList.get(1)); + } + + @Test + public void resolveConfigAttributesWithLocalAndInheritedAnnotationsAndClasses() throws Exception { + List attributesList = + resolveContextConfigurationAttributes(ClassesMethodBar.class.getDeclaredMethod("something")); + + assertNotNull(attributesList); + assertEquals(2, attributesList.size()); + assertClassesMethodBarAttributes(attributesList.get(0)); + assertClassesMethodFooAttributes(attributesList.get(1)); + } + + @Test + public void resolveConfigAttributesWithLocationsAndClasses() throws Exception { + List attributesList = + resolveContextConfigurationAttributes(LocationsAndClasses.class.getDeclaredMethod("something")); + + assertNotNull(attributesList); + assertEquals(1, attributesList.size()); + } + + + // ------------------------------------------------------------------------- + + private static class ConflictingLocations { + @ContextConfiguration(value = "x", locations = "y") + public void something() { + // for test purposes + } + } + + private static class LocationsAndClasses { + @ContextConfiguration(locations = "x", classes = Object.class) + public void something() { + // for test purposes + } + } +} diff --git a/spring-test/src/test/java/org/springframework/test/context/support/ContextLoaderUtilsContextHierarchyOnMethodTests.java b/spring-test/src/test/java/org/springframework/test/context/support/ContextLoaderUtilsContextHierarchyOnMethodTests.java new file mode 100644 index 000000000000..e9cf32f1c38b --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/support/ContextLoaderUtilsContextHierarchyOnMethodTests.java @@ -0,0 +1,737 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.support; + +import java.lang.reflect.Method; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import org.junit.Test; + +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextConfigurationAttributes; +import org.springframework.test.context.ContextHierarchy; +import org.springframework.test.context.ContextLoader; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.hasItems; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; +import static org.springframework.test.context.support.ContextLoaderUtils.GENERATED_CONTEXT_HIERARCHY_LEVEL_PREFIX; +import static org.springframework.test.context.support.ContextLoaderUtils.buildContextHierarchyMap; +import static org.springframework.test.context.support.ContextLoaderUtils.resolveContextHierarchyAttributes; + +/** + * Unit tests for {@link ContextLoaderUtils} involving context hierarchies parsed from method-level annotations. + * + * @author Sergei Ustimenko + * @since 5.0 + */ +public class ContextLoaderUtilsContextHierarchyOnMethodTests extends AbstractContextConfigurationUtilsTests { + + @Test(expected = IllegalStateException.class) + public void resolveContextHierarchyAttributesForSingleTestClassWithContextConfigurationAndContextHierarchy() + throws Exception { + resolveContextHierarchyAttributes( + SingleTestClassWithContextConfigurationAndContextHierarchy.class.getDeclaredMethod("something")); + } + + @Test(expected = IllegalStateException.class) + public void + resolveContextHierarchyAttributesForSingleTestClassWithContextConfigurationAndContextHierarchyOnSingleMetaAnnotation() + throws Exception { + resolveContextHierarchyAttributes( + SingleTestClassWithContextConfigurationAndContextHierarchyOnSingleMetaAnnotation.class.getDeclaredMethod("something")); + } + + @Test + public void resolveContextHierarchyAttributesForSingleTestClassWithImplicitSingleLevelContextHierarchy() + throws Exception { + List> hierarchyAttributes = resolveContextHierarchyAttributes( + BareMethodAnnotations.class.getDeclaredMethod("something")); + assertEquals(1, hierarchyAttributes.size()); + List configAttributesList = hierarchyAttributes.get(0); + assertEquals(1, configAttributesList.size()); + } + + @Test + public void resolveContextHierarchyAttributesForSingleTestClassWithSingleLevelContextHierarchy() throws Exception { + List> hierarchyAttributes = resolveContextHierarchyAttributes( + SingleTestClassWithSingleLevelContextHierarchy.class.getDeclaredMethod("something")); + assertEquals(1, hierarchyAttributes.size()); + List configAttributesList = hierarchyAttributes.get(0); + assertEquals(1, configAttributesList.size()); + } + + @Test + public void resolveContextHierarchyAttributesForSingleTestClassWithSingleLevelContextHierarchyFromMetaAnnotation() + throws Exception { + Class testClass + = SingleTestClassWithSingleLevelContextHierarchyFromMetaAnnotation.class; + Method something = testClass.getDeclaredMethod("something"); + List> hierarchyAttributes = resolveContextHierarchyAttributes(something); + assertEquals(1, hierarchyAttributes.size()); + + List configAttributesList = hierarchyAttributes.get(0); + assertNotNull(configAttributesList); + assertEquals(1, configAttributesList.size()); + assertAttributes(configAttributesList.get(0), + testClass, + something, + new String[] {"A.xml"}, + EMPTY_CLASS_ARRAY, + ContextLoader.class, + true); + } + + @Test + public void resolveContextHierarchyAttributesForSingleTestClassWithTripleLevelContextHierarchy() throws Exception { + Class testClass + = SingleTestClassWithTripleLevelContextHierarchy.class; + Method something = testClass.getDeclaredMethod("something"); + List> hierarchyAttributes = resolveContextHierarchyAttributes(something); + assertEquals(1, hierarchyAttributes.size()); + + List configAttributesList = hierarchyAttributes.get(0); + assertNotNull(configAttributesList); + assertEquals(3, configAttributesList.size()); + assertAttributes(configAttributesList.get(0), + testClass, + something, + new String[] {"A.xml"}, + EMPTY_CLASS_ARRAY, + ContextLoader.class, + true); + assertAttributes(configAttributesList.get(1), + testClass, + something, + new String[] {"B.xml"}, + EMPTY_CLASS_ARRAY, + ContextLoader.class, + true); + assertAttributes(configAttributesList.get(2), + testClass, + something, + new String[] {"C.xml"}, + EMPTY_CLASS_ARRAY, + ContextLoader.class, + true); + } + + @Test + public void resolveContextHierarchyAttributesForTestClassHierarchyWithSingleLevelContextHierarchies() + throws Exception { + List> hierarchyAttributes = resolveContextHierarchyAttributes( + TestClass3WithSingleLevelContextHierarchy.class.getDeclaredMethod("something")); + assertEquals(3, hierarchyAttributes.size()); + + List configAttributesListClassLevel1 = hierarchyAttributes.get(0); + assertEquals(1, configAttributesListClassLevel1.size()); + assertThat(configAttributesListClassLevel1.get(0).getLocations()[0], equalTo("one.xml")); + + List configAttributesListClassLevel2 = hierarchyAttributes.get(1); + assertEquals(1, configAttributesListClassLevel2.size()); + assertArrayEquals(new String[] {"two-A.xml", "two-B.xml"}, + configAttributesListClassLevel2.get(0).getLocations()); + + List configAttributesListClassLevel3 = hierarchyAttributes.get(2); + assertEquals(1, configAttributesListClassLevel3.size()); + assertThat(configAttributesListClassLevel3.get(0).getLocations()[0], equalTo("three.xml")); + } + + @Test + public void + resolveContextHierarchyAttributesForTestClassHierarchyWithSingleLevelContextHierarchiesAndMetaAnnotations() + throws Exception { + Method something = TestClass3WithSingleLevelContextHierarchyFromMetaAnnotation.class.getDeclaredMethod( + "something"); + List> hierarchyAttributes = resolveContextHierarchyAttributes(something); + assertEquals(3, hierarchyAttributes.size()); + + List configAttributesListClassLevel1 = hierarchyAttributes.get(0); + assertEquals(1, configAttributesListClassLevel1.size()); + assertThat(configAttributesListClassLevel1.get(0).getLocations()[0], equalTo("A.xml")); + assertAttributes(configAttributesListClassLevel1.get(0), + TestClass1WithSingleLevelContextHierarchyFromMetaAnnotation.class, + TestClass1WithSingleLevelContextHierarchyFromMetaAnnotation.class.getDeclaredMethod("something"), + new String[] {"A.xml"}, + EMPTY_CLASS_ARRAY, + ContextLoader.class, + true); + + List configAttributesListClassLevel2 = hierarchyAttributes.get(1); + assertEquals(1, configAttributesListClassLevel2.size()); + assertArrayEquals(new String[] {"B-one.xml", "B-two.xml"}, + configAttributesListClassLevel2.get(0).getLocations()); + assertAttributes(configAttributesListClassLevel2.get(0), + TestClass2WithSingleLevelContextHierarchyFromMetaAnnotation.class, + TestClass2WithSingleLevelContextHierarchyFromMetaAnnotation.class.getDeclaredMethod("something"), + new String[] {"B-one.xml", "B-two.xml"}, + EMPTY_CLASS_ARRAY, + ContextLoader.class, + true); + + List configAttributesListClassLevel3 = hierarchyAttributes.get(2); + assertEquals(1, configAttributesListClassLevel3.size()); + assertThat(configAttributesListClassLevel3.get(0).getLocations()[0], equalTo("C.xml")); + assertAttributes(configAttributesListClassLevel3.get(0), + TestClass3WithSingleLevelContextHierarchyFromMetaAnnotation.class, + something, + new String[] {"C.xml"}, + EMPTY_CLASS_ARRAY, + ContextLoader.class, + true); + } + + private void assertOneTwo(List> hierarchyAttributes) { + assertEquals(2, hierarchyAttributes.size()); + + List configAttributesListClassLevel1 = hierarchyAttributes.get(0); + List configAttributesListClassLevel2 = hierarchyAttributes.get(1); + + assertEquals(1, configAttributesListClassLevel1.size()); + assertThat(configAttributesListClassLevel1.get(0).getLocations()[0], equalTo("one.xml")); + + assertEquals(1, configAttributesListClassLevel2.size()); + assertThat(configAttributesListClassLevel2.get(0).getLocations()[0], equalTo("two.xml")); + } + + @Test + public void resolveContextHierarchyAttributesForTestClassHierarchyWithBareContextConfigurationInSuperclass() + throws Exception { + assertOneTwo(resolveContextHierarchyAttributes( + TestClass2WithBareContextConfigurationInSuperclass.class.getDeclaredMethod("something"))); + } + + @Test + public void resolveContextHierarchyAttributesForTestClassHierarchyWithBareContextConfigurationInSubclass() + throws Exception { + assertOneTwo(resolveContextHierarchyAttributes( + TestClass2WithBareContextConfigurationInSubclass.class.getDeclaredMethod("something"))); + } + + @Test + public void + resolveContextHierarchyAttributesForTestClassHierarchyWithBareMetaContextConfigWithOverridesInSuperclass() + throws Exception { + assertOneTwo(resolveContextHierarchyAttributes( + TestClass2WithBareMetaContextConfigWithOverridesInSuperclass.class.getDeclaredMethod("something"))); + } + + @Test + public void resolveContextHierarchyAttributesForTestClassHierarchyWithBareMetaContextConfigWithOverridesInSubclass() + throws Exception { + assertOneTwo(resolveContextHierarchyAttributes( + TestClass2WithBareMetaContextConfigWithOverridesInSubclass.class.getDeclaredMethod("something"))); + } + + @Test + public void resolveContextHierarchyAttributesForTestClassHierarchyWithMultiLevelContextHierarchies() + throws Exception { + List> hierarchyAttributes = resolveContextHierarchyAttributes( + TestClass3WithMultiLevelContextHierarchy.class.getDeclaredMethod("something")); + assertEquals(3, hierarchyAttributes.size()); + + List configAttributesListClassLevel1 = hierarchyAttributes.get(0); + assertEquals(2, configAttributesListClassLevel1.size()); + assertThat(configAttributesListClassLevel1.get(0).getLocations()[0], equalTo("1-A.xml")); + assertThat(configAttributesListClassLevel1.get(1).getLocations()[0], equalTo("1-B.xml")); + + List configAttributesListClassLevel2 = hierarchyAttributes.get(1); + assertEquals(2, configAttributesListClassLevel2.size()); + assertThat(configAttributesListClassLevel2.get(0).getLocations()[0], equalTo("2-A.xml")); + assertThat(configAttributesListClassLevel2.get(1).getLocations()[0], equalTo("2-B.xml")); + + List configAttributesListClassLevel3 = hierarchyAttributes.get(2); + assertEquals(3, configAttributesListClassLevel3.size()); + assertThat(configAttributesListClassLevel3.get(0).getLocations()[0], equalTo("3-A.xml")); + assertThat(configAttributesListClassLevel3.get(1).getLocations()[0], equalTo("3-B.xml")); + assertThat(configAttributesListClassLevel3.get(2).getLocations()[0], equalTo("3-C.xml")); + } + + @Test + public void buildContextHierarchyMapForTestClassHierarchyWithMultiLevelContextHierarchies() throws Exception { + Map> map = buildContextHierarchyMap( + TestClass3WithMultiLevelContextHierarchy.class.getDeclaredMethod("something")); + + assertThat(map.size(), is(3)); + assertThat(map.keySet(), hasItems("alpha", "beta", "gamma")); + + List alphaConfig = map.get("alpha"); + assertThat(alphaConfig.size(), is(3)); + assertThat(alphaConfig.get(0).getLocations()[0], is("1-A.xml")); + assertThat(alphaConfig.get(1).getLocations()[0], is("2-A.xml")); + assertThat(alphaConfig.get(2).getLocations()[0], is("3-A.xml")); + + List betaConfig = map.get("beta"); + assertThat(betaConfig.size(), is(3)); + assertThat(betaConfig.get(0).getLocations()[0], is("1-B.xml")); + assertThat(betaConfig.get(1).getLocations()[0], is("2-B.xml")); + assertThat(betaConfig.get(2).getLocations()[0], is("3-B.xml")); + + List gammaConfig = map.get("gamma"); + assertThat(gammaConfig.size(), is(1)); + assertThat(gammaConfig.get(0).getLocations()[0], is("3-C.xml")); + } + + @Test + public void buildContextHierarchyMapForTestClassHierarchyWithMultiLevelContextHierarchiesAndUnnamedConfig() + throws Exception { + Map> map = buildContextHierarchyMap( + TestClass3WithMultiLevelContextHierarchyAndUnnamedConfig.class.getDeclaredMethod("something")); + + String level1 = GENERATED_CONTEXT_HIERARCHY_LEVEL_PREFIX + 1; + String level2 = GENERATED_CONTEXT_HIERARCHY_LEVEL_PREFIX + 2; + String level3 = GENERATED_CONTEXT_HIERARCHY_LEVEL_PREFIX + 3; + String level4 = GENERATED_CONTEXT_HIERARCHY_LEVEL_PREFIX + 4; + String level5 = GENERATED_CONTEXT_HIERARCHY_LEVEL_PREFIX + 5; + String level6 = GENERATED_CONTEXT_HIERARCHY_LEVEL_PREFIX + 6; + String level7 = GENERATED_CONTEXT_HIERARCHY_LEVEL_PREFIX + 7; + + assertThat(map.size(), is(7)); + assertThat(map.keySet(), hasItems(level1, level2, level3, level4, level5, level6, level7)); + + List level1Config = map.get(level1); + assertThat(level1Config.size(), is(1)); + assertThat(level1Config.get(0).getLocations()[0], is("1-A.xml")); + + List level2Config = map.get(level2); + assertThat(level2Config.size(), is(1)); + assertThat(level2Config.get(0).getLocations()[0], is("1-B.xml")); + + List level3Config = map.get(level3); + assertThat(level3Config.size(), is(1)); + assertThat(level3Config.get(0).getLocations()[0], is("2-A.xml")); + + List level7Config = map.get(level7); + assertThat(level7Config.size(), is(1)); + assertThat(level7Config.get(0).getLocations()[0], is("3-C.xml")); + } + + @Test + public void buildContextHierarchyMapForTestClassHierarchyWithMultiLevelContextHierarchiesAndPartiallyNamedConfig() + throws Exception { + Map> map = buildContextHierarchyMap( + TestClass2WithMultiLevelContextHierarchyAndPartiallyNamedConfig.class.getDeclaredMethod("something")); + + String level1 = "parent"; + String level2 = GENERATED_CONTEXT_HIERARCHY_LEVEL_PREFIX + 2; + String level3 = GENERATED_CONTEXT_HIERARCHY_LEVEL_PREFIX + 3; + + assertThat(map.size(), is(3)); + assertThat(map.keySet(), hasItems(level1, level2, level3)); + Iterator levels = map.keySet().iterator(); + assertThat(levels.next(), is(level1)); + assertThat(levels.next(), is(level2)); + assertThat(levels.next(), is(level3)); + + List level1Config = map.get(level1); + assertThat(level1Config.size(), is(2)); + assertThat(level1Config.get(0).getLocations()[0], is("1-A.xml")); + assertThat(level1Config.get(1).getLocations()[0], is("2-A.xml")); + + List level2Config = map.get(level2); + assertThat(level2Config.size(), is(1)); + assertThat(level2Config.get(0).getLocations()[0], is("1-B.xml")); + + List level3Config = map.get(level3); + assertThat(level3Config.size(), is(1)); + assertThat(level3Config.get(0).getLocations()[0], is("2-C.xml")); + } + + private void assertContextConfigEntriesAreNotUnique(Method testMethod) { + try { + buildContextHierarchyMap(testMethod); + fail("Should throw an IllegalStateException"); + } catch (IllegalStateException e) { + String msg = String.format( + "The @ContextConfiguration elements configured via @ContextHierarchy on test method [%s] and its " + + "supermethods must define unique contexts per hierarchy level.", + testMethod.getName()); + assertEquals(msg, e.getMessage()); + } + } + + @Test + public void buildContextHierarchyMapForSingleTestClassWithMultiLevelContextHierarchyWithEmptyContextConfig() + throws Exception { + assertContextConfigEntriesAreNotUnique( + SingleTestClassWithMultiLevelContextHierarchyWithEmptyContextConfig.class.getDeclaredMethod("something")); + } + + @Test + public void buildContextHierarchyMapForSingleTestClassWithMultiLevelContextHierarchyWithDuplicatedContextConfig() + throws Exception { + assertContextConfigEntriesAreNotUnique + (SingleTestClassWithMultiLevelContextHierarchyWithDuplicatedContextConfig.class.getDeclaredMethod("something")); + } + + @Test + public void buildContextHierarchyForInterfaceHierarchy() throws Exception { + Map> map = buildContextHierarchyMap( + TestClassWithMethodAnnotatedInInterfaces.class.getDeclaredMethod("something")); + String level1 = "superInterface"; + String level2 = GENERATED_CONTEXT_HIERARCHY_LEVEL_PREFIX + 2; + String level3 = "parent"; + String level4 = GENERATED_CONTEXT_HIERARCHY_LEVEL_PREFIX + 4; + String level5 = GENERATED_CONTEXT_HIERARCHY_LEVEL_PREFIX + 5; + + assertThat(map.size(), is(5)); + assertThat(map.keySet(), hasItems(level1, level2, level3, level4, level5)); + Iterator levels = map.keySet().iterator(); + assertThat(levels.next(), is(level1)); + assertThat(levels.next(), is(level2)); + assertThat(levels.next(), is(level3)); + assertThat(levels.next(), is(level4)); + assertThat(levels.next(), is(level5)); + + List level1Config = map.get(level1); + assertThat(level1Config.size(), is(1)); + assertThat(level1Config.get(0).getLocations()[0], is("boo.xml")); + + List level2Config = map.get(level2); + assertThat(level2Config.size(), is(1)); + assertThat(level2Config.get(0).getLocations()[0], is("bak.xml")); + + List level3Config = map.get(level3); + assertThat(level3Config.size(), is(1)); + assertThat(level3Config.get(0).getLocations()[0], is("baz.xml")); + + List level4Config = map.get(level4); + assertThat(level4Config.size(), is(1)); + assertThat(level4Config.get(0).getLocations()[0], is("bar.xml")); + + List level5Config = map.get(level5); + assertThat(level5Config.size(), is(1)); + assertThat(level5Config.get(0).getLocations()[0], is("foo.xml")); + } + + + //------------------------------------------------------------- + + private static class SingleTestClassWithContextConfigurationAndContextHierarchy { + @ContextConfiguration("foo.xml") + @ContextHierarchy(@ContextConfiguration("bar.xml")) + public void something() { + // for test purposes + } + + } + + private static class SingleTestClassWithContextConfigurationAndContextHierarchyOnSingleMetaAnnotation { + @ContextLoaderUtilsContextHierarchyTests.ContextConfigurationAndContextHierarchyOnSingleMeta + public void something() { + // for test purposes + } + } + + private static class SingleTestClassWithSingleLevelContextHierarchy { + @ContextHierarchy(@ContextConfiguration("A.xml")) + public void something() { + // for test purposes + } + } + + private static class SingleTestClassWithTripleLevelContextHierarchy { + @ContextHierarchy({// + // + @ContextConfiguration("A.xml"),// + @ContextConfiguration("B.xml"),// + @ContextConfiguration("C.xml") // + }) + public void something() { + // for test purposes + } + } + + private static class TestClass1WithSingleLevelContextHierarchy { + @ContextHierarchy(@ContextConfiguration("one.xml")) + public void something() { + // for test purposes + } + } + + private static class TestClass2WithSingleLevelContextHierarchy extends TestClass1WithSingleLevelContextHierarchy { + @ContextHierarchy(@ContextConfiguration({"two-A.xml", "two-B.xml"})) + public void something() { + // for test purposes + } + } + + private static class TestClass3WithSingleLevelContextHierarchy extends TestClass2WithSingleLevelContextHierarchy { + @ContextHierarchy(@ContextConfiguration("three.xml")) + public void something() { + // for test purposes + } + } + + private static class TestClass1WithBareContextConfigurationInSuperclass { + @ContextConfiguration("one.xml") + public void something() { + // for test purposes + } + } + + private static class TestClass2WithBareContextConfigurationInSuperclass extends TestClass1WithBareContextConfigurationInSuperclass { + @ContextHierarchy(@ContextConfiguration("two.xml")) + public void something() { + // for test purposes + } + } + + private static class TestClass1WithBareContextConfigurationInSubclass { + @ContextHierarchy(@ContextConfiguration("one.xml")) + public void something() { + // for test purposes + } + } + + private static class TestClass2WithBareContextConfigurationInSubclass extends TestClass1WithBareContextConfigurationInSubclass { + @ContextConfiguration("two.xml") + public void something() { + // for test purposes + } + } + + private static class TestClass1WithBareMetaContextConfigWithOverridesInSuperclass { + @ContextLoaderUtilsContextHierarchyTests.ContextConfigWithOverrides(locations = "one.xml") + public void something() { + // for test purposes + } + } + + private static class TestClass2WithBareMetaContextConfigWithOverridesInSuperclass extends TestClass1WithBareMetaContextConfigWithOverridesInSuperclass { + @ContextHierarchy(@ContextConfiguration(locations = "two.xml")) + public void something() { + // for test purposes + } + } + + private static class TestClass1WithBareMetaContextConfigWithOverridesInSubclass { + @ContextHierarchy(@ContextConfiguration(locations = "one.xml")) + public void something() { + // for test purposes + } + } + + private static class TestClass2WithBareMetaContextConfigWithOverridesInSubclass extends TestClass1WithBareMetaContextConfigWithOverridesInSubclass { + @ContextLoaderUtilsContextHierarchyTests.ContextConfigWithOverrides(locations = "two.xml") + public void something() { + // for test purposes + } + } + + + private static class SingleTestClassWithSingleLevelContextHierarchyFromMetaAnnotation { + @ContextLoaderUtilsContextHierarchyTests.ContextHierarchyA + public void something() { + // for test purposes + } + } + + private static class TestClass1WithSingleLevelContextHierarchyFromMetaAnnotation { + @ContextLoaderUtilsContextHierarchyTests.ContextHierarchyA + public void something() { + // for test purposes + } + } + + private static class TestClass2WithSingleLevelContextHierarchyFromMetaAnnotation extends TestClass1WithSingleLevelContextHierarchyFromMetaAnnotation { + @ContextLoaderUtilsContextHierarchyTests.ContextHierarchyB + public void something() { + // for test purposes + } + } + + private static class TestClass3WithSingleLevelContextHierarchyFromMetaAnnotation extends TestClass2WithSingleLevelContextHierarchyFromMetaAnnotation { + @ContextLoaderUtilsContextHierarchyTests.ContextHierarchyC + public void something() { + // for test purposes + } + } + + + private static class TestClass1WithMultiLevelContextHierarchy { + @ContextHierarchy({// + // + @ContextConfiguration(locations = "1-A.xml", name = "alpha"),// + @ContextConfiguration(locations = "1-B.xml", name = "beta") // + }) + public void something() { + // for test purposes + } + } + + private static class TestClass2WithMultiLevelContextHierarchy extends TestClass1WithMultiLevelContextHierarchy { + @Override + @ContextHierarchy({// + // + @ContextConfiguration(locations = "2-A.xml", name = "alpha"),// + @ContextConfiguration(locations = "2-B.xml", name = "beta") // + }) + public void something() { + // for test purposes + } + } + + private static class TestClass3WithMultiLevelContextHierarchy extends TestClass2WithMultiLevelContextHierarchy { + @Override + @ContextHierarchy({// + // + @ContextConfiguration(locations = "3-A.xml", name = "alpha"),// + @ContextConfiguration(locations = "3-B.xml", name = "beta"),// + @ContextConfiguration(locations = "3-C.xml", name = "gamma") // + }) + public void something() { + // for test purposes + } + } + + + private static class TestClass1WithMultiLevelContextHierarchyAndUnnamedConfig { + @ContextHierarchy({// + // + @ContextConfiguration(locations = "1-A.xml"),// + @ContextConfiguration(locations = "1-B.xml") // + }) + public void something() { + // for test purposes + } + } + + + private static class TestClass2WithMultiLevelContextHierarchyAndUnnamedConfig extends TestClass1WithMultiLevelContextHierarchyAndUnnamedConfig { + @Override + @ContextHierarchy({// + // + @ContextConfiguration(locations = "2-A.xml"),// + @ContextConfiguration(locations = "2-B.xml") // + }) + public void something() { + // for test purposes + } + } + + + private static class TestClass3WithMultiLevelContextHierarchyAndUnnamedConfig extends TestClass2WithMultiLevelContextHierarchyAndUnnamedConfig { + @Override + @ContextHierarchy({// + // + @ContextConfiguration(locations = "3-A.xml"),// + @ContextConfiguration(locations = "3-B.xml"),// + @ContextConfiguration(locations = "3-C.xml") // + }) + public void something() { + // for test purposes + } + } + + + private static class TestClass1WithMultiLevelContextHierarchyAndPartiallyNamedConfig { + @ContextHierarchy({// + // + @ContextConfiguration(locations = "1-A.xml", name = "parent"),// + @ContextConfiguration(locations = "1-B.xml") // + }) + public void something() { + // for test purposes + } + } + + + private static class TestClass2WithMultiLevelContextHierarchyAndPartiallyNamedConfig extends TestClass1WithMultiLevelContextHierarchyAndPartiallyNamedConfig { + @Override + @ContextHierarchy({// + // + @ContextConfiguration(locations = "2-A.xml", name = "parent"),// + @ContextConfiguration(locations = "2-C.xml") // + }) + public void something() { + // for test purposes + } + } + + + private static class SingleTestClassWithMultiLevelContextHierarchyWithEmptyContextConfig { + @ContextHierarchy({ + // + @ContextConfiguration,// + @ContextConfiguration // + }) + public void something() { + // for test purposes + } + } + + private static class SingleTestClassWithMultiLevelContextHierarchyWithDuplicatedContextConfig { + @ContextHierarchy({ + // + @ContextConfiguration("foo.xml"),// + @ContextConfiguration(classes = AbstractContextConfigurationUtilsTests.BarConfig.class),// duplicate! + @ContextConfiguration("baz.xml"),// + @ContextConfiguration(classes = AbstractContextConfigurationUtilsTests.BarConfig.class),// duplicate! + @ContextConfiguration(loader = AnnotationConfigContextLoader.class) // + }) + public void something() { + // for test purposes + } + } + + private static class TestClassWithMethodAnnotatedInInterfaces extends TestClassWithMethodAnnotatedInInterfacesParent + implements FirstLevelAnnotation { + @Override + public void something() { + //for test purposes + } + } + + private interface FirstLevelAnnotation extends SecondLevelAnnotation { + @Override + @ContextHierarchy({@ContextConfiguration(value = "foo.xml")}) + void something(); + } + + private interface SecondLevelAnnotation extends ThirdLevelAnnotation { + @Override + @ContextHierarchy({@ContextConfiguration("bar.xml")}) + void something(); + } + + private interface ThirdLevelAnnotation { + @ContextHierarchy({@ContextConfiguration(value = "baz.xml", name = "parent")}) + void something(); + } + + private static class TestClassWithMethodAnnotatedInInterfacesParent implements FirstLevelOnSuperClassInterface { + @Override + @ContextHierarchy({@ContextConfiguration(value = "bak.xml")}) + public void something() { + //for test purposes + } + } + + private interface FirstLevelOnSuperClassInterface { + @ContextHierarchy({@ContextConfiguration(value = "boo.xml", name = "superInterface")}) + void something(); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/support/ContextLoaderUtilsContextHierarchyTests.java b/spring-test/src/test/java/org/springframework/test/context/support/ContextLoaderUtilsContextHierarchyTests.java index 37025fdf0f9e..e43735516db1 100644 --- a/spring-test/src/test/java/org/springframework/test/context/support/ContextLoaderUtilsContextHierarchyTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/support/ContextLoaderUtilsContextHierarchyTests.java @@ -16,8 +16,10 @@ package org.springframework.test.context.support; +import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; import java.util.Iterator; import java.util.List; import java.util.Map; @@ -370,10 +372,11 @@ public void buildContextHierarchyMapForTestClassHierarchyWithMultiLevelContextHi private static class SingleTestClassWithContextConfigurationAndContextHierarchy { } + @Target({ElementType.TYPE, ElementType.METHOD}) @ContextConfiguration("foo.xml") @ContextHierarchy(@ContextConfiguration("bar.xml")) @Retention(RetentionPolicy.RUNTIME) - private static @interface ContextConfigurationAndContextHierarchyOnSingleMeta { + @interface ContextConfigurationAndContextHierarchyOnSingleMeta { } @ContextConfigurationAndContextHierarchyOnSingleMeta @@ -550,17 +553,19 @@ public void initialize(ConfigurableApplicationContext applicationContext) { @ContextHierarchy(@ContextConfiguration("A.xml")) @Retention(RetentionPolicy.RUNTIME) - private static @interface ContextHierarchyA { + @interface ContextHierarchyA { } + @Target({ElementType.TYPE, ElementType.METHOD}) @ContextHierarchy(@ContextConfiguration({ "B-one.xml", "B-two.xml" })) @Retention(RetentionPolicy.RUNTIME) - private static @interface ContextHierarchyB { + @interface ContextHierarchyB { } + @Target({ElementType.TYPE, ElementType.METHOD}) @ContextHierarchy(@ContextConfiguration("C.xml")) @Retention(RetentionPolicy.RUNTIME) - private static @interface ContextHierarchyC { + @interface ContextHierarchyC { } @ContextHierarchyA @@ -583,9 +588,10 @@ private static class TestClass3WithSingleLevelContextHierarchyFromMetaAnnotation // ------------------------------------------------------------------------- + @Target({ElementType.TYPE, ElementType.METHOD}) @ContextConfiguration @Retention(RetentionPolicy.RUNTIME) - private static @interface ContextConfigWithOverrides { + @interface ContextConfigWithOverrides { String[] locations() default "A.xml"; } diff --git a/spring-test/src/test/java/org/springframework/test/util/MetaAnnotationUtilsForMethod.java b/spring-test/src/test/java/org/springframework/test/util/MetaAnnotationUtilsForMethod.java new file mode 100644 index 000000000000..b8d6314fb2ef --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/util/MetaAnnotationUtilsForMethod.java @@ -0,0 +1,720 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.util; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; + +import org.junit.Test; + +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; +import org.springframework.stereotype.Service; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.transaction.annotation.Transactional; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.springframework.core.annotation.AnnotationUtils.findAnnotation; +import static org.springframework.test.util.MetaAnnotationUtils.findAnnotationDescriptor; +import static org.springframework.test.util.MetaAnnotationUtils.findAnnotationDescriptorForTypes; + +/** + * Unit tests for {@link MetaAnnotationUtils} for method-level annotations handling methods. + * + * @author Sergei Ustimenko + * @since 5.0 + * @see MetaAnnotationUtilsTests + */ +public class MetaAnnotationUtilsForMethod { + + private void assertAtComponentOnComposedAnnotation(Method method, String name, + Class composedAnnotationType) { + assertAtComponentOnComposedAnnotation(method, method.getDeclaringClass(), name, composedAnnotationType); + } + + private void assertAtComponentOnComposedAnnotation(Method method, Class rootDeclaringClass, String name, + Class composedAnnotationType) { + assertAtComponentOnComposedAnnotation(method, rootDeclaringClass, composedAnnotationType, name, + composedAnnotationType); + } + + private void assertAtComponentOnComposedAnnotation(Method method, Class rootDeclaringClass, + Class declaringClass, String name, Class composedAnnotationType) { + MetaAnnotationUtils.AnnotationDescriptor descriptor = findAnnotationDescriptor(method, + Component.class); + assertNotNull("AnnotationDescriptor should not be null", descriptor); + assertEquals("rootDeclaringClass", rootDeclaringClass, descriptor.getRootDeclaringClass()); + assertEquals("declaringClass", declaringClass, descriptor.getDeclaringClass()); + assertEquals("annotationType", Component.class, descriptor.getAnnotationType()); + assertEquals("component name", name, descriptor.getAnnotation().value()); + assertNotNull("composedAnnotation should not be null", descriptor.getComposedAnnotation()); + assertEquals("composedAnnotationType", composedAnnotationType, descriptor.getComposedAnnotationType()); + } + + @Test + public void findAnnotationDescriptorWithNoAnnotationPresent() throws Exception { + assertNull(findAnnotationDescriptor( + NonAnnotatedInterface.class.getMethod("something"), Transactional.class)); + assertNull(findAnnotationDescriptor( + NonAnnotatedClass.class.getMethod("something"), Transactional.class)); + } + + @Test + public void findAnnotationDescriptorWithInheritedAnnotationOnMethod() throws Exception { + // Note: @Transactional is inherited + Method inheritedAnnotationMethod = InheritedAnnotationClass.class.getMethod("something"); + + assertEquals(InheritedAnnotationClass.class, findAnnotationDescriptor( + inheritedAnnotationMethod,Transactional.class).getRootDeclaringClass()); + assertEquals(InheritedAnnotationClass.class, findAnnotationDescriptor( + inheritedAnnotationMethod,Transactional.class).getDeclaringClass()); + assertEquals(inheritedAnnotationMethod, findAnnotationDescriptor( + inheritedAnnotationMethod,Transactional.class).getDeclaringMethod()); + + assertEquals(SubInheritedAnnotationClass.class, findAnnotationDescriptor( + SubInheritedAnnotationClass.class.getMethod("something"), Transactional.class).getRootDeclaringClass()); + assertEquals(InheritedAnnotationClass.class, findAnnotationDescriptor( + SubInheritedAnnotationClass.class.getMethod("something"), Transactional.class).getDeclaringClass()); + assertEquals(inheritedAnnotationMethod, findAnnotationDescriptor( + SubInheritedAnnotationClass.class.getMethod("something"), Transactional.class).getDeclaringMethod()); + } + + @Test + public void findAnnotationDescriptorWithInheritedAnnotationOnInterface() throws Exception { + // Note: @Transactional is inherited + Method something = InheritedAnnotationInterface.class.getMethod("something"); + + Transactional rawAnnotation = something.getAnnotation(Transactional.class); + + MetaAnnotationUtils.AnnotationOnMethodDescriptor descriptor; + + descriptor = findAnnotationDescriptor(something, Transactional.class); + assertNotNull(descriptor); + assertEquals(InheritedAnnotationInterface.class, descriptor.getRootDeclaringClass()); + assertEquals(InheritedAnnotationInterface.class, descriptor.getDeclaringClass()); + assertEquals(something, descriptor.getDeclaringMethod()); + assertEquals(rawAnnotation, descriptor.getAnnotation()); + + Method somethingInSubInherited = SubInheritedAnnotationInterface.class.getMethod("something"); + descriptor = findAnnotationDescriptor(somethingInSubInherited, Transactional.class); + assertNotNull(descriptor); + assertEquals(SubInheritedAnnotationInterface.class, descriptor.getRootDeclaringClass()); + assertEquals(InheritedAnnotationInterface.class, descriptor.getDeclaringClass()); + assertEquals(InheritedAnnotationInterface.class.getDeclaredMethod("something"), descriptor.getDeclaringMethod()); + assertEquals(rawAnnotation, descriptor.getAnnotation()); + + Method somethingInSubSubInherited = SubSubInheritedAnnotationInterface.class.getMethod("something"); + descriptor = findAnnotationDescriptor(somethingInSubSubInherited, Transactional.class); + assertNotNull(descriptor); + assertEquals(SubSubInheritedAnnotationInterface.class, descriptor.getRootDeclaringClass()); + assertEquals(InheritedAnnotationInterface.class, descriptor.getDeclaringClass()); + assertEquals(InheritedAnnotationInterface.class.getDeclaredMethod("something"), descriptor.getDeclaringMethod()); + assertEquals(rawAnnotation, descriptor.getAnnotation()); + } + + @Test + public void findAnnotationDescriptorForNonInheritedAnnotationOnClass() throws Exception { + // Note: @Order is not inherited. + MetaAnnotationUtils.AnnotationOnMethodDescriptor something = findAnnotationDescriptor( + NonInheritedAnnotationClass.class.getMethod("something"), Order.class); + assertEquals(NonInheritedAnnotationClass.class, something.getRootDeclaringClass()); + assertEquals(NonInheritedAnnotationClass.class.getMethod("something"), something.getDeclaringMethod()); + + MetaAnnotationUtils.AnnotationOnMethodDescriptor somethingOnSub = findAnnotationDescriptor( + SubNonInheritedAnnotationClass.class.getMethod("something"), Order.class); + assertEquals(SubNonInheritedAnnotationClass.class, somethingOnSub.getRootDeclaringClass()); + assertEquals(NonInheritedAnnotationClass.class.getMethod("something"), somethingOnSub.getDeclaringMethod()); + } + + @Test + public void findAnnotationDescriptorForNonInheritedAnnotationOnInterface() throws Exception { + // Note: @Order is not inherited. + Order rawAnnotation = NonInheritedAnnotationInterface.class.getMethod("something").getAnnotation(Order.class); + + MetaAnnotationUtils.AnnotationOnMethodDescriptor descriptor; + + descriptor = findAnnotationDescriptor(NonInheritedAnnotationInterface.class.getMethod("something"), Order.class); + assertNotNull(descriptor); + assertEquals(NonInheritedAnnotationInterface.class, descriptor.getRootDeclaringClass()); + assertEquals(NonInheritedAnnotationInterface.class, descriptor.getDeclaringClass()); + assertEquals(NonInheritedAnnotationInterface.class.getDeclaredMethod("something"), descriptor.getDeclaringMethod()); + assertEquals(rawAnnotation, descriptor.getAnnotation()); + + descriptor = findAnnotationDescriptor(SubNonInheritedAnnotationInterface.class.getMethod("something"), Order.class); + assertNotNull(descriptor); + assertEquals(SubNonInheritedAnnotationInterface.class, descriptor.getRootDeclaringClass()); + assertEquals(NonInheritedAnnotationInterface.class, descriptor.getDeclaringClass()); + assertEquals(NonInheritedAnnotationInterface.class.getDeclaredMethod("something"), descriptor.getDeclaringMethod()); + assertEquals(rawAnnotation, descriptor.getAnnotation()); + } + + @Test + public void findAnnotationDescriptorWithMetaComponentAnnotation() throws Exception { + assertAtComponentOnComposedAnnotation(HasMetaComponentAnnotation.class.getDeclaredMethod("something"), + "meta1", MetaAnnotationUtilsTests.Meta1.class); + } + + @Test + public void findAnnotationDescriptorWithLocalAndMetaComponentAnnotation() throws Exception { + Class annotationType = Transactional.class; + Method something = HasLocalAndMetaComponentAnnotation.class.getDeclaredMethod("something"); + MetaAnnotationUtils.AnnotationOnMethodDescriptor descriptor = findAnnotationDescriptor( + something, annotationType); + + assertEquals(HasLocalAndMetaComponentAnnotation.class, descriptor.getRootDeclaringClass()); + assertEquals(annotationType, descriptor.getAnnotationType()); + assertEquals(something, descriptor.getDeclaringMethod()); + assertNull(descriptor.getComposedAnnotation()); + assertNull(descriptor.getComposedAnnotationType()); + } + + @Test + public void findAnnotationDescriptorForInterfaceWithMetaAnnotation() throws Exception { + assertAtComponentOnComposedAnnotation(InterfaceWithMetaAnnotation.class.getDeclaredMethod("something"), + "meta1", MetaAnnotationUtilsTests.Meta1.class); + } + + @Test + public void findAnnotationDescriptorForClassWithMetaAnnotatedInterface() throws Exception { + Method something = ClassWithMetaAnnotatedInterface.class.getMethod("something"); + Component rawAnnotation = findAnnotation(something, Component.class); + + MetaAnnotationUtils.AnnotationOnMethodDescriptor descriptor; + + descriptor = findAnnotationDescriptor(something, Component.class); + assertNotNull(descriptor); + assertEquals(ClassWithMetaAnnotatedInterface.class, descriptor.getRootDeclaringClass()); + assertEquals(MetaAnnotationUtilsTests.Meta1.class, descriptor.getDeclaringClass()); + assertEquals(InterfaceWithMetaAnnotation.class.getDeclaredMethod("something"), descriptor.getDeclaringMethod()); + assertEquals(rawAnnotation, descriptor.getAnnotation()); + assertEquals(MetaAnnotationUtilsTests.Meta1.class, descriptor.getComposedAnnotation().annotationType()); + } + + @Test + public void findAnnotationDescriptorForClassWithLocalMetaAnnotationAndAnnotatedSuperclass() throws Exception { + Method something = MetaAnnotatedAndSuperAnnotatedContextConfigClass.class.getDeclaredMethod("something"); + MetaAnnotationUtils.AnnotationOnMethodDescriptor descriptor = findAnnotationDescriptor( + something, ContextConfiguration.class); + + assertNotNull("AnnotationDescriptor should not be null", descriptor); + assertEquals("rootDeclaringClass", MetaAnnotatedAndSuperAnnotatedContextConfigClass.class, descriptor.getRootDeclaringClass()); + assertEquals("declaringClass", MetaAnnotationUtilsTests.MetaConfig.class, descriptor.getDeclaringClass()); + assertEquals("annotationType", ContextConfiguration.class, descriptor.getAnnotationType()); + assertEquals("method", something, descriptor.getDeclaringMethod()); + assertNotNull("composedAnnotation should not be null", descriptor.getComposedAnnotation()); + assertEquals("composedAnnotationType", MetaAnnotationUtilsTests.MetaConfig.class, descriptor.getComposedAnnotationType()); + + assertArrayEquals("configured classes", new Class[] { String.class }, + descriptor.getAnnotationAttributes().getClassArray("classes")); + } + + @Test + public void findAnnotationDescriptorForClassWithLocalMetaAnnotationAndMetaAnnotatedInterface() throws Exception { + assertAtComponentOnComposedAnnotation( + ClassWithLocalMetaAnnotationAndMetaAnnotatedInterface.class.getDeclaredMethod("something"), + "meta2", MetaAnnotationUtilsTests.Meta2.class); + } + + @Test + public void findAnnotationDescriptorForSubClassWithLocalMetaAnnotationAndMetaAnnotatedInterface() throws Exception { + assertAtComponentOnComposedAnnotation( + SubClassWithLocalMetaAnnotationAndMetaAnnotatedInterface.class.getDeclaredMethod("something"), + SubClassWithLocalMetaAnnotationAndMetaAnnotatedInterface.class, + "meta2", MetaAnnotationUtilsTests.Meta2.class); + } + + + @Test + public void findAnnotationDescriptorOnMetaMetaAnnotatedClass() throws Exception { + Method something = MetaMetaAnnotatedClass.class.getDeclaredMethod("something"); + assertAtComponentOnComposedAnnotation(something, MetaMetaAnnotatedClass.class, + MetaAnnotationUtilsTests.Meta2.class, "meta2", MetaAnnotationUtilsTests.MetaMeta.class); + } + + @Test + public void findAnnotationDescriptorOnMetaMetaMetaAnnotatedClass() throws Exception { + Method something = MetaMetaMetaAnnotatedClass.class.getDeclaredMethod("something"); + assertAtComponentOnComposedAnnotation(something, MetaMetaMetaAnnotatedClass.class, + MetaAnnotationUtilsTests.Meta2.class, "meta2", MetaAnnotationUtilsTests.MetaMetaMeta.class); + } + + + @Test + public void findAnnotationDescriptorOnAnnotatedClassWithMissingTargetMetaAnnotation() throws Exception { + // InheritedAnnotationClass is NOT annotated or meta-annotated with @Component + MetaAnnotationUtils.AnnotationDescriptor + descriptor = findAnnotationDescriptor( + InheritedAnnotationClass.class.getDeclaredMethod("something"), Component.class); + assertNull("Should not find @Component on InheritedAnnotationClass", descriptor); + } + + @Test + public void findAnnotationDescriptorOnMetaCycleAnnotatedClassWithMissingTargetMetaAnnotation() throws Exception { + MetaAnnotationUtils.AnnotationDescriptor + descriptor = findAnnotationDescriptor( + MetaCycleAnnotatedClass.class.getDeclaredMethod("something"), Component.class); + assertNull("Should not find @Component on MetaCycleAnnotatedClass", descriptor); + } + + // ------------------------------------------------------------------------- + + @Test + @SuppressWarnings("unchecked") + public void findAnnotationDescriptorForTypesWithNoAnnotationPresent() throws Exception { + assertNull(findAnnotationDescriptorForTypes(NonAnnotatedInterface.class.getDeclaredMethod("something"), + Transactional.class, Component.class)); + assertNull(findAnnotationDescriptorForTypes(NonAnnotatedClass.class.getDeclaredMethod("something"), + Transactional.class, Order.class)); + } + + @Test + @SuppressWarnings("unchecked") + public void findAnnotationDescriptorForTypesWithInheritedAnnotationOnClass() throws Exception { + // Note: @Transactional is inherited + MetaAnnotationUtils.UntypedAnnotationOnMethodDescriptor something = findAnnotationDescriptorForTypes( + InheritedAnnotationClass.class.getDeclaredMethod("something"), Transactional.class); + assertEquals(InheritedAnnotationClass.class, something.getRootDeclaringClass()); + assertEquals(InheritedAnnotationClass.class, something.getDeclaringClass()); + assertEquals(InheritedAnnotationClass.class.getDeclaredMethod("something"), something.getDeclaringMethod()); + + MetaAnnotationUtils.UntypedAnnotationOnMethodDescriptor somethingOnSub = findAnnotationDescriptorForTypes( + SubInheritedAnnotationClass.class.getDeclaredMethod("something"), Transactional.class); + assertEquals(InheritedAnnotationClass.class, somethingOnSub.getDeclaringClass()); + assertEquals(SubInheritedAnnotationClass.class, somethingOnSub.getRootDeclaringClass()); + assertEquals(InheritedAnnotationClass.class.getDeclaredMethod("something"), somethingOnSub.getDeclaringMethod()); + } + + @Test + @SuppressWarnings("unchecked") + public void findAnnotationDescriptorForTypesWithInheritedAnnotationOnInterface() throws Exception { + // Note: @Transactional is inherited + Transactional rawAnnotation = InheritedAnnotationInterface.class.getDeclaredMethod("something") + .getAnnotation(Transactional.class); + + MetaAnnotationUtils.UntypedAnnotationOnMethodDescriptor descriptor; + + descriptor = findAnnotationDescriptorForTypes(InheritedAnnotationInterface.class.getDeclaredMethod("something"), + Transactional.class); + assertNotNull(descriptor); + assertEquals(InheritedAnnotationInterface.class, descriptor.getRootDeclaringClass()); + assertEquals(InheritedAnnotationInterface.class, descriptor.getDeclaringClass()); + assertEquals(InheritedAnnotationInterface.class.getDeclaredMethod("something"), descriptor.getDeclaringMethod()); + assertEquals(rawAnnotation, descriptor.getAnnotation()); + + descriptor = findAnnotationDescriptorForTypes(SubInheritedAnnotationInterface.class.getDeclaredMethod("something"), + Transactional.class); + assertNotNull(descriptor); + assertEquals(SubInheritedAnnotationInterface.class, descriptor.getRootDeclaringClass()); + assertEquals(InheritedAnnotationInterface.class, descriptor.getDeclaringClass()); + assertEquals(InheritedAnnotationInterface.class.getDeclaredMethod("something"), descriptor.getDeclaringMethod()); + assertEquals(rawAnnotation, descriptor.getAnnotation()); + + descriptor = findAnnotationDescriptorForTypes(SubSubInheritedAnnotationInterface.class.getDeclaredMethod("something"), + Transactional.class); + assertNotNull(descriptor); + assertEquals(SubSubInheritedAnnotationInterface.class, descriptor.getRootDeclaringClass()); + assertEquals(InheritedAnnotationInterface.class, descriptor.getDeclaringClass()); + assertEquals(InheritedAnnotationInterface.class.getDeclaredMethod("something"), descriptor.getDeclaringMethod()); + assertEquals(rawAnnotation, descriptor.getAnnotation()); + } + + @Test + @SuppressWarnings("unchecked") + public void findAnnotationDescriptorForTypesForNonInheritedAnnotationOnClass() throws Exception { + // Note: @Order is not inherited. + MetaAnnotationUtils.UntypedAnnotationOnMethodDescriptor something = findAnnotationDescriptorForTypes( + NonInheritedAnnotationClass.class.getDeclaredMethod("something"), Order.class); + assertEquals(NonInheritedAnnotationClass.class, something.getRootDeclaringClass()); + assertEquals(NonInheritedAnnotationClass.class, something.getDeclaringClass()); + assertEquals(NonInheritedAnnotationClass.class.getDeclaredMethod("something"), something.getDeclaringMethod()); + + + MetaAnnotationUtils.UntypedAnnotationOnMethodDescriptor somethingOnSub = findAnnotationDescriptorForTypes( + SubNonInheritedAnnotationClass.class.getDeclaredMethod("something"), Order.class); + assertEquals(SubNonInheritedAnnotationClass.class, somethingOnSub.getRootDeclaringClass()); + assertEquals(NonInheritedAnnotationClass.class, somethingOnSub.getDeclaringClass()); + assertEquals(NonInheritedAnnotationClass.class.getDeclaredMethod("something"), somethingOnSub.getDeclaringMethod()); + } + + @Test + @SuppressWarnings("unchecked") + public void findAnnotationDescriptorForTypesForNonInheritedAnnotationOnInterface() throws Exception { + // Note: @Order is not inherited. + Order rawAnnotation = NonInheritedAnnotationInterface.class.getDeclaredMethod("something").getAnnotation(Order.class); + + MetaAnnotationUtils.UntypedAnnotationOnMethodDescriptor descriptor; + + descriptor = findAnnotationDescriptorForTypes( + NonInheritedAnnotationInterface.class.getDeclaredMethod("something"), Order.class); + assertNotNull(descriptor); + assertEquals(NonInheritedAnnotationInterface.class, descriptor.getRootDeclaringClass()); + assertEquals(NonInheritedAnnotationInterface.class, descriptor.getDeclaringClass()); + assertEquals(NonInheritedAnnotationInterface.class.getDeclaredMethod("something"), descriptor.getDeclaringMethod()); + assertEquals(rawAnnotation, descriptor.getAnnotation()); + + descriptor = findAnnotationDescriptorForTypes( + SubNonInheritedAnnotationInterface.class.getDeclaredMethod("something"), Order.class); + assertNotNull(descriptor); + assertEquals(SubNonInheritedAnnotationInterface.class, descriptor.getRootDeclaringClass()); + assertEquals(NonInheritedAnnotationInterface.class, descriptor.getDeclaringClass()); + assertEquals(NonInheritedAnnotationInterface.class.getDeclaredMethod("something"), descriptor.getDeclaringMethod()); + assertEquals(rawAnnotation, descriptor.getAnnotation()); + } + + @Test + @SuppressWarnings("unchecked") + public void findAnnotationDescriptorForTypesWithLocalAndMetaComponentAnnotation() throws Exception { + Class annotationType = Transactional.class; + MetaAnnotationUtils.UntypedAnnotationOnMethodDescriptor descriptor = findAnnotationDescriptorForTypes( + HasLocalAndMetaComponentAnnotation.class.getDeclaredMethod("something"), Component.class, + annotationType, Order.class); + + assertEquals(HasLocalAndMetaComponentAnnotation.class, descriptor.getRootDeclaringClass()); + assertEquals(HasLocalAndMetaComponentAnnotation.class.getDeclaredMethod("something"), descriptor.getDeclaringMethod()); + assertEquals(annotationType, descriptor.getAnnotationType()); + assertNull(descriptor.getComposedAnnotation()); + assertNull(descriptor.getComposedAnnotationType()); + } + + private void assertAtComponentOnComposedAnnotationForMultipleCandidateTypes(Method method, String name, + Class composedAnnotationType) { + assertAtComponentOnComposedAnnotationForMultipleCandidateTypes(method, method.getDeclaringClass(), name, + composedAnnotationType); + } + + private void assertAtComponentOnComposedAnnotationForMultipleCandidateTypes(Method method, + Class rootDeclaringClass, String name, Class composedAnnotationType) { + assertAtComponentOnComposedAnnotationForMultipleCandidateTypes(method, method, rootDeclaringClass, + composedAnnotationType, name, composedAnnotationType); + } + + @SuppressWarnings("unchecked") + private void assertAtComponentOnComposedAnnotationForMultipleCandidateTypes(Method startMethod, + Method expectedMethod, Class rootDeclaringClass, Class declaringClass, String name, + Class composedAnnotationType) { + Class annotationType = Component.class; + MetaAnnotationUtils.UntypedAnnotationOnMethodDescriptor descriptor = findAnnotationDescriptorForTypes( + startMethod, Service.class, annotationType, Order.class, Transactional.class); + assertNotNull("UntypedAnnotationMethodDescriptor should not be null", descriptor); + assertEquals("rootDeclaringClass", rootDeclaringClass, descriptor.getRootDeclaringClass()); + assertEquals("declaringClass", declaringClass, descriptor.getDeclaringClass()); + assertEquals("declaringMethod", expectedMethod, descriptor.getDeclaringMethod()); + assertEquals("annotationType", annotationType, descriptor.getAnnotationType()); + assertEquals("component name", name, ((Component) descriptor.getAnnotation()).value()); + assertNotNull("composedAnnotation should not be null", descriptor.getComposedAnnotation()); + assertEquals("composedAnnotationType", composedAnnotationType, descriptor.getComposedAnnotationType()); + } + + @Test + public void findAnnotationDescriptorForTypesWithMetaComponentAnnotation() throws Exception { + assertAtComponentOnComposedAnnotationForMultipleCandidateTypes( + HasMetaComponentAnnotation.class.getDeclaredMethod("something"), "meta1", + MetaAnnotationUtilsTests.Meta1.class); + } + + @Test + @SuppressWarnings("unchecked") + public void findAnnotationDescriptorForTypesWithMetaAnnotationWithDefaultAttributes() throws Exception { + Class startClass = MetaConfigWithDefaultAttributesTestCase.class; + Class annotationType = ContextConfiguration.class; + + MetaAnnotationUtils.UntypedAnnotationOnMethodDescriptor descriptor = findAnnotationDescriptorForTypes( + startClass.getDeclaredMethod("something"), Service.class, + ContextConfiguration.class, Order.class, Transactional.class); + + assertNotNull(descriptor); + assertEquals(startClass, descriptor.getRootDeclaringClass()); + assertEquals(MetaAnnotationUtilsTests.MetaConfig.class, descriptor.getDeclaringClass()); + assertEquals(startClass.getDeclaredMethod("something"), descriptor.getDeclaringMethod()); + assertEquals(annotationType, descriptor.getAnnotationType()); + assertArrayEquals(new Class[] {}, ((ContextConfiguration) descriptor.getAnnotation()).value()); + assertArrayEquals(new Class[] { + MetaAnnotationUtilsTests.MetaConfig.DevConfig.class, + MetaAnnotationUtilsTests.MetaConfig.ProductionConfig.class + }, descriptor.getAnnotationAttributes().getClassArray("classes")); + assertNotNull(descriptor.getComposedAnnotation()); + assertEquals(MetaAnnotationUtilsTests.MetaConfig.class, descriptor.getComposedAnnotationType()); + } + + @Test + @SuppressWarnings("unchecked") + public void findAnnotationDescriptorForTypesWithMetaAnnotationWithOverriddenAttributes() throws Exception { + Class startClass = MetaConfigWithOverriddenAttributesTestCase.class; + Class annotationType = ContextConfiguration.class; + + MetaAnnotationUtils.UntypedAnnotationOnMethodDescriptor descriptor = findAnnotationDescriptorForTypes( + startClass.getDeclaredMethod("something"), Service.class, + ContextConfiguration.class, Order.class, Transactional.class); + + assertNotNull(descriptor); + assertEquals(startClass, descriptor.getRootDeclaringClass()); + assertEquals(MetaAnnotationUtilsTests.MetaConfig.class, descriptor.getDeclaringClass()); + assertEquals(startClass.getDeclaredMethod("something"), descriptor.getDeclaringMethod()); + assertEquals(annotationType, descriptor.getAnnotationType()); + assertArrayEquals(new Class[] {}, ((ContextConfiguration) descriptor.getAnnotation()).value()); + assertArrayEquals(new Class[] { MetaAnnotationUtilsTests.class }, + descriptor.getAnnotationAttributes().getClassArray("classes")); + assertNotNull(descriptor.getComposedAnnotation()); + assertEquals(MetaAnnotationUtilsTests.MetaConfig.class, descriptor.getComposedAnnotationType()); + } + + @Test + public void findAnnotationDescriptorForTypesForInterfaceWithMetaAnnotation() throws Exception { + assertAtComponentOnComposedAnnotationForMultipleCandidateTypes( + InterfaceWithMetaAnnotation.class.getDeclaredMethod("something"), + "meta1", MetaAnnotationUtilsTests.Meta1.class); + } + + @Test + @SuppressWarnings("unchecked") + public void findAnnotationDescriptorForTypesForClassWithMetaAnnotatedInterface() throws Exception { + Component rawAnnotation = findAnnotation(ClassWithMetaAnnotatedInterface.class.getDeclaredMethod("something"), + Component.class); + + MetaAnnotationUtils.UntypedAnnotationOnMethodDescriptor descriptor; + + descriptor = findAnnotationDescriptorForTypes( + ClassWithMetaAnnotatedInterface.class.getDeclaredMethod("something"), + Service.class, Component.class, Order.class, Transactional.class); + assertNotNull(descriptor); + assertEquals(ClassWithMetaAnnotatedInterface.class, descriptor.getRootDeclaringClass()); + assertEquals(MetaAnnotationUtilsTests.Meta1.class, descriptor.getDeclaringClass()); + assertEquals(InterfaceWithMetaAnnotation.class.getDeclaredMethod("something"), descriptor.getDeclaringMethod()); + assertEquals(rawAnnotation, descriptor.getAnnotation()); + assertEquals(MetaAnnotationUtilsTests.Meta1.class, descriptor.getComposedAnnotation().annotationType()); + } + + @Test + public void findAnnotationDescriptorForTypesForClassWithLocalMetaAnnotationAndMetaAnnotatedInterface() throws Exception { + assertAtComponentOnComposedAnnotationForMultipleCandidateTypes( + ClassWithLocalMetaAnnotationAndMetaAnnotatedInterface.class.getDeclaredMethod("something"), + "meta2", MetaAnnotationUtilsTests.Meta2.class); + } + + @Test + public void findAnnotationDescriptorForTypesForSubClassWithLocalMetaAnnotationAndMetaAnnotatedInterface() throws Exception { + assertAtComponentOnComposedAnnotationForMultipleCandidateTypes( + SubClassWithLocalMetaAnnotationAndMetaAnnotatedInterface.class.getDeclaredMethod("something"), + ClassWithLocalMetaAnnotationAndMetaAnnotatedInterface.class.getDeclaredMethod("something"), + SubClassWithLocalMetaAnnotationAndMetaAnnotatedInterface.class, + MetaAnnotationUtilsTests.Meta2.class, "meta2", MetaAnnotationUtilsTests.Meta2.class); + } + + @Test + public void findAnnotationDescriptorForTypesOnMetaMetaAnnotatedClass() throws Exception { + Class startClass = MetaMetaAnnotatedClass.class; + Method something = startClass.getDeclaredMethod("something"); + assertAtComponentOnComposedAnnotationForMultipleCandidateTypes( + something, something, + startClass, MetaAnnotationUtilsTests.Meta2.class, "meta2", + MetaAnnotationUtilsTests.MetaMeta.class); + } + + @Test + public void findAnnotationDescriptorForTypesOnMetaMetaMetaAnnotatedClass() throws Exception { + Class startClass = MetaMetaMetaAnnotatedClass.class; + Method something = startClass.getDeclaredMethod("something"); + assertAtComponentOnComposedAnnotationForMultipleCandidateTypes( + something, something, + startClass, MetaAnnotationUtilsTests.Meta2.class, "meta2", + MetaAnnotationUtilsTests.MetaMetaMeta.class); + } + + @Test + @SuppressWarnings("unchecked") + public void findAnnotationDescriptorForTypesOnAnnotatedClassWithMissingTargetMetaAnnotation() throws Exception { + // InheritedAnnotationClass is NOT annotated or meta-annotated with @Component, + // @Service, or @Order, but it is annotated with @Transactional. + MetaAnnotationUtils.UntypedAnnotationOnMethodDescriptor + descriptor = findAnnotationDescriptorForTypes(InheritedAnnotationClass.class.getDeclaredMethod("something"), + Service.class, Component.class, Order.class); + assertNull("Should not find @Component on InheritedAnnotationClass", descriptor); + } + + @Test + @SuppressWarnings("unchecked") + public void findAnnotationDescriptorForTypesOnMetaCycleAnnotatedClassWithMissingTargetMetaAnnotation() throws Exception { + MetaAnnotationUtils.UntypedAnnotationDescriptor + descriptor = findAnnotationDescriptorForTypes(MetaCycleAnnotatedClass.class.getDeclaredMethod("something"), + Service.class, Component.class, Order.class); + assertNull("Should not find @Component on MetaCycleAnnotatedClass", descriptor); + } + + // ------------------------------------------------------------------------- + + static class HasMetaComponentAnnotation { + @MetaAnnotationUtilsTests.Meta1 + public void something(){ + // for test purposes + } + } + + static class HasLocalAndMetaComponentAnnotation { + @MetaAnnotationUtilsTests.Meta1 + @Transactional + @MetaAnnotationUtilsTests.Meta2 + public void something(){ + // for test purposes + } + } + + interface InterfaceWithMetaAnnotation { + @MetaAnnotationUtilsTests.Meta1 + void something(); + } + + static class ClassWithMetaAnnotatedInterface implements InterfaceWithMetaAnnotation { + @Override + public void something(){ + // for test purposes + } + } + + static class ClassWithLocalMetaAnnotationAndMetaAnnotatedInterface implements InterfaceWithMetaAnnotation { + @Override + @MetaAnnotationUtilsTests.Meta2 + public void something(){ + // for test purposes + } + } + + static class SubClassWithLocalMetaAnnotationAndMetaAnnotatedInterface extends ClassWithLocalMetaAnnotationAndMetaAnnotatedInterface { + @Override + public void something(){ + // for test purposes + } + } + + static class MetaMetaAnnotatedClass { + @MetaAnnotationUtilsTests.MetaMeta + public void something(){ + // for test purposes + } + } + + static class MetaMetaMetaAnnotatedClass { + @MetaAnnotationUtilsTests.MetaMetaMeta + public void something(){ + // for test purposes + } + } + + static class MetaCycleAnnotatedClass { + @MetaAnnotationUtilsTests.MetaCycle3 + public void something(){ + // for test purposes + } + } + + public class MetaConfigWithDefaultAttributesTestCase { + @MetaAnnotationUtilsTests.MetaConfig + public void something(){ + // for test purposes + } + } + + public class MetaConfigWithOverriddenAttributesTestCase { + @MetaAnnotationUtilsTests.MetaConfig(classes = MetaAnnotationUtilsTests.class) + public void something(){ + // for test purposes + } + } + + // ------------------------------------------------------------------------- + + interface InheritedAnnotationInterface { + @Transactional + void something(); + } + + interface SubInheritedAnnotationInterface extends InheritedAnnotationInterface { + @Override + void something(); + } + + interface SubSubInheritedAnnotationInterface extends SubInheritedAnnotationInterface { + @Override + void something(); + } + + interface NonInheritedAnnotationInterface { + @Order + void something(); + } + + interface SubNonInheritedAnnotationInterface extends NonInheritedAnnotationInterface { + @Override + void something(); + } + + static class NonAnnotatedClass { + public void something(){ + // for test purposes + } + } + + interface NonAnnotatedInterface { + void something(); + } + + static class InheritedAnnotationClass { + @Transactional + public void something(){ + // for test purposes + } + } + + static class SubInheritedAnnotationClass extends InheritedAnnotationClass { + @Override + public void something(){ + // for test purposes + } + } + + static class NonInheritedAnnotationClass { + @Order + public void something(){ + // for test purposes + } + } + + static class SubNonInheritedAnnotationClass extends NonInheritedAnnotationClass { + @Override + public void something(){ + // for test purposes + } + } + + static class AnnotatedContextConfigClass { + @ContextConfiguration(classes = Number.class) + public void something(){ + // for test purposes + } + } + + static class MetaAnnotatedAndSuperAnnotatedContextConfigClass extends AnnotatedContextConfigClass { + @MetaAnnotationUtilsTests.MetaConfig(classes = String.class) + @Override + public void something(){ + // for test purposes + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/util/MetaAnnotationUtilsTests.java b/spring-test/src/test/java/org/springframework/test/util/MetaAnnotationUtilsTests.java index 3416f215f0aa..a4411df95584 100644 --- a/spring-test/src/test/java/org/springframework/test/util/MetaAnnotationUtilsTests.java +++ b/spring-test/src/test/java/org/springframework/test/util/MetaAnnotationUtilsTests.java @@ -491,7 +491,7 @@ public void findAnnotationDescriptorForTypesOnMetaCycleAnnotatedClassWithMissing @Component(value = "meta1") @Order @Retention(RetentionPolicy.RUNTIME) - @Target(ElementType.TYPE) + @Target({ElementType.TYPE, ElementType.METHOD}) @Documented static @interface Meta1 { } @@ -499,21 +499,21 @@ public void findAnnotationDescriptorForTypesOnMetaCycleAnnotatedClassWithMissing @Component(value = "meta2") @Transactional @Retention(RetentionPolicy.RUNTIME) - @Target(ElementType.TYPE) + @Target({ElementType.TYPE, ElementType.METHOD}) @Documented static @interface Meta2 { } @Meta2 @Retention(RetentionPolicy.RUNTIME) - @Target(ElementType.TYPE) + @Target({ElementType.TYPE, ElementType.METHOD}) @Documented @interface MetaMeta { } @MetaMeta @Retention(RetentionPolicy.RUNTIME) - @Target(ElementType.TYPE) + @Target({ElementType.TYPE, ElementType.METHOD}) @Documented @interface MetaMetaMeta { } @@ -534,14 +534,14 @@ public void findAnnotationDescriptorForTypesOnMetaCycleAnnotatedClassWithMissing @MetaCycle2 @Retention(RetentionPolicy.RUNTIME) - @Target(ElementType.TYPE) + @Target({ElementType.TYPE, ElementType.METHOD}) @Documented @interface MetaCycle3 { } @ContextConfiguration @Retention(RetentionPolicy.RUNTIME) - @Target(ElementType.TYPE) + @Target({ElementType.TYPE, ElementType.METHOD}) @Documented static @interface MetaConfig {