org.springframework.test.context.support.ContextLoaderUtils Maven / Gradle / Ivy
Show all versions of spring-test Show documentation
/*
* Copyright 2002-2022 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
*
* https://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.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.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.context.TestContextAnnotationUtils.AnnotationDescriptor;
import org.springframework.test.context.TestContextAnnotationUtils.UntypedAnnotationDescriptor;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import static org.springframework.core.annotation.AnnotationUtils.getAnnotation;
import static org.springframework.core.annotation.AnnotationUtils.isAnnotationDeclaredLocally;
import static org.springframework.test.context.TestContextAnnotationUtils.findAnnotationDescriptor;
import static org.springframework.test.context.TestContextAnnotationUtils.findAnnotationDescriptorForTypes;
/**
* Utility methods for resolving {@link ContextConfigurationAttributes} from the
* {@link ContextConfiguration @ContextConfiguration} and
* {@link ContextHierarchy @ContextHierarchy} annotations for use with
* {@link SmartContextLoader SmartContextLoaders}.
*
* @author Sam Brannen
* @since 3.1
* @see SmartContextLoader
* @see ContextConfigurationAttributes
* @see ContextConfiguration
* @see ContextHierarchy
*/
abstract class ContextLoaderUtils {
static final String GENERATED_CONTEXT_HIERARCHY_LEVEL_PREFIX = "ContextHierarchyLevel#";
private static final Log logger = LogFactory.getLog(ContextLoaderUtils.class);
/**
* Resolve the list of lists of {@linkplain ContextConfigurationAttributes context
* configuration attributes} for the supplied {@linkplain Class test class} and its
* superclasses and enclosing classes, taking into account context hierarchies
* declared via {@link ContextHierarchy @ContextHierarchy} and
* {@link ContextConfiguration @ContextConfiguration}.
* The outer list represents a top-down ordering of context configuration
* attributes, where each element in the list represents the context configuration
* declared on a given test class in the class hierarchy or enclosing class
* hierarchy. Each nested list contains the context configuration attributes
* declared either via a single instance of {@code @ContextConfiguration} on
* the particular class or via multiple instances of {@code @ContextConfiguration}
* declared within a single {@code @ContextHierarchy} instance on the particular
* class. Furthermore, each nested list maintains the order in which
* {@code @ContextConfiguration} instances are declared.
*
Note that the {@link ContextConfiguration#inheritLocations inheritLocations} and
* {@link ContextConfiguration#inheritInitializers() inheritInitializers} flags of
* {@link ContextConfiguration @ContextConfiguration} will not
* be taken into consideration. If these flags need to be honored, that must be
* handled manually when traversing the nested lists returned by this method.
* @param testClass the class for which to resolve the context hierarchy attributes
* (must not be {@code null})
* @return the list of lists of configuration attributes for the specified class;
* never {@code null}
* @throws IllegalArgumentException if the supplied class is {@code null}; or if
* neither {@code @ContextConfiguration} nor {@code @ContextHierarchy} is
* present on the supplied class
* @throws IllegalStateException if a test class or composed annotation
* in the class hierarchy declares both {@code @ContextConfiguration} and
* {@code @ContextHierarchy} as top-level annotations.
* @since 3.2.2
* @see #buildContextHierarchyMap(Class)
* @see #resolveContextConfigurationAttributes(Class)
*/
@SuppressWarnings("unchecked")
static List> resolveContextHierarchyAttributes(Class testClass) {
Assert.notNull(testClass, "Class must not be null");
Class contextConfigType = ContextConfiguration.class;
Class contextHierarchyType = ContextHierarchy.class;
List> hierarchyAttributes = new ArrayList<>();
UntypedAnnotationDescriptor desc =
findAnnotationDescriptorForTypes(testClass, contextConfigType, contextHierarchyType);
Assert.notNull(desc, () -> String.format(
"Could not find an 'annotation declaring class' for annotation type [%s] or [%s] and test class [%s]",
contextConfigType.getName(), contextHierarchyType.getName(), testClass.getName()));
while (desc != null) {
Class rootDeclaringClass = desc.getRootDeclaringClass();
Class declaringClass = desc.getDeclaringClass();
boolean contextConfigDeclaredLocally = isAnnotationDeclaredLocally(contextConfigType, declaringClass);
boolean contextHierarchyDeclaredLocally = isAnnotationDeclaredLocally(contextHierarchyType, declaringClass);
if (contextConfigDeclaredLocally && contextHierarchyDeclaredLocally) {
String msg = String.format("Class [%s] has been configured with both @ContextConfiguration " +
"and @ContextHierarchy. Only one of these annotations may be declared on a test class " +
"or composed annotation.", declaringClass.getName());
logger.error(msg);
throw new IllegalStateException(msg);
}
List configAttributesList = new ArrayList<>();
if (contextConfigDeclaredLocally) {
ContextConfiguration contextConfiguration = (ContextConfiguration) desc.getAnnotation();
convertContextConfigToConfigAttributesAndAddToList(
contextConfiguration, rootDeclaringClass, configAttributesList);
}
else if (contextHierarchyDeclaredLocally) {
ContextHierarchy contextHierarchy = getAnnotation(declaringClass, contextHierarchyType);
if (contextHierarchy != null) {
for (ContextConfiguration contextConfiguration : contextHierarchy.value()) {
convertContextConfigToConfigAttributesAndAddToList(
contextConfiguration, rootDeclaringClass, configAttributesList);
}
}
}
else {
// This should theoretically never happen...
String msg = String.format("Test class [%s] has been configured with neither @ContextConfiguration " +
"nor @ContextHierarchy as a class-level annotation.", rootDeclaringClass.getName());
logger.error(msg);
throw new IllegalStateException(msg);
}
hierarchyAttributes.add(0, configAttributesList);
desc = desc.next();
}
return hierarchyAttributes;
}
/**
* Build a context hierarchy map for the supplied {@linkplain Class
* test class} and its superclasses and enclosing classes, taking into account
* context hierarchies declared via {@link ContextHierarchy @ContextHierarchy}
* and {@link ContextConfiguration @ContextConfiguration}.
* Each value in the map represents the consolidated list of {@linkplain
* ContextConfigurationAttributes context configuration attributes} for a
* given level in the context hierarchy (potentially across the test class
* hierarchy and enclosing class hierarchy), keyed by the
* {@link ContextConfiguration#name() name} of the context hierarchy level.
*
If a given level in the context hierarchy does not have an explicit
* name (i.e., configured via {@link ContextConfiguration#name}), a name will
* be generated for that hierarchy level by appending the numerical level to
* the {@link #GENERATED_CONTEXT_HIERARCHY_LEVEL_PREFIX}.
* @param testClass the class for which to resolve the context hierarchy map
* (must not be {@code null})
* @return a map of context configuration attributes for the context hierarchy,
* keyed by context hierarchy level name; never {@code null}
* @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.
* @since 3.2.2
* @see #resolveContextHierarchyAttributes(Class)
*/
static Map> buildContextHierarchyMap(Class testClass) {
Map> map = new LinkedHashMap<>();
int hierarchyLevel = 1;
for (List configAttributesList : resolveContextHierarchyAttributes(testClass)) {
for (ContextConfigurationAttributes configAttributes : configAttributesList) {
String name = configAttributes.getName();
// Assign a generated name?
if (!StringUtils.hasText(name)) {
name = GENERATED_CONTEXT_HIERARCHY_LEVEL_PREFIX + hierarchyLevel;
}
// Encountered a new context hierarchy level?
if (!map.containsKey(name)) {
hierarchyLevel++;
map.put(name, new ArrayList<>());
}
map.get(name).add(configAttributes);
}
}
// 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;
}
/**
* Resolve the list of {@linkplain ContextConfigurationAttributes context
* configuration attributes} for the supplied {@linkplain Class test class}
* and its superclasses and enclosing classes.
* Note that the {@link ContextConfiguration#inheritLocations inheritLocations} and
* {@link ContextConfiguration#inheritInitializers() inheritInitializers} flags of
* {@link ContextConfiguration @ContextConfiguration} will not
* be taken into consideration. If these flags need to be honored, that must be
* handled manually when traversing the list returned by this method.
* @param testClass the class for which to resolve the configuration attributes
* (must not be {@code null})
* @return the list of configuration attributes for the specified class, ordered
* bottom-up (i.e., as if we were traversing up the class hierarchy
* and enclosing class hierarchy); never {@code null}
* @throws IllegalArgumentException if the supplied class is {@code null} or if
* {@code @ContextConfiguration} is not present on the supplied class
*/
static List resolveContextConfigurationAttributes(Class testClass) {
Assert.notNull(testClass, "Class must not be null");
Class annotationType = ContextConfiguration.class;
AnnotationDescriptor descriptor = findAnnotationDescriptor(testClass, annotationType);
Assert.notNull(descriptor, () -> String.format(
"Could not find an 'annotation declaring class' for annotation type [%s] and class [%s]",
annotationType.getName(), testClass.getName()));
List attributesList = new ArrayList<>();
ContextConfiguration previousAnnotation = null;
Class previousDeclaringClass = null;
while (descriptor != null) {
ContextConfiguration currentAnnotation = descriptor.getAnnotation();
// Don't ignore duplicate @ContextConfiguration declaration without resources,
// because the ContextLoader will likely detect default resources specific to the
// annotated class.
if (currentAnnotation.equals(previousAnnotation) && hasResources(currentAnnotation)) {
if (logger.isDebugEnabled()) {
logger.debug(String.format("Ignoring duplicate %s declaration on [%s], "
+ "since it is also declared on [%s].", currentAnnotation,
previousDeclaringClass.getName(), descriptor.getRootDeclaringClass().getName()));
}
}
else {
convertContextConfigToConfigAttributesAndAddToList(currentAnnotation,
descriptor.getRootDeclaringClass(), attributesList);
}
previousAnnotation = currentAnnotation;
previousDeclaringClass = descriptor.getRootDeclaringClass();
descriptor = descriptor.next();
}
return attributesList;
}
private static boolean hasResources(ContextConfiguration contextConfiguration) {
return (contextConfiguration.locations().length > 0 || contextConfiguration.classes().length > 0);
}
/**
* Convenience method for creating a {@link ContextConfigurationAttributes}
* instance from the supplied {@link ContextConfiguration} annotation and
* declaring class and then adding the attributes to the supplied list.
*/
private static void convertContextConfigToConfigAttributesAndAddToList(ContextConfiguration contextConfiguration,
Class declaringClass, List attributesList) {
if (logger.isTraceEnabled()) {
logger.trace(String.format("Retrieved @ContextConfiguration [%s] for declaring class [%s].",
contextConfiguration, declaringClass.getName()));
}
ContextConfigurationAttributes attributes =
new ContextConfigurationAttributes(declaringClass, contextConfiguration);
if (logger.isTraceEnabled()) {
logger.trace("Resolved context configuration attributes: " + attributes);
}
attributesList.add(attributes);
}
}