001/*
002 * Copyright 2002-2018 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.test.context.support;
018
019import java.util.ArrayList;
020import java.util.Arrays;
021import java.util.Collection;
022import java.util.Collections;
023import java.util.LinkedHashSet;
024import java.util.List;
025import java.util.Map;
026import java.util.Set;
027
028import org.apache.commons.logging.Log;
029import org.apache.commons.logging.LogFactory;
030
031import org.springframework.beans.BeanInstantiationException;
032import org.springframework.beans.BeanUtils;
033import org.springframework.core.annotation.AnnotationAwareOrderComparator;
034import org.springframework.core.annotation.AnnotationUtils;
035import org.springframework.core.io.support.SpringFactoriesLoader;
036import org.springframework.lang.Nullable;
037import org.springframework.test.context.BootstrapContext;
038import org.springframework.test.context.CacheAwareContextLoaderDelegate;
039import org.springframework.test.context.ContextConfiguration;
040import org.springframework.test.context.ContextConfigurationAttributes;
041import org.springframework.test.context.ContextCustomizer;
042import org.springframework.test.context.ContextCustomizerFactory;
043import org.springframework.test.context.ContextHierarchy;
044import org.springframework.test.context.ContextLoader;
045import org.springframework.test.context.MergedContextConfiguration;
046import org.springframework.test.context.SmartContextLoader;
047import org.springframework.test.context.TestContext;
048import org.springframework.test.context.TestContextBootstrapper;
049import org.springframework.test.context.TestExecutionListener;
050import org.springframework.test.context.TestExecutionListeners;
051import org.springframework.test.context.TestExecutionListeners.MergeMode;
052import org.springframework.test.util.MetaAnnotationUtils;
053import org.springframework.test.util.MetaAnnotationUtils.AnnotationDescriptor;
054import org.springframework.util.Assert;
055import org.springframework.util.ClassUtils;
056import org.springframework.util.StringUtils;
057
058/**
059 * Abstract implementation of the {@link TestContextBootstrapper} interface which
060 * provides most of the behavior required by a bootstrapper.
061 *
062 * <p>Concrete subclasses typically will only need to provide implementations for
063 * the following methods:
064 * <ul>
065 * <li>{@link #getDefaultContextLoaderClass}
066 * <li>{@link #processMergedContextConfiguration}
067 * </ul>
068 *
069 * <p>To plug in custom
070 * {@link org.springframework.test.context.cache.ContextCache ContextCache}
071 * support, override {@link #getCacheAwareContextLoaderDelegate()}.
072 *
073 * @author Sam Brannen
074 * @author Juergen Hoeller
075 * @author Phillip Webb
076 * @since 4.1
077 */
078public abstract class AbstractTestContextBootstrapper implements TestContextBootstrapper {
079
080        private final Log logger = LogFactory.getLog(getClass());
081
082        @Nullable
083        private BootstrapContext bootstrapContext;
084
085
086        @Override
087        public void setBootstrapContext(BootstrapContext bootstrapContext) {
088                this.bootstrapContext = bootstrapContext;
089        }
090
091        @Override
092        public BootstrapContext getBootstrapContext() {
093                Assert.state(this.bootstrapContext != null, "No BootstrapContext set");
094                return this.bootstrapContext;
095        }
096
097        /**
098         * Build a new {@link DefaultTestContext} using the {@linkplain Class test class}
099         * in the {@link BootstrapContext} associated with this bootstrapper and
100         * by delegating to {@link #buildMergedContextConfiguration()} and
101         * {@link #getCacheAwareContextLoaderDelegate()}.
102         * <p>Concrete subclasses may choose to override this method to return a
103         * custom {@link TestContext} implementation.
104         * @since 4.2
105         */
106        @Override
107        public TestContext buildTestContext() {
108                return new DefaultTestContext(getBootstrapContext().getTestClass(), buildMergedContextConfiguration(),
109                                getCacheAwareContextLoaderDelegate());
110        }
111
112        @Override
113        public final List<TestExecutionListener> getTestExecutionListeners() {
114                Class<?> clazz = getBootstrapContext().getTestClass();
115                Class<TestExecutionListeners> annotationType = TestExecutionListeners.class;
116                List<Class<? extends TestExecutionListener>> classesList = new ArrayList<>();
117                boolean usingDefaults = false;
118
119                AnnotationDescriptor<TestExecutionListeners> descriptor =
120                                MetaAnnotationUtils.findAnnotationDescriptor(clazz, annotationType);
121
122                // Use defaults?
123                if (descriptor == null) {
124                        if (logger.isDebugEnabled()) {
125                                logger.debug(String.format("@TestExecutionListeners is not present for class [%s]: using defaults.",
126                                                clazz.getName()));
127                        }
128                        usingDefaults = true;
129                        classesList.addAll(getDefaultTestExecutionListenerClasses());
130                }
131                else {
132                        // Traverse the class hierarchy...
133                        while (descriptor != null) {
134                                Class<?> declaringClass = descriptor.getDeclaringClass();
135                                TestExecutionListeners testExecutionListeners = descriptor.synthesizeAnnotation();
136                                if (logger.isTraceEnabled()) {
137                                        logger.trace(String.format("Retrieved @TestExecutionListeners [%s] for declaring class [%s].",
138                                                        testExecutionListeners, declaringClass.getName()));
139                                }
140
141                                boolean inheritListeners = testExecutionListeners.inheritListeners();
142                                AnnotationDescriptor<TestExecutionListeners> superDescriptor =
143                                                MetaAnnotationUtils.findAnnotationDescriptor(
144                                                                descriptor.getRootDeclaringClass().getSuperclass(), annotationType);
145
146                                // If there are no listeners to inherit, we might need to merge the
147                                // locally declared listeners with the defaults.
148                                if ((!inheritListeners || superDescriptor == null) &&
149                                                testExecutionListeners.mergeMode() == MergeMode.MERGE_WITH_DEFAULTS) {
150                                        if (logger.isDebugEnabled()) {
151                                                logger.debug(String.format("Merging default listeners with listeners configured via " +
152                                                                "@TestExecutionListeners for class [%s].", descriptor.getRootDeclaringClass().getName()));
153                                        }
154                                        usingDefaults = true;
155                                        classesList.addAll(getDefaultTestExecutionListenerClasses());
156                                }
157
158                                classesList.addAll(0, Arrays.asList(testExecutionListeners.listeners()));
159
160                                descriptor = (inheritListeners ? superDescriptor : null);
161                        }
162                }
163
164                Collection<Class<? extends TestExecutionListener>> classesToUse = classesList;
165                // Remove possible duplicates if we loaded default listeners.
166                if (usingDefaults) {
167                        classesToUse = new LinkedHashSet<>(classesList);
168                }
169
170                List<TestExecutionListener> listeners = instantiateListeners(classesToUse);
171                // Sort by Ordered/@Order if we loaded default listeners.
172                if (usingDefaults) {
173                        AnnotationAwareOrderComparator.sort(listeners);
174                }
175
176                if (logger.isInfoEnabled()) {
177                        logger.info("Using TestExecutionListeners: " + listeners);
178                }
179                return listeners;
180        }
181
182        private List<TestExecutionListener> instantiateListeners(Collection<Class<? extends TestExecutionListener>> classes) {
183                List<TestExecutionListener> listeners = new ArrayList<>(classes.size());
184                for (Class<? extends TestExecutionListener> listenerClass : classes) {
185                        try {
186                                listeners.add(BeanUtils.instantiateClass(listenerClass));
187                        }
188                        catch (BeanInstantiationException ex) {
189                                if (ex.getCause() instanceof NoClassDefFoundError) {
190                                        // TestExecutionListener not applicable due to a missing dependency
191                                        if (logger.isDebugEnabled()) {
192                                                logger.debug(String.format(
193                                                                "Skipping candidate TestExecutionListener [%s] due to a missing dependency. " +
194                                                                "Specify custom listener classes or make the default listener classes " +
195                                                                "and their required dependencies available. Offending class: [%s]",
196                                                                listenerClass.getName(), ex.getCause().getMessage()));
197                                        }
198                                }
199                                else {
200                                        throw ex;
201                                }
202                        }
203                }
204                return listeners;
205        }
206
207        /**
208         * Get the default {@link TestExecutionListener} classes for this bootstrapper.
209         * <p>This method is invoked by {@link #getTestExecutionListeners()} and
210         * delegates to {@link #getDefaultTestExecutionListenerClassNames()} to
211         * retrieve the class names.
212         * <p>If a particular class cannot be loaded, a {@code DEBUG} message will
213         * be logged, but the associated exception will not be rethrown.
214         */
215        @SuppressWarnings("unchecked")
216        protected Set<Class<? extends TestExecutionListener>> getDefaultTestExecutionListenerClasses() {
217                Set<Class<? extends TestExecutionListener>> defaultListenerClasses = new LinkedHashSet<>();
218                ClassLoader cl = getClass().getClassLoader();
219                for (String className : getDefaultTestExecutionListenerClassNames()) {
220                        try {
221                                defaultListenerClasses.add((Class<? extends TestExecutionListener>) ClassUtils.forName(className, cl));
222                        }
223                        catch (Throwable ex) {
224                                if (logger.isDebugEnabled()) {
225                                        logger.debug("Could not load default TestExecutionListener class [" + className +
226                                                        "]. Specify custom listener classes or make the default listener classes available.", ex);
227                                }
228                        }
229                }
230                return defaultListenerClasses;
231        }
232
233        /**
234         * Get the names of the default {@link TestExecutionListener} classes for
235         * this bootstrapper.
236         * <p>The default implementation looks up all
237         * {@code org.springframework.test.context.TestExecutionListener} entries
238         * configured in all {@code META-INF/spring.factories} files on the classpath.
239         * <p>This method is invoked by {@link #getDefaultTestExecutionListenerClasses()}.
240         * @return an <em>unmodifiable</em> list of names of default {@code TestExecutionListener}
241         * classes
242         * @see SpringFactoriesLoader#loadFactoryNames
243         */
244        protected List<String> getDefaultTestExecutionListenerClassNames() {
245                List<String> classNames =
246                                SpringFactoriesLoader.loadFactoryNames(TestExecutionListener.class, getClass().getClassLoader());
247                if (logger.isInfoEnabled()) {
248                        logger.info(String.format("Loaded default TestExecutionListener class names from location [%s]: %s",
249                                        SpringFactoriesLoader.FACTORIES_RESOURCE_LOCATION, classNames));
250                }
251                return Collections.unmodifiableList(classNames);
252        }
253
254        /**
255         * {@inheritDoc}
256         */
257        @SuppressWarnings("unchecked")
258        @Override
259        public final MergedContextConfiguration buildMergedContextConfiguration() {
260                Class<?> testClass = getBootstrapContext().getTestClass();
261                CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate = getCacheAwareContextLoaderDelegate();
262
263                if (MetaAnnotationUtils.findAnnotationDescriptorForTypes(
264                                testClass, ContextConfiguration.class, ContextHierarchy.class) == null) {
265                        return buildDefaultMergedContextConfiguration(testClass, cacheAwareContextLoaderDelegate);
266                }
267
268                if (AnnotationUtils.findAnnotation(testClass, ContextHierarchy.class) != null) {
269                        Map<String, List<ContextConfigurationAttributes>> hierarchyMap =
270                                        ContextLoaderUtils.buildContextHierarchyMap(testClass);
271                        MergedContextConfiguration parentConfig = null;
272                        MergedContextConfiguration mergedConfig = null;
273
274                        for (List<ContextConfigurationAttributes> list : hierarchyMap.values()) {
275                                List<ContextConfigurationAttributes> reversedList = new ArrayList<>(list);
276                                Collections.reverse(reversedList);
277
278                                // Don't use the supplied testClass; instead ensure that we are
279                                // building the MCC for the actual test class that declared the
280                                // configuration for the current level in the context hierarchy.
281                                Assert.notEmpty(reversedList, "ContextConfigurationAttributes list must not be empty");
282                                Class<?> declaringClass = reversedList.get(0).getDeclaringClass();
283
284                                mergedConfig = buildMergedContextConfiguration(
285                                                declaringClass, reversedList, parentConfig, cacheAwareContextLoaderDelegate, true);
286                                parentConfig = mergedConfig;
287                        }
288
289                        // Return the last level in the context hierarchy
290                        Assert.state(mergedConfig != null, "No merged context configuration");
291                        return mergedConfig;
292                }
293                else {
294                        return buildMergedContextConfiguration(testClass,
295                                        ContextLoaderUtils.resolveContextConfigurationAttributes(testClass),
296                                        null, cacheAwareContextLoaderDelegate, true);
297                }
298        }
299
300        private MergedContextConfiguration buildDefaultMergedContextConfiguration(Class<?> testClass,
301                        CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate) {
302
303                List<ContextConfigurationAttributes> defaultConfigAttributesList =
304                                Collections.singletonList(new ContextConfigurationAttributes(testClass));
305
306                ContextLoader contextLoader = resolveContextLoader(testClass, defaultConfigAttributesList);
307                if (logger.isInfoEnabled()) {
308                        logger.info(String.format(
309                                        "Neither @ContextConfiguration nor @ContextHierarchy found for test class [%s], using %s",
310                                        testClass.getName(), contextLoader.getClass().getSimpleName()));
311                }
312                return buildMergedContextConfiguration(testClass, defaultConfigAttributesList, null,
313                                cacheAwareContextLoaderDelegate, false);
314        }
315
316        /**
317         * Build the {@link MergedContextConfiguration merged context configuration}
318         * for the supplied {@link Class testClass}, context configuration attributes,
319         * and parent context configuration.
320         * @param testClass the test class for which the {@code MergedContextConfiguration}
321         * should be built (must not be {@code null})
322         * @param configAttributesList the list of context configuration attributes for the
323         * specified test class, ordered <em>bottom-up</em> (i.e., as if we were
324         * traversing up the class hierarchy); never {@code null} or empty
325         * @param parentConfig the merged context configuration for the parent application
326         * context in a context hierarchy, or {@code null} if there is no parent
327         * @param cacheAwareContextLoaderDelegate the cache-aware context loader delegate to
328         * be passed to the {@code MergedContextConfiguration} constructor
329         * @param requireLocationsClassesOrInitializers whether locations, classes, or
330         * initializers are required; typically {@code true} but may be set to {@code false}
331         * if the configured loader supports empty configuration
332         * @return the merged context configuration
333         * @see #resolveContextLoader
334         * @see ContextLoaderUtils#resolveContextConfigurationAttributes
335         * @see SmartContextLoader#processContextConfiguration
336         * @see ContextLoader#processLocations
337         * @see ActiveProfilesUtils#resolveActiveProfiles
338         * @see ApplicationContextInitializerUtils#resolveInitializerClasses
339         * @see MergedContextConfiguration
340         */
341        private MergedContextConfiguration buildMergedContextConfiguration(Class<?> testClass,
342                        List<ContextConfigurationAttributes> configAttributesList, @Nullable MergedContextConfiguration parentConfig,
343                        CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate,
344                        boolean requireLocationsClassesOrInitializers) {
345
346                Assert.notEmpty(configAttributesList, "ContextConfigurationAttributes list must not be null or empty");
347
348                ContextLoader contextLoader = resolveContextLoader(testClass, configAttributesList);
349                List<String> locations = new ArrayList<>();
350                List<Class<?>> classes = new ArrayList<>();
351                List<Class<?>> initializers = new ArrayList<>();
352
353                for (ContextConfigurationAttributes configAttributes : configAttributesList) {
354                        if (logger.isTraceEnabled()) {
355                                logger.trace(String.format("Processing locations and classes for context configuration attributes %s",
356                                                configAttributes));
357                        }
358                        if (contextLoader instanceof SmartContextLoader) {
359                                SmartContextLoader smartContextLoader = (SmartContextLoader) contextLoader;
360                                smartContextLoader.processContextConfiguration(configAttributes);
361                                locations.addAll(0, Arrays.asList(configAttributes.getLocations()));
362                                classes.addAll(0, Arrays.asList(configAttributes.getClasses()));
363                        }
364                        else {
365                                String[] processedLocations = contextLoader.processLocations(
366                                                configAttributes.getDeclaringClass(), configAttributes.getLocations());
367                                locations.addAll(0, Arrays.asList(processedLocations));
368                                // Legacy ContextLoaders don't know how to process classes
369                        }
370                        initializers.addAll(0, Arrays.asList(configAttributes.getInitializers()));
371                        if (!configAttributes.isInheritLocations()) {
372                                break;
373                        }
374                }
375
376                Set<ContextCustomizer> contextCustomizers = getContextCustomizers(testClass,
377                                Collections.unmodifiableList(configAttributesList));
378
379                Assert.state(!(requireLocationsClassesOrInitializers &&
380                                areAllEmpty(locations, classes, initializers, contextCustomizers)), () -> String.format(
381                                "%s was unable to detect defaults, and no ApplicationContextInitializers " +
382                                "or ContextCustomizers were declared for context configuration attributes %s",
383                                contextLoader.getClass().getSimpleName(), configAttributesList));
384
385                MergedTestPropertySources mergedTestPropertySources =
386                                TestPropertySourceUtils.buildMergedTestPropertySources(testClass);
387                MergedContextConfiguration mergedConfig = new MergedContextConfiguration(testClass,
388                                StringUtils.toStringArray(locations), ClassUtils.toClassArray(classes),
389                                ApplicationContextInitializerUtils.resolveInitializerClasses(configAttributesList),
390                                ActiveProfilesUtils.resolveActiveProfiles(testClass),
391                                mergedTestPropertySources.getLocations(),
392                                mergedTestPropertySources.getProperties(),
393                                contextCustomizers, contextLoader, cacheAwareContextLoaderDelegate, parentConfig);
394
395                return processMergedContextConfiguration(mergedConfig);
396        }
397
398        private Set<ContextCustomizer> getContextCustomizers(Class<?> testClass,
399                        List<ContextConfigurationAttributes> configAttributes) {
400
401                List<ContextCustomizerFactory> factories = getContextCustomizerFactories();
402                Set<ContextCustomizer> customizers = new LinkedHashSet<>(factories.size());
403                for (ContextCustomizerFactory factory : factories) {
404                        ContextCustomizer customizer = factory.createContextCustomizer(testClass, configAttributes);
405                        if (customizer != null) {
406                                customizers.add(customizer);
407                        }
408                }
409                return customizers;
410        }
411
412        /**
413         * Get the {@link ContextCustomizerFactory} instances for this bootstrapper.
414         * <p>The default implementation uses the {@link SpringFactoriesLoader} mechanism
415         * for loading factories configured in all {@code META-INF/spring.factories}
416         * files on the classpath.
417         * @since 4.3
418         * @see SpringFactoriesLoader#loadFactories
419         */
420        protected List<ContextCustomizerFactory> getContextCustomizerFactories() {
421                return SpringFactoriesLoader.loadFactories(ContextCustomizerFactory.class, getClass().getClassLoader());
422        }
423
424        /**
425         * Resolve the {@link ContextLoader} {@linkplain Class class} to use for the
426         * supplied list of {@link ContextConfigurationAttributes} and then instantiate
427         * and return that {@code ContextLoader}.
428         * <p>If the user has not explicitly declared which loader to use, the value
429         * returned from {@link #getDefaultContextLoaderClass} will be used as the
430         * default context loader class. For details on the class resolution process,
431         * see {@link #resolveExplicitContextLoaderClass} and
432         * {@link #getDefaultContextLoaderClass}.
433         * @param testClass the test class for which the {@code ContextLoader} should be
434         * resolved; must not be {@code null}
435         * @param configAttributesList the list of configuration attributes to process; must
436         * not be {@code null}; must be ordered <em>bottom-up</em>
437         * (i.e., as if we were traversing up the class hierarchy)
438         * @return the resolved {@code ContextLoader} for the supplied {@code testClass}
439         * (never {@code null})
440         * @throws IllegalStateException if {@link #getDefaultContextLoaderClass(Class)}
441         * returns {@code null}
442         */
443        protected ContextLoader resolveContextLoader(Class<?> testClass,
444                        List<ContextConfigurationAttributes> configAttributesList) {
445
446                Assert.notNull(testClass, "Class must not be null");
447                Assert.notNull(configAttributesList, "ContextConfigurationAttributes list must not be null");
448
449                Class<? extends ContextLoader> contextLoaderClass = resolveExplicitContextLoaderClass(configAttributesList);
450                if (contextLoaderClass == null) {
451                        contextLoaderClass = getDefaultContextLoaderClass(testClass);
452                }
453                if (logger.isTraceEnabled()) {
454                        logger.trace(String.format("Using ContextLoader class [%s] for test class [%s]",
455                                        contextLoaderClass.getName(), testClass.getName()));
456                }
457                return BeanUtils.instantiateClass(contextLoaderClass, ContextLoader.class);
458        }
459
460        /**
461         * Resolve the {@link ContextLoader} {@linkplain Class class} to use for the supplied
462         * list of {@link ContextConfigurationAttributes}.
463         * <p>Beginning with the first level in the context configuration attributes hierarchy:
464         * <ol>
465         * <li>If the {@link ContextConfigurationAttributes#getContextLoaderClass()
466         * contextLoaderClass} property of {@link ContextConfigurationAttributes} is
467         * configured with an explicit class, that class will be returned.</li>
468         * <li>If an explicit {@code ContextLoader} class is not specified at the current
469         * level in the hierarchy, traverse to the next level in the hierarchy and return to
470         * step #1.</li>
471         * </ol>
472         * @param configAttributesList the list of configuration attributes to process;
473         * must not be {@code null}; must be ordered <em>bottom-up</em>
474         * (i.e., as if we were traversing up the class hierarchy)
475         * @return the {@code ContextLoader} class to use for the supplied configuration
476         * attributes, or {@code null} if no explicit loader is found
477         * @throws IllegalArgumentException if supplied configuration attributes are
478         * {@code null} or <em>empty</em>
479         */
480        @Nullable
481        protected Class<? extends ContextLoader> resolveExplicitContextLoaderClass(
482                        List<ContextConfigurationAttributes> configAttributesList) {
483
484                Assert.notNull(configAttributesList, "ContextConfigurationAttributes list must not be null");
485
486                for (ContextConfigurationAttributes configAttributes : configAttributesList) {
487                        if (logger.isTraceEnabled()) {
488                                logger.trace(String.format("Resolving ContextLoader for context configuration attributes %s",
489                                                configAttributes));
490                        }
491                        Class<? extends ContextLoader> contextLoaderClass = configAttributes.getContextLoaderClass();
492                        if (ContextLoader.class != contextLoaderClass) {
493                                if (logger.isDebugEnabled()) {
494                                        logger.debug(String.format(
495                                                        "Found explicit ContextLoader class [%s] for context configuration attributes %s",
496                                                        contextLoaderClass.getName(), configAttributes));
497                                }
498                                return contextLoaderClass;
499                        }
500                }
501                return null;
502        }
503
504        /**
505         * Get the {@link CacheAwareContextLoaderDelegate} to use for transparent
506         * interaction with the {@code ContextCache}.
507         * <p>The default implementation simply delegates to
508         * {@code getBootstrapContext().getCacheAwareContextLoaderDelegate()}.
509         * <p>Concrete subclasses may choose to override this method to return a custom
510         * {@code CacheAwareContextLoaderDelegate} implementation with custom
511         * {@link org.springframework.test.context.cache.ContextCache ContextCache} support.
512         * @return the context loader delegate (never {@code null})
513         */
514        protected CacheAwareContextLoaderDelegate getCacheAwareContextLoaderDelegate() {
515                return getBootstrapContext().getCacheAwareContextLoaderDelegate();
516        }
517
518        /**
519         * Determine the default {@link ContextLoader} {@linkplain Class class}
520         * to use for the supplied test class.
521         * <p>The class returned by this method will only be used if a {@code ContextLoader}
522         * class has not been explicitly declared via {@link ContextConfiguration#loader}.
523         * @param testClass the test class for which to retrieve the default
524         * {@code ContextLoader} class
525         * @return the default {@code ContextLoader} class for the supplied test class
526         * (never {@code null})
527         */
528        protected abstract Class<? extends ContextLoader> getDefaultContextLoaderClass(Class<?> testClass);
529
530        /**
531         * Process the supplied, newly instantiated {@link MergedContextConfiguration} instance.
532         * <p>The returned {@link MergedContextConfiguration} instance may be a wrapper
533         * around or a replacement for the original.
534         * <p>The default implementation simply returns the supplied instance unmodified.
535         * <p>Concrete subclasses may choose to return a specialized subclass of
536         * {@link MergedContextConfiguration} based on properties in the supplied instance.
537         * @param mergedConfig the {@code MergedContextConfiguration} to process; never {@code null}
538         * @return a fully initialized {@code MergedContextConfiguration}; never {@code null}
539         */
540        protected MergedContextConfiguration processMergedContextConfiguration(MergedContextConfiguration mergedConfig) {
541                return mergedConfig;
542        }
543
544
545        private static boolean areAllEmpty(Collection<?>... collections) {
546                return Arrays.stream(collections).allMatch(Collection::isEmpty);
547        }
548
549}