org.springframework.test.context.ContextLoaderUtils.java Source code

Java tutorial

Introduction

Here is the source code for org.springframework.test.context.ContextLoaderUtils.java

Source

/*
 * Copyright 2002-2013 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.test.context;

import static org.springframework.beans.BeanUtils.instantiateClass;
import static org.springframework.core.annotation.AnnotationUtils.findAnnotationDeclaringClass;
import static org.springframework.core.annotation.AnnotationUtils.findAnnotationDeclaringClassForTypes;
import static org.springframework.core.annotation.AnnotationUtils.isAnnotationDeclaredLocally;

import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;

/**
 * Utility methods for working with {@link ContextLoader ContextLoaders} and
 * {@link SmartContextLoader SmartContextLoaders} and resolving resource locations,
 * annotated classes, active bean definition profiles, and application context
 * initializers.
 *
 * @author Sam Brannen
 * @since 3.1
 * @see ContextLoader
 * @see SmartContextLoader
 * @see ContextConfiguration
 * @see ContextConfigurationAttributes
 * @see ActiveProfiles
 * @see ApplicationContextInitializer
 * @see ContextHierarchy
 * @see MergedContextConfiguration
 */
abstract class ContextLoaderUtils {

    static final String GENERATED_CONTEXT_HIERARCHY_LEVEL_PREFIX = "ContextHierarchyLevel#";

    private static final Log logger = LogFactory.getLog(ContextLoaderUtils.class);

    private static final String DEFAULT_CONTEXT_LOADER_CLASS_NAME = "org.springframework.test.context.support.DelegatingSmartContextLoader";
    private static final String DEFAULT_WEB_CONTEXT_LOADER_CLASS_NAME = "org.springframework.test.context.web.WebDelegatingSmartContextLoader";

    private static final String WEB_APP_CONFIGURATION_CLASS_NAME = "org.springframework.test.context.web.WebAppConfiguration";
    private static final String WEB_MERGED_CONTEXT_CONFIGURATION_CLASS_NAME = "org.springframework.test.context.web.WebMergedContextConfiguration";

    private ContextLoaderUtils() {
        /* no-op */
    }

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

        if (!StringUtils.hasText(defaultContextLoaderClassName)) {
            Class<? extends Annotation> webAppConfigClass = loadWebAppConfigurationClass();
            defaultContextLoaderClassName = webAppConfigClass != null
                    && testClass.isAnnotationPresent(webAppConfigClass) ? DEFAULT_WEB_CONTEXT_LOADER_CLASS_NAME
                            : DEFAULT_CONTEXT_LOADER_CLASS_NAME;
        }

        Class<? extends ContextLoader> contextLoaderClass = resolveContextLoaderClass(testClass,
                configAttributesList, defaultContextLoaderClassName);

        return instantiateClass(contextLoaderClass, ContextLoader.class);
    }

    /**
     * Resolve the {@link ContextLoader} {@linkplain Class class} to use for the supplied
     * list of {@link ContextConfigurationAttributes}.
     *
     * <p>Beginning with the first level in the context configuration attributes hierarchy:
     *
     * <ol>
     * <li>If the {@link ContextConfigurationAttributes#getContextLoaderClass()
     * contextLoaderClass} property of {@link ContextConfigurationAttributes} is
     * configured with an explicit class, that class will be returned.</li>
     * <li>If an explicit {@code ContextLoader} class is not specified at the current
     * level in the hierarchy, traverse to the next level in the hierarchy and return to
     * step #1.</li>
     * <li>If no explicit {@code ContextLoader} class is found after traversing the
     * hierarchy, an attempt will be made to load and return the class with the supplied
     * {@code defaultContextLoaderClassName}.</li>
     * </ol>
     *
     * @param testClass the class for which to resolve the {@code ContextLoader} class;
     * must not be {@code null}; only used for logging purposes
     * @param configAttributesList the list of configuration attributes to process; must
     * not be {@code null} or <em>empty</em>; must be ordered <em>bottom-up</em>
     * (i.e., as if we were traversing up the class hierarchy)
     * @param defaultContextLoaderClassName the name of the default {@code ContextLoader}
     * class to use; must not be {@code null} or empty
     * @return the {@code ContextLoader} class to use for the supplied test class
     * @throws IllegalArgumentException if {@code @ContextConfiguration} is not
     * <em>present</em> on the supplied test class
     * @throws IllegalStateException if the default {@code ContextLoader} class could not
     * be loaded
     */
    @SuppressWarnings("unchecked")
    static Class<? extends ContextLoader> resolveContextLoaderClass(Class<?> testClass,
            List<ContextConfigurationAttributes> configAttributesList, String defaultContextLoaderClassName) {
        Assert.notNull(testClass, "Class must not be null");
        Assert.notEmpty(configAttributesList, "ContextConfigurationAttributes list must not be empty");
        Assert.hasText(defaultContextLoaderClassName, "Default ContextLoader class name must not be null or empty");

        for (ContextConfigurationAttributes configAttributes : configAttributesList) {
            if (logger.isTraceEnabled()) {
                logger.trace(String.format("Processing ContextLoader for context configuration attributes %s",
                        configAttributes));
            }

            Class<? extends ContextLoader> contextLoaderClass = configAttributes.getContextLoaderClass();
            if (!ContextLoader.class.equals(contextLoaderClass)) {
                if (logger.isDebugEnabled()) {
                    logger.debug(String.format(
                            "Found explicit ContextLoader class [%s] for context configuration attributes %s",
                            contextLoaderClass.getName(), configAttributes));
                }
                return contextLoaderClass;
            }
        }

        try {
            if (logger.isTraceEnabled()) {
                logger.trace(String.format("Using default ContextLoader class [%s] for test class [%s]",
                        defaultContextLoaderClassName, testClass.getName()));
            }
            return (Class<? extends ContextLoader>) ClassUtils.forName(defaultContextLoaderClassName,
                    ContextLoaderUtils.class.getClassLoader());
        } catch (Throwable t) {
            throw new IllegalStateException("Could not load default ContextLoader class ["
                    + defaultContextLoaderClassName + "]. Specify @ContextConfiguration's 'loader' "
                    + "attribute or make the default loader class available.", t);
        }
    }

    /**
     * Convenience method for creating a {@link ContextConfigurationAttributes} instance
     * from the supplied {@link ContextConfiguration} and declaring class and then adding
     * the attributes to the supplied list.
     */
    private static void convertContextConfigToConfigAttributesAndAddToList(
            ContextConfiguration contextConfiguration, Class<?> declaringClass,
            final List<ContextConfigurationAttributes> attributesList) {
        if (logger.isTraceEnabled()) {
            logger.trace(String.format("Retrieved @ContextConfiguration [%s] for declaring class [%s].",
                    contextConfiguration, declaringClass.getName()));
        }

        ContextConfigurationAttributes attributes = new ContextConfigurationAttributes(declaringClass,
                contextConfiguration);
        if (logger.isTraceEnabled()) {
            logger.trace("Resolved context configuration attributes: " + attributes);
        }
        attributesList.add(attributes);
    }

    /**
     * Resolve the list of lists of {@linkplain ContextConfigurationAttributes context
     * configuration attributes} for the supplied {@linkplain Class test class} and its
     * superclasses, taking into account context hierarchies declared via
     * {@link ContextHierarchy @ContextHierarchy} and
     * {@link ContextConfiguration @ContextConfiguration}.
     *
     * <p>The outer list represents a top-down ordering of context configuration
     * attributes, where each element in the list represents the context configuration
     * declared on a given test class in the class hierarchy. Each nested list
     * contains the context configuration attributes declared either via a single
     * instance of {@code @ContextConfiguration} on the particular class or via
     * multiple instances of {@code @ContextConfiguration} declared within a
     * single {@code @ContextHierarchy} instance on the particular class.
     * Furthermore, each nested list maintains the order in which
     * {@code @ContextConfiguration} instances are declared.
     *
     * <p>Note that the {@link ContextConfiguration#inheritLocations inheritLocations} and
     * {@link ContextConfiguration#inheritInitializers() inheritInitializers} flags of
     * {@link ContextConfiguration @ContextConfiguration} will <strong>not</strong>
     * be taken into consideration. If these flags need to be honored, that must be
     * handled manually when traversing the nested lists returned by this method.
     *
     * @param testClass the class for which to resolve the context hierarchy attributes
     * (must not be {@code null})
     * @return the list of lists of configuration attributes for the specified class;
     * never {@code null}
     * @throws IllegalArgumentException if the supplied class is {@code null}; if
     * neither {@code @ContextConfiguration} nor {@code @ContextHierarchy} is
     * <em>present</em> on the supplied class; if a given class in the class hierarchy
     * declares both {@code @ContextConfiguration} and {@code @ContextHierarchy} as
     * top-level annotations; or if individual {@code @ContextConfiguration}
     * elements within a {@code @ContextHierarchy} declaration on a given class
     * in the class hierarchy do not define unique context configuration.
     *
     * @since 3.2.2
     * @see #buildContextHierarchyMap(Class)
     * @see #resolveContextConfigurationAttributes(Class)
     */
    static List<List<ContextConfigurationAttributes>> resolveContextHierarchyAttributes(Class<?> testClass) {
        Assert.notNull(testClass, "Class must not be null");

        final Class<ContextConfiguration> contextConfigType = ContextConfiguration.class;
        final Class<ContextHierarchy> contextHierarchyType = ContextHierarchy.class;
        final List<Class<? extends Annotation>> annotationTypes = Arrays.asList(contextConfigType,
                contextHierarchyType);

        final List<List<ContextConfigurationAttributes>> hierarchyAttributes = new ArrayList<List<ContextConfigurationAttributes>>();

        Class<?> declaringClass = findAnnotationDeclaringClassForTypes(annotationTypes, testClass);
        Assert.notNull(declaringClass, String.format(
                "Could not find an 'annotation declaring class' for annotation type [%s] or [%s] and test class [%s]",
                contextConfigType.getName(), contextHierarchyType.getName(), testClass.getName()));

        while (declaringClass != null) {

            boolean contextConfigDeclaredLocally = isAnnotationDeclaredLocally(contextConfigType, declaringClass);
            boolean contextHierarchyDeclaredLocally = isAnnotationDeclaredLocally(contextHierarchyType,
                    declaringClass);

            if (contextConfigDeclaredLocally && contextHierarchyDeclaredLocally) {
                String msg = String.format("Test class [%s] has been configured with both @ContextConfiguration "
                        + "and @ContextHierarchy as class-level annotations. Only one of these annotations may "
                        + "be declared as a top-level annotation per test class.", declaringClass.getName());
                logger.error(msg);
                throw new IllegalStateException(msg);
            }

            final List<ContextConfigurationAttributes> configAttributesList = new ArrayList<ContextConfigurationAttributes>();

            if (contextConfigDeclaredLocally) {
                ContextConfiguration contextConfiguration = declaringClass.getAnnotation(contextConfigType);
                convertContextConfigToConfigAttributesAndAddToList(contextConfiguration, declaringClass,
                        configAttributesList);
            } else if (contextHierarchyDeclaredLocally) {
                ContextHierarchy contextHierarchy = declaringClass.getAnnotation(contextHierarchyType);
                for (ContextConfiguration contextConfiguration : contextHierarchy.value()) {
                    convertContextConfigToConfigAttributesAndAddToList(contextConfiguration, declaringClass,
                            configAttributesList);
                }

                // Check for uniqueness
                Set<ContextConfigurationAttributes> configAttributesSet = new HashSet<ContextConfigurationAttributes>(
                        configAttributesList);
                if (configAttributesSet.size() != configAttributesList.size()) {
                    String msg = String.format(
                            "The @ContextConfiguration elements configured via "
                                    + "@ContextHierarchy in test class [%s] must define unique contexts to load.",
                            declaringClass.getName());
                    logger.error(msg);
                    throw new IllegalStateException(msg);
                }
            } else {
                // This should theoretically actually never happen...
                String msg = String.format("Test class [%s] has been configured with neither @ContextConfiguration "
                        + "nor @ContextHierarchy as a class-level annotation.", declaringClass.getName());
                logger.error(msg);
                throw new IllegalStateException(msg);
            }

            hierarchyAttributes.add(0, configAttributesList);

            declaringClass = findAnnotationDeclaringClassForTypes(annotationTypes, declaringClass.getSuperclass());
        }

        return hierarchyAttributes;
    }

    /**
     * Build a <em>context hierarchy map</em> for the supplied {@linkplain Class
     * test class} and its superclasses, taking into account context hierarchies
     * declared via {@link ContextHierarchy @ContextHierarchy} and
     * {@link ContextConfiguration @ContextConfiguration}.
     *
     * <p>Each value in the map represents the consolidated list of {@linkplain
     * ContextConfigurationAttributes context configuration attributes} for a
     * given level in the context hierarchy (potentially across the test class
     * hierarchy), keyed by the {@link ContextConfiguration#name() name} of the
     * context hierarchy level.
     *
     * <p>If a given level in the context hierarchy does not have an explicit
     * name (i.e., configured via {@link ContextConfiguration#name}), a name will
     * be generated for that hierarchy level by appending the numerical level to
     * the {@link #GENERATED_CONTEXT_HIERARCHY_LEVEL_PREFIX}.
     *
     * @param testClass the class for which to resolve the context hierarchy map
     * (must not be {@code null})
     * @return a map of context configuration attributes for the context hierarchy,
     * keyed by context hierarchy level name; never {@code null}
     *
     * @since 3.2.2
     * @see #resolveContextHierarchyAttributes(Class)
     */
    static Map<String, List<ContextConfigurationAttributes>> buildContextHierarchyMap(Class<?> testClass) {
        final Map<String, List<ContextConfigurationAttributes>> map = new LinkedHashMap<String, List<ContextConfigurationAttributes>>();
        int hierarchyLevel = 1;

        for (List<ContextConfigurationAttributes> 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<ContextConfigurationAttributes>());
                }

                map.get(name).add(configAttributes);
            }
        }

        return map;
    }

    /**
     * Resolve the list of {@linkplain ContextConfigurationAttributes context
     * configuration attributes} for the supplied {@linkplain Class test class} and its
     * superclasses.
     *
     * <p>Note that the {@link ContextConfiguration#inheritLocations inheritLocations} and
     * {@link ContextConfiguration#inheritInitializers() inheritInitializers} flags of
     * {@link ContextConfiguration @ContextConfiguration} will <strong>not</strong>
     * 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
     * <em>bottom-up</em> (i.e., as if we were traversing up the class hierarchy);
     * never {@code null}
     * @throws IllegalArgumentException if the supplied class is {@code null} or if
     * {@code @ContextConfiguration} is not <em>present</em> on the supplied class
     */
    static List<ContextConfigurationAttributes> resolveContextConfigurationAttributes(Class<?> testClass) {
        Assert.notNull(testClass, "Class must not be null");

        final List<ContextConfigurationAttributes> attributesList = new ArrayList<ContextConfigurationAttributes>();

        Class<ContextConfiguration> annotationType = ContextConfiguration.class;
        Class<?> declaringClass = findAnnotationDeclaringClass(annotationType, testClass);
        Assert.notNull(declaringClass,
                String.format(
                        "Could not find an 'annotation declaring class' for annotation type [%s] and class [%s]",
                        annotationType.getName(), testClass.getName()));

        while (declaringClass != null) {
            ContextConfiguration contextConfiguration = declaringClass.getAnnotation(annotationType);
            convertContextConfigToConfigAttributesAndAddToList(contextConfiguration, declaringClass,
                    attributesList);
            declaringClass = findAnnotationDeclaringClass(annotationType, declaringClass.getSuperclass());
        }

        return attributesList;
    }

    /**
     * Resolve the list of merged {@code ApplicationContextInitializer} classes for the
     * supplied list of {@code ContextConfigurationAttributes}.
     *
     * <p>Note that the {@link ContextConfiguration#inheritInitializers inheritInitializers}
     * flag of {@link ContextConfiguration @ContextConfiguration} will be taken into
     * consideration. Specifically, if the {@code inheritInitializers} flag is set to
     * {@code true} for a given level in the class hierarchy represented by the provided
     * configuration attributes, context initializer classes defined at the given level
     * will be merged with those defined in higher levels of the class hierarchy.
     *
     * @param configAttributesList the list of configuration attributes to process; must
     * not be {@code null} or <em>empty</em>; must be ordered <em>bottom-up</em>
     * (i.e., as if we were traversing up the class hierarchy)
     * @return the set of merged context initializer classes, including those from
     * superclasses if appropriate (never {@code null})
     * @since 3.2
     */
    static Set<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>> resolveInitializerClasses(
            List<ContextConfigurationAttributes> configAttributesList) {
        Assert.notEmpty(configAttributesList, "ContextConfigurationAttributes list must not be empty");

        final Set<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>> initializerClasses = //
                new HashSet<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>>();

        for (ContextConfigurationAttributes configAttributes : configAttributesList) {
            if (logger.isTraceEnabled()) {
                logger.trace(
                        String.format("Processing context initializers for context configuration attributes %s",
                                configAttributes));
            }

            initializerClasses.addAll(Arrays.asList(configAttributes.getInitializers()));

            if (!configAttributes.isInheritInitializers()) {
                break;
            }
        }

        return initializerClasses;
    }

    /**
     * Resolve <em>active bean definition profiles</em> for the supplied {@link Class}.
     *
     * <p>Note that the {@link ActiveProfiles#inheritProfiles inheritProfiles} flag of
     * {@link ActiveProfiles @ActiveProfiles} will be taken into consideration.
     * Specifically, if the {@code inheritProfiles} flag is set to {@code true}, profiles
     * defined in the test class will be merged with those defined in superclasses.
     *
     * @param testClass the class for which to resolve the active profiles (must not be
     * {@code null})
     * @return the set of active profiles for the specified class, including active
     * profiles from superclasses if appropriate (never {@code null})
     * @see ActiveProfiles
     * @see org.springframework.context.annotation.Profile
     */
    static String[] resolveActiveProfiles(Class<?> testClass) {
        Assert.notNull(testClass, "Class must not be null");

        Class<ActiveProfiles> annotationType = ActiveProfiles.class;
        Class<?> declaringClass = findAnnotationDeclaringClass(annotationType, testClass);

        if (declaringClass == null && logger.isDebugEnabled()) {
            logger.debug(String.format(
                    "Could not find an 'annotation declaring class' for annotation type [%s] and class [%s]",
                    annotationType.getName(), testClass.getName()));
        }

        final Set<String> activeProfiles = new HashSet<String>();

        while (declaringClass != null) {
            ActiveProfiles annotation = declaringClass.getAnnotation(annotationType);

            if (logger.isTraceEnabled()) {
                logger.trace(String.format("Retrieved @ActiveProfiles [%s] for declaring class [%s].", annotation,
                        declaringClass.getName()));
            }

            String[] profiles = annotation.profiles();
            String[] valueProfiles = annotation.value();

            if (!ObjectUtils.isEmpty(valueProfiles) && !ObjectUtils.isEmpty(profiles)) {
                String msg = String.format(
                        "Test class [%s] has been configured with @ActiveProfiles' 'value' [%s] "
                                + "and 'profiles' [%s] attributes. Only one declaration of active bean "
                                + "definition profiles is permitted per @ActiveProfiles annotation.",
                        declaringClass.getName(), ObjectUtils.nullSafeToString(valueProfiles),
                        ObjectUtils.nullSafeToString(profiles));
                logger.error(msg);
                throw new IllegalStateException(msg);
            } else if (!ObjectUtils.isEmpty(valueProfiles)) {
                profiles = valueProfiles;
            }

            for (String profile : profiles) {
                if (StringUtils.hasText(profile)) {
                    activeProfiles.add(profile.trim());
                }
            }

            declaringClass = annotation.inheritProfiles()
                    ? findAnnotationDeclaringClass(annotationType, declaringClass.getSuperclass())
                    : null;
        }

        return StringUtils.toStringArray(activeProfiles);
    }

    /**
     * Build the {@link MergedContextConfiguration merged context configuration} for
     * the supplied {@link Class testClass} and {@code defaultContextLoaderClassName},
     * taking into account context hierarchies declared via
     * {@link ContextHierarchy @ContextHierarchy} and
     * {@link ContextConfiguration @ContextConfiguration}.
     *
     * @param testClass the test class for which the {@code MergedContextConfiguration}
     * should be built (must not be {@code null})
     * @param defaultContextLoaderClassName the name of the default {@code ContextLoader}
     * class to use (may be {@code null})
     * @param cacheAwareContextLoaderDelegate the cache-aware context loader delegate to
     * be passed to the {@code MergedContextConfiguration} constructor
     * @return the merged context configuration
     * @see #buildContextHierarchyMap(Class)
     * @see #buildMergedContextConfiguration(Class, List, String, MergedContextConfiguration, CacheAwareContextLoaderDelegate)
     */
    @SuppressWarnings("javadoc")
    static MergedContextConfiguration buildMergedContextConfiguration(Class<?> testClass,
            String defaultContextLoaderClassName, CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate) {

        if (testClass.isAnnotationPresent(ContextHierarchy.class)) {
            Map<String, List<ContextConfigurationAttributes>> hierarchyMap = buildContextHierarchyMap(testClass);

            MergedContextConfiguration parentConfig = null;
            MergedContextConfiguration mergedConfig = null;

            for (List<ContextConfigurationAttributes> list : hierarchyMap.values()) {
                List<ContextConfigurationAttributes> reversedList = new ArrayList<ContextConfigurationAttributes>(
                        list);
                Collections.reverse(reversedList);

                // Don't use the supplied testClass; instead ensure that we are
                // building the MCC for the actual test class that declared the
                // configuration for the current level in the context hierarchy.
                Assert.notEmpty(reversedList, "ContextConfigurationAttributes list must not be empty");
                Class<?> declaringClass = reversedList.get(0).getDeclaringClass();

                mergedConfig = buildMergedContextConfiguration(declaringClass, reversedList,
                        defaultContextLoaderClassName, parentConfig, cacheAwareContextLoaderDelegate);
                parentConfig = mergedConfig;
            }

            // Return the last level in the context hierarchy
            return mergedConfig;
        } else {
            return buildMergedContextConfiguration(testClass, resolveContextConfigurationAttributes(testClass),
                    defaultContextLoaderClassName, null, cacheAwareContextLoaderDelegate);
        }
    }

    /**
     * Build the {@link MergedContextConfiguration merged context configuration} for the
     * supplied {@link Class testClass}, context configuration attributes,
     * {@code defaultContextLoaderClassName}, and parent context configuration.
     *
     * @param testClass the test class for which the {@code MergedContextConfiguration}
     * should be built (must not be {@code null})
     * @param configAttributesList the list of context configuration attributes for the
     * specified test class, ordered <em>bottom-up</em> (i.e., as if we were
     * traversing up the class hierarchy); never {@code null} or empty
     * @param defaultContextLoaderClassName the name of the default {@code ContextLoader}
     * class to use (may be {@code null})
     * @param parentConfig the merged context configuration for the parent application
     * context in a context hierarchy, or {@code null} if there is no parent
     * @param cacheAwareContextLoaderDelegate the cache-aware context loader delegate to
     * be passed to the {@code MergedContextConfiguration} constructor
     * @return the merged context configuration
     * @see #resolveContextLoader
     * @see #resolveContextConfigurationAttributes
     * @see SmartContextLoader#processContextConfiguration
     * @see ContextLoader#processLocations
     * @see #resolveActiveProfiles
     * @see MergedContextConfiguration
     */
    private static MergedContextConfiguration buildMergedContextConfiguration(final Class<?> testClass,
            final List<ContextConfigurationAttributes> configAttributesList,
            final String defaultContextLoaderClassName, MergedContextConfiguration parentConfig,
            CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate) {

        final ContextLoader contextLoader = resolveContextLoader(testClass, configAttributesList,
                defaultContextLoaderClassName);
        final List<String> locationsList = new ArrayList<String>();
        final List<Class<?>> classesList = new ArrayList<Class<?>>();

        for (ContextConfigurationAttributes configAttributes : configAttributesList) {
            if (logger.isTraceEnabled()) {
                logger.trace(
                        String.format("Processing locations and classes for context configuration attributes %s",
                                configAttributes));
            }

            if (contextLoader instanceof SmartContextLoader) {
                SmartContextLoader smartContextLoader = (SmartContextLoader) contextLoader;
                smartContextLoader.processContextConfiguration(configAttributes);
                locationsList.addAll(0, Arrays.asList(configAttributes.getLocations()));
                classesList.addAll(0, Arrays.asList(configAttributes.getClasses()));
            } else {
                String[] processedLocations = contextLoader.processLocations(configAttributes.getDeclaringClass(),
                        configAttributes.getLocations());
                locationsList.addAll(0, Arrays.asList(processedLocations));
                // Legacy ContextLoaders don't know how to process classes
            }

            if (!configAttributes.isInheritLocations()) {
                break;
            }
        }

        String[] locations = StringUtils.toStringArray(locationsList);
        Class<?>[] classes = ClassUtils.toClassArray(classesList);
        Set<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>> initializerClasses = resolveInitializerClasses(
                configAttributesList);
        String[] activeProfiles = resolveActiveProfiles(testClass);

        MergedContextConfiguration mergedConfig = buildWebMergedContextConfiguration(testClass, locations, classes,
                initializerClasses, activeProfiles, contextLoader, cacheAwareContextLoaderDelegate, parentConfig);

        if (mergedConfig == null) {
            mergedConfig = new MergedContextConfiguration(testClass, locations, classes, initializerClasses,
                    activeProfiles, contextLoader, cacheAwareContextLoaderDelegate, parentConfig);
        }

        return mergedConfig;
    }

    /**
     * Load the {@link org.springframework.test.context.web.WebAppConfiguration}
     * class, using reflection in order to avoid package cycles.
     *
     * @return the {@code @WebAppConfiguration} class or {@code null} if it cannot be loaded
     * @since 3.2
     */
    @SuppressWarnings("unchecked")
    private static Class<? extends Annotation> loadWebAppConfigurationClass() {
        Class<? extends Annotation> webAppConfigClass = null;
        try {
            webAppConfigClass = (Class<? extends Annotation>) ClassUtils.forName(WEB_APP_CONFIGURATION_CLASS_NAME,
                    ContextLoaderUtils.class.getClassLoader());
        } catch (Throwable t) {
            if (logger.isDebugEnabled()) {
                logger.debug(
                        "Could not load @WebAppConfiguration class [" + WEB_APP_CONFIGURATION_CLASS_NAME + "].", t);
            }
        }
        return webAppConfigClass;
    }

    /**
     * Attempt to build a {@link org.springframework.test.context.web.WebMergedContextConfiguration}
     * from the supplied arguments, using reflection in order to avoid package cycles.
     *
     * @return the {@code WebMergedContextConfiguration} or {@code null} if it could not be built
     * @since 3.2
     */
    @SuppressWarnings("unchecked")
    private static MergedContextConfiguration buildWebMergedContextConfiguration(Class<?> testClass,
            String[] locations, Class<?>[] classes,
            Set<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>> initializerClasses,
            String[] activeProfiles, ContextLoader contextLoader,
            CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate,
            MergedContextConfiguration parentConfig) {

        Class<? extends Annotation> webAppConfigClass = loadWebAppConfigurationClass();

        if (webAppConfigClass != null && testClass.isAnnotationPresent(webAppConfigClass)) {
            Annotation annotation = testClass.getAnnotation(webAppConfigClass);
            String resourceBasePath = (String) AnnotationUtils.getValue(annotation);

            try {
                Class<? extends MergedContextConfiguration> webMergedConfigClass = (Class<? extends MergedContextConfiguration>) ClassUtils
                        .forName(WEB_MERGED_CONTEXT_CONFIGURATION_CLASS_NAME,
                                ContextLoaderUtils.class.getClassLoader());

                Constructor<? extends MergedContextConfiguration> constructor = ClassUtils
                        .getConstructorIfAvailable(webMergedConfigClass, Class.class, String[].class, Class[].class,
                                Set.class, String[].class, String.class, ContextLoader.class,
                                CacheAwareContextLoaderDelegate.class, MergedContextConfiguration.class);

                if (constructor != null) {
                    return instantiateClass(constructor, testClass, locations, classes, initializerClasses,
                            activeProfiles, resourceBasePath, contextLoader, cacheAwareContextLoaderDelegate,
                            parentConfig);
                }
            } catch (Throwable t) {
                if (logger.isDebugEnabled()) {
                    logger.debug("Could not instantiate [" + WEB_MERGED_CONTEXT_CONFIGURATION_CLASS_NAME + "].", t);
                }
            }
        }

        return null;
    }

}