001/* 002 * Copyright 2002-2017 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; 018 019import java.io.Serializable; 020import java.util.Arrays; 021import java.util.Collections; 022import java.util.LinkedHashSet; 023import java.util.Set; 024 025import org.springframework.context.ApplicationContext; 026import org.springframework.context.ApplicationContextInitializer; 027import org.springframework.core.style.ToStringCreator; 028import org.springframework.lang.Nullable; 029import org.springframework.util.Assert; 030import org.springframework.util.ObjectUtils; 031import org.springframework.util.StringUtils; 032 033/** 034 * {@code MergedContextConfiguration} encapsulates the <em>merged</em> 035 * context configuration declared on a test class and all of its superclasses 036 * via {@link ContextConfiguration @ContextConfiguration}, 037 * {@link ActiveProfiles @ActiveProfiles}, and 038 * {@link TestPropertySource @TestPropertySource}. 039 * 040 * <p>Merged context resource locations, annotated classes, active profiles, 041 * property resource locations, and in-lined properties represent all declared 042 * values in the test class hierarchy taking into consideration the semantics 043 * of the {@link ContextConfiguration#inheritLocations}, 044 * {@link ActiveProfiles#inheritProfiles}, 045 * {@link TestPropertySource#inheritLocations}, and 046 * {@link TestPropertySource#inheritProperties} flags. 047 * 048 * <p>A {@link SmartContextLoader} uses {@code MergedContextConfiguration} 049 * to load an {@link org.springframework.context.ApplicationContext ApplicationContext}. 050 * 051 * <p>{@code MergedContextConfiguration} is also used by the 052 * {@link org.springframework.test.context.cache.ContextCache ContextCache} 053 * as the key for caching an 054 * {@link org.springframework.context.ApplicationContext ApplicationContext} 055 * that was loaded using properties of this {@code MergedContextConfiguration}. 056 * 057 * @author Sam Brannen 058 * @author Phillip Webb 059 * @since 3.1 060 * @see ContextConfiguration 061 * @see ContextHierarchy 062 * @see ActiveProfiles 063 * @see TestPropertySource 064 * @see ContextConfigurationAttributes 065 * @see SmartContextLoader#loadContext(MergedContextConfiguration) 066 */ 067public class MergedContextConfiguration implements Serializable { 068 069 private static final long serialVersionUID = -3290560718464957422L; 070 071 private static final String[] EMPTY_STRING_ARRAY = new String[0]; 072 073 private static final Class<?>[] EMPTY_CLASS_ARRAY = new Class<?>[0]; 074 075 private static final Set<Class<? extends ApplicationContextInitializer<?>>> EMPTY_INITIALIZER_CLASSES = 076 Collections.<Class<? extends ApplicationContextInitializer<?>>> emptySet(); 077 078 private static final Set<ContextCustomizer> EMPTY_CONTEXT_CUSTOMIZERS = Collections.<ContextCustomizer> emptySet(); 079 080 081 private final Class<?> testClass; 082 083 private final String[] locations; 084 085 private final Class<?>[] classes; 086 087 private final Set<Class<? extends ApplicationContextInitializer<?>>> contextInitializerClasses; 088 089 private final String[] activeProfiles; 090 091 private final String[] propertySourceLocations; 092 093 private final String[] propertySourceProperties; 094 095 private final Set<ContextCustomizer> contextCustomizers; 096 097 private final ContextLoader contextLoader; 098 099 @Nullable 100 private final CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate; 101 102 @Nullable 103 private final MergedContextConfiguration parent; 104 105 106 /** 107 * Create a new {@code MergedContextConfiguration} instance for the 108 * supplied parameters. 109 * @param testClass the test class for which the configuration was merged 110 * @param locations the merged context resource locations 111 * @param classes the merged annotated classes 112 * @param activeProfiles the merged active bean definition profiles 113 * @param contextLoader the resolved {@code ContextLoader} 114 */ 115 public MergedContextConfiguration(Class<?> testClass, String[] locations, Class<?>[] classes, 116 String[] activeProfiles, ContextLoader contextLoader) { 117 118 this(testClass, locations, classes, null, activeProfiles, contextLoader); 119 } 120 121 /** 122 * Create a new {@code MergedContextConfiguration} instance for the 123 * supplied parameters. 124 * @param testClass the test class for which the configuration was merged 125 * @param locations the merged context resource locations 126 * @param classes the merged annotated classes 127 * @param contextInitializerClasses the merged context initializer classes 128 * @param activeProfiles the merged active bean definition profiles 129 * @param contextLoader the resolved {@code ContextLoader} 130 */ 131 public MergedContextConfiguration(Class<?> testClass, String[] locations, Class<?>[] classes, 132 @Nullable Set<Class<? extends ApplicationContextInitializer<?>>> contextInitializerClasses, 133 String[] activeProfiles, ContextLoader contextLoader) { 134 135 this(testClass, locations, classes, contextInitializerClasses, activeProfiles, contextLoader, null, null); 136 } 137 138 /** 139 * Create a new {@code MergedContextConfiguration} instance for the 140 * supplied parameters. 141 * @param testClass the test class for which the configuration was merged 142 * @param locations the merged context resource locations 143 * @param classes the merged annotated classes 144 * @param contextInitializerClasses the merged context initializer classes 145 * @param activeProfiles the merged active bean definition profiles 146 * @param contextLoader the resolved {@code ContextLoader} 147 * @param cacheAwareContextLoaderDelegate a cache-aware context loader 148 * delegate with which to retrieve the parent context 149 * @param parent the parent configuration or {@code null} if there is no parent 150 * @since 3.2.2 151 */ 152 public MergedContextConfiguration(Class<?> testClass, String[] locations, Class<?>[] classes, 153 @Nullable Set<Class<? extends ApplicationContextInitializer<?>>> contextInitializerClasses, 154 String[] activeProfiles, ContextLoader contextLoader, 155 @Nullable CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate, 156 @Nullable MergedContextConfiguration parent) { 157 158 this(testClass, locations, classes, contextInitializerClasses, activeProfiles, null, null, 159 contextLoader, cacheAwareContextLoaderDelegate, parent); 160 } 161 162 /** 163 * Create a new {@code MergedContextConfiguration} instance by copying 164 * all fields from the supplied {@code MergedContextConfiguration}. 165 * @since 4.1 166 */ 167 public MergedContextConfiguration(MergedContextConfiguration mergedConfig) { 168 this(mergedConfig.testClass, mergedConfig.locations, mergedConfig.classes, 169 mergedConfig.contextInitializerClasses, mergedConfig.activeProfiles, mergedConfig.propertySourceLocations, 170 mergedConfig.propertySourceProperties, mergedConfig.contextCustomizers, 171 mergedConfig.contextLoader, mergedConfig.cacheAwareContextLoaderDelegate, mergedConfig.parent); 172 } 173 174 /** 175 * Create a new {@code MergedContextConfiguration} instance for the 176 * supplied parameters. 177 * <p>If a {@code null} value is supplied for {@code locations}, 178 * {@code classes}, {@code activeProfiles}, {@code propertySourceLocations}, 179 * or {@code propertySourceProperties} an empty array will be stored instead. 180 * If a {@code null} value is supplied for the 181 * {@code contextInitializerClasses} an empty set will be stored instead. 182 * Furthermore, active profiles will be sorted, and duplicate profiles 183 * will be removed. 184 * @param testClass the test class for which the configuration was merged 185 * @param locations the merged context resource locations 186 * @param classes the merged annotated classes 187 * @param contextInitializerClasses the merged context initializer classes 188 * @param activeProfiles the merged active bean definition profiles 189 * @param propertySourceLocations the merged {@code PropertySource} locations 190 * @param propertySourceProperties the merged {@code PropertySource} properties 191 * @param contextLoader the resolved {@code ContextLoader} 192 * @param cacheAwareContextLoaderDelegate a cache-aware context loader 193 * delegate with which to retrieve the parent context 194 * @param parent the parent configuration or {@code null} if there is no parent 195 * @since 4.1 196 */ 197 public MergedContextConfiguration(Class<?> testClass, @Nullable String[] locations, @Nullable Class<?>[] classes, 198 @Nullable Set<Class<? extends ApplicationContextInitializer<?>>> contextInitializerClasses, 199 @Nullable String[] activeProfiles, @Nullable String[] propertySourceLocations, 200 @Nullable String[] propertySourceProperties, ContextLoader contextLoader, 201 @Nullable CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate, 202 @Nullable MergedContextConfiguration parent) { 203 204 this(testClass, locations, classes, contextInitializerClasses, activeProfiles, 205 propertySourceLocations, propertySourceProperties, 206 EMPTY_CONTEXT_CUSTOMIZERS, contextLoader, 207 cacheAwareContextLoaderDelegate, parent); 208 } 209 210 /** 211 * Create a new {@code MergedContextConfiguration} instance for the 212 * supplied parameters. 213 * <p>If a {@code null} value is supplied for {@code locations}, 214 * {@code classes}, {@code activeProfiles}, {@code propertySourceLocations}, 215 * or {@code propertySourceProperties} an empty array will be stored instead. 216 * If a {@code null} value is supplied for {@code contextInitializerClasses} 217 * or {@code contextCustomizers}, an empty set will be stored instead. 218 * Furthermore, active profiles will be sorted, and duplicate profiles 219 * will be removed. 220 * @param testClass the test class for which the configuration was merged 221 * @param locations the merged context resource locations 222 * @param classes the merged annotated classes 223 * @param contextInitializerClasses the merged context initializer classes 224 * @param activeProfiles the merged active bean definition profiles 225 * @param propertySourceLocations the merged {@code PropertySource} locations 226 * @param propertySourceProperties the merged {@code PropertySource} properties 227 * @param contextCustomizers the context customizers 228 * @param contextLoader the resolved {@code ContextLoader} 229 * @param cacheAwareContextLoaderDelegate a cache-aware context loader 230 * delegate with which to retrieve the parent context 231 * @param parent the parent configuration or {@code null} if there is no parent 232 * @since 4.3 233 */ 234 public MergedContextConfiguration(Class<?> testClass, @Nullable String[] locations, @Nullable Class<?>[] classes, 235 @Nullable Set<Class<? extends ApplicationContextInitializer<?>>> contextInitializerClasses, 236 @Nullable String[] activeProfiles, @Nullable String[] propertySourceLocations, 237 @Nullable String[] propertySourceProperties, @Nullable Set<ContextCustomizer> contextCustomizers, 238 ContextLoader contextLoader, @Nullable CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate, 239 @Nullable MergedContextConfiguration parent) { 240 241 this.testClass = testClass; 242 this.locations = processStrings(locations); 243 this.classes = processClasses(classes); 244 this.contextInitializerClasses = processContextInitializerClasses(contextInitializerClasses); 245 this.activeProfiles = processActiveProfiles(activeProfiles); 246 this.propertySourceLocations = processStrings(propertySourceLocations); 247 this.propertySourceProperties = processStrings(propertySourceProperties); 248 this.contextCustomizers = processContextCustomizers(contextCustomizers); 249 this.contextLoader = contextLoader; 250 this.cacheAwareContextLoaderDelegate = cacheAwareContextLoaderDelegate; 251 this.parent = parent; 252 } 253 254 255 /** 256 * Get the {@linkplain Class test class} associated with this 257 * {@code MergedContextConfiguration}. 258 */ 259 public Class<?> getTestClass() { 260 return this.testClass; 261 } 262 263 /** 264 * Get the merged resource locations for {@code ApplicationContext} 265 * configuration files for the {@linkplain #getTestClass() test class}. 266 * <p>Context resource locations typically represent XML configuration 267 * files or Groovy scripts. 268 */ 269 public String[] getLocations() { 270 return this.locations; 271 } 272 273 /** 274 * Get the merged annotated classes for the {@linkplain #getTestClass() test class}. 275 */ 276 public Class<?>[] getClasses() { 277 return this.classes; 278 } 279 280 /** 281 * Determine if this {@code MergedContextConfiguration} instance has 282 * path-based context resource locations. 283 * @return {@code true} if the {@link #getLocations() locations} array is not empty 284 * @since 4.0.4 285 * @see #hasResources() 286 * @see #hasClasses() 287 */ 288 public boolean hasLocations() { 289 return !ObjectUtils.isEmpty(getLocations()); 290 } 291 292 /** 293 * Determine if this {@code MergedContextConfiguration} instance has 294 * class-based resources. 295 * @return {@code true} if the {@link #getClasses() classes} array is not empty 296 * @since 4.0.4 297 * @see #hasResources() 298 * @see #hasLocations() 299 */ 300 public boolean hasClasses() { 301 return !ObjectUtils.isEmpty(getClasses()); 302 } 303 304 /** 305 * Determine if this {@code MergedContextConfiguration} instance has 306 * either path-based context resource locations or class-based resources. 307 * @return {@code true} if either the {@link #getLocations() locations} 308 * or the {@link #getClasses() classes} array is not empty 309 * @since 4.0.4 310 * @see #hasLocations() 311 * @see #hasClasses() 312 */ 313 public boolean hasResources() { 314 return (hasLocations() || hasClasses()); 315 } 316 317 /** 318 * Get the merged {@code ApplicationContextInitializer} classes for the 319 * {@linkplain #getTestClass() test class}. 320 */ 321 public Set<Class<? extends ApplicationContextInitializer<?>>> getContextInitializerClasses() { 322 return this.contextInitializerClasses; 323 } 324 325 /** 326 * Get the merged active bean definition profiles for the 327 * {@linkplain #getTestClass() test class}. 328 * @see ActiveProfiles 329 */ 330 public String[] getActiveProfiles() { 331 return this.activeProfiles; 332 } 333 334 /** 335 * Get the merged resource locations for test {@code PropertySources} 336 * for the {@linkplain #getTestClass() test class}. 337 * @see TestPropertySource#locations 338 * @see java.util.Properties 339 */ 340 public String[] getPropertySourceLocations() { 341 return this.propertySourceLocations; 342 } 343 344 /** 345 * Get the merged test {@code PropertySource} properties for the 346 * {@linkplain #getTestClass() test class}. 347 * <p>Properties will be loaded into the {@code Environment}'s set of 348 * {@code PropertySources}. 349 * @see TestPropertySource#properties 350 * @see java.util.Properties 351 */ 352 public String[] getPropertySourceProperties() { 353 return this.propertySourceProperties; 354 } 355 356 /** 357 * Get the merged {@link ContextCustomizer ContextCustomizers} that will be applied 358 * when the application context is loaded. 359 */ 360 public Set<ContextCustomizer> getContextCustomizers() { 361 return this.contextCustomizers; 362 } 363 364 /** 365 * Get the resolved {@link ContextLoader} for the {@linkplain #getTestClass() test class}. 366 */ 367 public ContextLoader getContextLoader() { 368 return this.contextLoader; 369 } 370 371 /** 372 * Get the {@link MergedContextConfiguration} for the parent application context 373 * in a context hierarchy. 374 * @return the parent configuration or {@code null} if there is no parent 375 * @see #getParentApplicationContext() 376 * @since 3.2.2 377 */ 378 @Nullable 379 public MergedContextConfiguration getParent() { 380 return this.parent; 381 } 382 383 /** 384 * Get the parent {@link ApplicationContext} for the context defined by this 385 * {@code MergedContextConfiguration} from the context cache. 386 * <p>If the parent context has not yet been loaded, it will be loaded, stored 387 * in the cache, and then returned. 388 * @return the parent {@code ApplicationContext} or {@code null} if there is no parent 389 * @see #getParent() 390 * @since 3.2.2 391 */ 392 @Nullable 393 public ApplicationContext getParentApplicationContext() { 394 if (this.parent == null) { 395 return null; 396 } 397 Assert.state(this.cacheAwareContextLoaderDelegate != null, 398 "Cannot retrieve a parent application context without access to the CacheAwareContextLoaderDelegate"); 399 return this.cacheAwareContextLoaderDelegate.loadContext(this.parent); 400 } 401 402 403 /** 404 * Determine if the supplied object is equal to this {@code MergedContextConfiguration} 405 * instance by comparing both object's {@linkplain #getLocations() locations}, 406 * {@linkplain #getClasses() annotated classes}, 407 * {@linkplain #getContextInitializerClasses() context initializer classes}, 408 * {@linkplain #getActiveProfiles() active profiles}, 409 * {@linkplain #getPropertySourceLocations() property source locations}, 410 * {@linkplain #getPropertySourceProperties() property source properties}, 411 * {@linkplain #getParent() parents}, and the fully qualified names of their 412 * {@link #getContextLoader() ContextLoaders}. 413 */ 414 @Override 415 public boolean equals(@Nullable Object other) { 416 if (this == other) { 417 return true; 418 } 419 if (other == null || other.getClass() != getClass()) { 420 return false; 421 } 422 423 MergedContextConfiguration otherConfig = (MergedContextConfiguration) other; 424 if (!Arrays.equals(this.locations, otherConfig.locations)) { 425 return false; 426 } 427 if (!Arrays.equals(this.classes, otherConfig.classes)) { 428 return false; 429 } 430 if (!this.contextInitializerClasses.equals(otherConfig.contextInitializerClasses)) { 431 return false; 432 } 433 if (!Arrays.equals(this.activeProfiles, otherConfig.activeProfiles)) { 434 return false; 435 } 436 if (!Arrays.equals(this.propertySourceLocations, otherConfig.propertySourceLocations)) { 437 return false; 438 } 439 if (!Arrays.equals(this.propertySourceProperties, otherConfig.propertySourceProperties)) { 440 return false; 441 } 442 if (!this.contextCustomizers.equals(otherConfig.contextCustomizers)) { 443 return false; 444 } 445 446 if (this.parent == null) { 447 if (otherConfig.parent != null) { 448 return false; 449 } 450 } 451 else if (!this.parent.equals(otherConfig.parent)) { 452 return false; 453 } 454 455 if (!nullSafeClassName(this.contextLoader).equals(nullSafeClassName(otherConfig.contextLoader))) { 456 return false; 457 } 458 459 return true; 460 } 461 462 /** 463 * Generate a unique hash code for all properties of this 464 * {@code MergedContextConfiguration} excluding the 465 * {@linkplain #getTestClass() test class}. 466 */ 467 @Override 468 public int hashCode() { 469 int result = Arrays.hashCode(this.locations); 470 result = 31 * result + Arrays.hashCode(this.classes); 471 result = 31 * result + this.contextInitializerClasses.hashCode(); 472 result = 31 * result + Arrays.hashCode(this.activeProfiles); 473 result = 31 * result + Arrays.hashCode(this.propertySourceLocations); 474 result = 31 * result + Arrays.hashCode(this.propertySourceProperties); 475 result = 31 * result + this.contextCustomizers.hashCode(); 476 result = 31 * result + (this.parent != null ? this.parent.hashCode() : 0); 477 result = 31 * result + nullSafeClassName(this.contextLoader).hashCode(); 478 return result; 479 } 480 481 /** 482 * Provide a String representation of the {@linkplain #getTestClass() test class}, 483 * {@linkplain #getLocations() locations}, {@linkplain #getClasses() annotated classes}, 484 * {@linkplain #getContextInitializerClasses() context initializer classes}, 485 * {@linkplain #getActiveProfiles() active profiles}, 486 * {@linkplain #getPropertySourceLocations() property source locations}, 487 * {@linkplain #getPropertySourceProperties() property source properties}, 488 * {@linkplain #getContextCustomizers() context customizers}, 489 * the name of the {@link #getContextLoader() ContextLoader}, and the 490 * {@linkplain #getParent() parent configuration}. 491 */ 492 @Override 493 public String toString() { 494 return new ToStringCreator(this) 495 .append("testClass", this.testClass) 496 .append("locations", ObjectUtils.nullSafeToString(this.locations)) 497 .append("classes", ObjectUtils.nullSafeToString(this.classes)) 498 .append("contextInitializerClasses", ObjectUtils.nullSafeToString(this.contextInitializerClasses)) 499 .append("activeProfiles", ObjectUtils.nullSafeToString(this.activeProfiles)) 500 .append("propertySourceLocations", ObjectUtils.nullSafeToString(this.propertySourceLocations)) 501 .append("propertySourceProperties", ObjectUtils.nullSafeToString(this.propertySourceProperties)) 502 .append("contextCustomizers", this.contextCustomizers) 503 .append("contextLoader", nullSafeClassName(this.contextLoader)) 504 .append("parent", this.parent) 505 .toString(); 506 } 507 508 509 private static String[] processStrings(@Nullable String[] array) { 510 return (array != null ? array : EMPTY_STRING_ARRAY); 511 } 512 513 private static Class<?>[] processClasses(@Nullable Class<?>[] classes) { 514 return (classes != null ? classes : EMPTY_CLASS_ARRAY); 515 } 516 517 private static Set<Class<? extends ApplicationContextInitializer<?>>> processContextInitializerClasses( 518 @Nullable Set<Class<? extends ApplicationContextInitializer<?>>> contextInitializerClasses) { 519 520 return (contextInitializerClasses != null ? 521 Collections.unmodifiableSet(contextInitializerClasses) : EMPTY_INITIALIZER_CLASSES); 522 } 523 524 private static Set<ContextCustomizer> processContextCustomizers( 525 @Nullable Set<ContextCustomizer> contextCustomizers) { 526 527 return (contextCustomizers != null ? 528 Collections.unmodifiableSet(contextCustomizers) : EMPTY_CONTEXT_CUSTOMIZERS); 529 } 530 531 private static String[] processActiveProfiles(@Nullable String[] activeProfiles) { 532 if (activeProfiles == null) { 533 return EMPTY_STRING_ARRAY; 534 } 535 536 // Active profiles must be unique 537 Set<String> profilesSet = new LinkedHashSet<>(Arrays.asList(activeProfiles)); 538 return StringUtils.toStringArray(profilesSet); 539 } 540 541 /** 542 * Generate a null-safe {@link String} representation of the supplied 543 * {@link ContextLoader} based solely on the fully qualified name of the 544 * loader or "null" if the supplied loader is {@code null}. 545 */ 546 protected static String nullSafeClassName(@Nullable ContextLoader contextLoader) { 547 return (contextLoader != null ? contextLoader.getClass().getName() : "null"); 548 } 549 550}