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}