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.List; 021import java.util.Set; 022 023import org.apache.commons.logging.Log; 024import org.apache.commons.logging.LogFactory; 025 026import org.springframework.beans.BeanUtils; 027import org.springframework.context.ApplicationContext; 028import org.springframework.context.ApplicationContextException; 029import org.springframework.context.ApplicationContextInitializer; 030import org.springframework.context.ConfigurableApplicationContext; 031import org.springframework.core.GenericTypeResolver; 032import org.springframework.core.annotation.AnnotationAwareOrderComparator; 033import org.springframework.core.env.PropertySource; 034import org.springframework.core.io.ClassPathResource; 035import org.springframework.test.context.ContextConfigurationAttributes; 036import org.springframework.test.context.ContextCustomizer; 037import org.springframework.test.context.ContextLoader; 038import org.springframework.test.context.MergedContextConfiguration; 039import org.springframework.test.context.SmartContextLoader; 040import org.springframework.test.context.util.TestContextResourceUtils; 041import org.springframework.util.Assert; 042import org.springframework.util.ClassUtils; 043import org.springframework.util.ObjectUtils; 044import org.springframework.util.ResourceUtils; 045 046/** 047 * Abstract application context loader that provides a basis for all concrete 048 * implementations of the {@link ContextLoader} SPI. Provides a 049 * <em>Template Method</em> based approach for {@link #processLocations processing} 050 * resource locations. 051 * 052 * <p>As of Spring 3.1, {@code AbstractContextLoader} also provides a basis 053 * for all concrete implementations of the {@link SmartContextLoader} SPI. For 054 * backwards compatibility with the {@code ContextLoader} SPI, 055 * {@link #processContextConfiguration(ContextConfigurationAttributes)} delegates 056 * to {@link #processLocations(Class, String...)}. 057 * 058 * @author Sam Brannen 059 * @author Juergen Hoeller 060 * @author Phillip Webb 061 * @since 2.5 062 * @see #generateDefaultLocations 063 * @see #getResourceSuffixes 064 * @see #modifyLocations 065 * @see #prepareContext 066 * @see #customizeContext 067 */ 068public abstract class AbstractContextLoader implements SmartContextLoader { 069 070 private static final String[] EMPTY_STRING_ARRAY = new String[0]; 071 072 private static final Log logger = LogFactory.getLog(AbstractContextLoader.class); 073 074 075 // SmartContextLoader 076 077 /** 078 * For backwards compatibility with the {@link ContextLoader} SPI, the 079 * default implementation simply delegates to {@link #processLocations(Class, String...)}, 080 * passing it the {@link ContextConfigurationAttributes#getDeclaringClass() 081 * declaring class} and {@link ContextConfigurationAttributes#getLocations() 082 * resource locations} retrieved from the supplied 083 * {@link ContextConfigurationAttributes configuration attributes}. The 084 * processed locations are then 085 * {@link ContextConfigurationAttributes#setLocations(String[]) set} in 086 * the supplied configuration attributes. 087 * <p>Can be overridden in subclasses — for example, to process 088 * annotated classes instead of resource locations. 089 * @since 3.1 090 * @see #processLocations(Class, String...) 091 */ 092 @Override 093 public void processContextConfiguration(ContextConfigurationAttributes configAttributes) { 094 String[] processedLocations = 095 processLocations(configAttributes.getDeclaringClass(), configAttributes.getLocations()); 096 configAttributes.setLocations(processedLocations); 097 } 098 099 /** 100 * Prepare the {@link ConfigurableApplicationContext} created by this 101 * {@code SmartContextLoader} <i>before</i> bean definitions are read. 102 * <p>The default implementation: 103 * <ul> 104 * <li>Sets the <em>active bean definition profiles</em> from the supplied 105 * {@code MergedContextConfiguration} in the 106 * {@link org.springframework.core.env.Environment Environment} of the 107 * context.</li> 108 * <li>Adds {@link PropertySource PropertySources} for all 109 * {@linkplain MergedContextConfiguration#getPropertySourceLocations() 110 * resource locations} and 111 * {@linkplain MergedContextConfiguration#getPropertySourceProperties() 112 * inlined properties} from the supplied {@code MergedContextConfiguration} 113 * to the {@code Environment} of the context.</li> 114 * <li>Determines what (if any) context initializer classes have been supplied 115 * via the {@code MergedContextConfiguration} and instantiates and 116 * {@linkplain ApplicationContextInitializer#initialize invokes} each with the 117 * given application context. 118 * <ul> 119 * <li>Any {@code ApplicationContextInitializers} implementing 120 * {@link org.springframework.core.Ordered Ordered} or annotated with {@link 121 * org.springframework.core.annotation.Order @Order} will be sorted appropriately.</li> 122 * </ul> 123 * </li> 124 * </ul> 125 * @param context the newly created application context 126 * @param mergedConfig the merged context configuration 127 * @since 3.2 128 * @see TestPropertySourceUtils#addPropertiesFilesToEnvironment 129 * @see TestPropertySourceUtils#addInlinedPropertiesToEnvironment 130 * @see ApplicationContextInitializer#initialize(ConfigurableApplicationContext) 131 * @see #loadContext(MergedContextConfiguration) 132 * @see ConfigurableApplicationContext#setId 133 */ 134 protected void prepareContext(ConfigurableApplicationContext context, MergedContextConfiguration mergedConfig) { 135 context.getEnvironment().setActiveProfiles(mergedConfig.getActiveProfiles()); 136 TestPropertySourceUtils.addPropertiesFilesToEnvironment(context, mergedConfig.getPropertySourceLocations()); 137 TestPropertySourceUtils.addInlinedPropertiesToEnvironment(context, mergedConfig.getPropertySourceProperties()); 138 invokeApplicationContextInitializers(context, mergedConfig); 139 } 140 141 @SuppressWarnings("unchecked") 142 private void invokeApplicationContextInitializers(ConfigurableApplicationContext context, 143 MergedContextConfiguration mergedConfig) { 144 145 Set<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>> initializerClasses = 146 mergedConfig.getContextInitializerClasses(); 147 if (initializerClasses.isEmpty()) { 148 // no ApplicationContextInitializers have been declared -> nothing to do 149 return; 150 } 151 152 List<ApplicationContextInitializer<ConfigurableApplicationContext>> initializerInstances = new ArrayList<ApplicationContextInitializer<ConfigurableApplicationContext>>(); 153 Class<?> contextClass = context.getClass(); 154 155 for (Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>> initializerClass : initializerClasses) { 156 Class<?> initializerContextClass = 157 GenericTypeResolver.resolveTypeArgument(initializerClass, ApplicationContextInitializer.class); 158 if (initializerContextClass != null && !initializerContextClass.isInstance(context)) { 159 throw new ApplicationContextException(String.format( 160 "Could not apply context initializer [%s] since its generic parameter [%s] " + 161 "is not assignable from the type of application context used by this " + 162 "context loader: [%s]", initializerClass.getName(), initializerContextClass.getName(), 163 contextClass.getName())); 164 } 165 initializerInstances.add((ApplicationContextInitializer<ConfigurableApplicationContext>) BeanUtils.instantiateClass(initializerClass)); 166 } 167 168 AnnotationAwareOrderComparator.sort(initializerInstances); 169 for (ApplicationContextInitializer<ConfigurableApplicationContext> initializer : initializerInstances) { 170 initializer.initialize(context); 171 } 172 } 173 174 /** 175 * Customize the {@link ConfigurableApplicationContext} created by this 176 * {@code ContextLoader} <em>after</em> bean definitions have been loaded 177 * into the context but <em>before</em> the context has been refreshed. 178 * <p>The default implementation delegates to all 179 * {@link MergedContextConfiguration#getContextCustomizers context customizers} 180 * that have been registered with the supplied {@code mergedConfig}. 181 * @param context the newly created application context 182 * @param mergedConfig the merged context configuration 183 * @since 4.3 184 */ 185 protected void customizeContext(ConfigurableApplicationContext context, MergedContextConfiguration mergedConfig) { 186 for (ContextCustomizer contextCustomizer : mergedConfig.getContextCustomizers()) { 187 contextCustomizer.customizeContext(context, mergedConfig); 188 } 189 } 190 191 192 // ContextLoader 193 194 /** 195 * If the supplied {@code locations} are {@code null} or <em>empty</em> 196 * and {@link #isGenerateDefaultLocations()} returns {@code true}, 197 * default locations will be {@link #generateDefaultLocations(Class) 198 * generated} (i.e., detected) for the specified {@link Class class} 199 * and the configured {@linkplain #getResourceSuffixes() resource suffixes}; 200 * otherwise, the supplied {@code locations} will be 201 * {@linkplain #modifyLocations modified} if necessary and returned. 202 * @param clazz the class with which the locations are associated: to be 203 * used when generating default locations 204 * @param locations the unmodified locations to use for loading the 205 * application context (can be {@code null} or empty) 206 * @return a processed array of application context resource locations 207 * @since 2.5 208 * @see #isGenerateDefaultLocations() 209 * @see #generateDefaultLocations(Class) 210 * @see #modifyLocations(Class, String...) 211 * @see org.springframework.test.context.ContextLoader#processLocations(Class, String...) 212 * @see #processContextConfiguration(ContextConfigurationAttributes) 213 */ 214 @Override 215 public final String[] processLocations(Class<?> clazz, String... locations) { 216 return (ObjectUtils.isEmpty(locations) && isGenerateDefaultLocations()) ? 217 generateDefaultLocations(clazz) : modifyLocations(clazz, locations); 218 } 219 220 /** 221 * Generate the default classpath resource locations array based on the 222 * supplied class. 223 * <p>For example, if the supplied class is {@code com.example.MyTest}, 224 * the generated locations will contain a single string with a value of 225 * {@code "classpath:com/example/MyTest<suffix>"}, where {@code <suffix>} 226 * is the value of the first configured 227 * {@linkplain #getResourceSuffixes() resource suffix} for which the 228 * generated location actually exists in the classpath. 229 * <p>As of Spring 3.1, the implementation of this method adheres to the 230 * contract defined in the {@link SmartContextLoader} SPI. Specifically, 231 * this method will <em>preemptively</em> verify that the generated default 232 * location actually exists. If it does not exist, this method will log a 233 * warning and return an empty array. 234 * <p>Subclasses can override this method to implement a different 235 * <em>default location generation</em> strategy. 236 * @param clazz the class for which the default locations are to be generated 237 * @return an array of default application context resource locations 238 * @since 2.5 239 * @see #getResourceSuffixes() 240 */ 241 protected String[] generateDefaultLocations(Class<?> clazz) { 242 Assert.notNull(clazz, "Class must not be null"); 243 244 String[] suffixes = getResourceSuffixes(); 245 for (String suffix : suffixes) { 246 Assert.hasText(suffix, "Resource suffix must not be empty"); 247 String resourcePath = ClassUtils.convertClassNameToResourcePath(clazz.getName()) + suffix; 248 String prefixedResourcePath = ResourceUtils.CLASSPATH_URL_PREFIX + resourcePath; 249 ClassPathResource classPathResource = new ClassPathResource(resourcePath); 250 if (classPathResource.exists()) { 251 if (logger.isInfoEnabled()) { 252 logger.info(String.format("Detected default resource location \"%s\" for test class [%s]", 253 prefixedResourcePath, clazz.getName())); 254 } 255 return new String[] {prefixedResourcePath}; 256 } 257 else if (logger.isDebugEnabled()) { 258 logger.debug(String.format("Did not detect default resource location for test class [%s]: " + 259 "%s does not exist", clazz.getName(), classPathResource)); 260 } 261 } 262 263 if (logger.isInfoEnabled()) { 264 logger.info(String.format("Could not detect default resource locations for test class [%s]: " + 265 "no resource found for suffixes %s.", clazz.getName(), ObjectUtils.nullSafeToString(suffixes))); 266 } 267 268 return EMPTY_STRING_ARRAY; 269 } 270 271 /** 272 * Generate a modified version of the supplied locations array and return it. 273 * <p>The default implementation simply delegates to 274 * {@link TestContextResourceUtils#convertToClasspathResourcePaths}. 275 * <p>Subclasses can override this method to implement a different 276 * <em>location modification</em> strategy. 277 * @param clazz the class with which the locations are associated 278 * @param locations the resource locations to be modified 279 * @return an array of modified application context resource locations 280 * @since 2.5 281 */ 282 protected String[] modifyLocations(Class<?> clazz, String... locations) { 283 return TestContextResourceUtils.convertToClasspathResourcePaths(clazz, locations); 284 } 285 286 /** 287 * Determine whether or not <em>default</em> resource locations should be 288 * generated if the {@code locations} provided to 289 * {@link #processLocations(Class, String...)} are {@code null} or empty. 290 * <p>As of Spring 3.1, the semantics of this method have been overloaded 291 * to include detection of either default resource locations or default 292 * configuration classes. Consequently, this method can also be used to 293 * determine whether or not <em>default</em> configuration classes should be 294 * detected if the {@code classes} present in the 295 * {@link ContextConfigurationAttributes configuration attributes} supplied 296 * to {@link #processContextConfiguration(ContextConfigurationAttributes)} 297 * are {@code null} or empty. 298 * <p>Can be overridden by subclasses to change the default behavior. 299 * @return always {@code true} by default 300 * @since 2.5 301 */ 302 protected boolean isGenerateDefaultLocations() { 303 return true; 304 } 305 306 /** 307 * Get the suffixes to append to {@link ApplicationContext} resource locations 308 * when detecting default locations. 309 * <p>The default implementation simply wraps the value returned by 310 * {@link #getResourceSuffix()} in a single-element array, but this 311 * can be overridden by subclasses in order to support multiple suffixes. 312 * @return the resource suffixes; never {@code null} or empty 313 * @since 4.1 314 * @see #generateDefaultLocations(Class) 315 */ 316 protected String[] getResourceSuffixes() { 317 return new String[] {getResourceSuffix()}; 318 } 319 320 /** 321 * Get the suffix to append to {@link ApplicationContext} resource locations 322 * when detecting default locations. 323 * <p>Subclasses must provide an implementation of this method that returns 324 * a single suffix. Alternatively subclasses may provide a <em>no-op</em> 325 * implementation of this method and override {@link #getResourceSuffixes()} 326 * in order to provide multiple custom suffixes. 327 * @return the resource suffix; never {@code null} or empty 328 * @since 2.5 329 * @see #generateDefaultLocations(Class) 330 * @see #getResourceSuffixes() 331 */ 332 protected abstract String getResourceSuffix(); 333 334}