AbstractContextLoader.java 16.8 KB
Newer Older
A
Arjen Poutsma 已提交
1
/*
2
 * Copyright 2002-2014 the original author or authors.
A
Arjen Poutsma 已提交
3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.test.context.support;

19 20
import java.io.IOException;
import java.io.StringReader;
21
import java.util.ArrayList;
22
import java.util.HashMap;
23
import java.util.List;
24 25
import java.util.Map;
import java.util.Properties;
26 27
import java.util.Set;

28 29
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
30

31
import org.springframework.beans.BeanUtils;
A
Arjen Poutsma 已提交
32
import org.springframework.context.ApplicationContext;
33 34 35 36
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.GenericTypeResolver;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
37 38 39
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.PropertySource;
40
import org.springframework.core.io.ClassPathResource;
41 42
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.ResourcePropertySource;
43
import org.springframework.test.context.ContextConfigurationAttributes;
A
Arjen Poutsma 已提交
44
import org.springframework.test.context.ContextLoader;
45
import org.springframework.test.context.MergedContextConfiguration;
46
import org.springframework.test.context.SmartContextLoader;
47
import org.springframework.test.context.util.TestContextResourceUtils;
A
Arjen Poutsma 已提交
48 49 50 51 52 53
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.ResourceUtils;

/**
54
 * Abstract application context loader that provides a basis for all concrete
55
 * implementations of the {@link ContextLoader} SPI. Provides a
J
Juergen Hoeller 已提交
56
 * <em>Template Method</em> based approach for {@link #processLocations processing}
57
 * resource locations.
S
Stevo Slavic 已提交
58
 *
59
 * <p>As of Spring 3.1, {@code AbstractContextLoader} also provides a basis
60
 * for all concrete implementations of the {@link SmartContextLoader} SPI. For
S
Stevo Slavic 已提交
61 62 63
 * backwards compatibility with the {@code ContextLoader} SPI,
 * {@link #processContextConfiguration(ContextConfigurationAttributes)} delegates
 * to {@link #processLocations(Class, String...)}.
A
Arjen Poutsma 已提交
64 65 66
 *
 * @author Sam Brannen
 * @author Juergen Hoeller
67
 * @author Dave Syer
A
Arjen Poutsma 已提交
68 69
 * @since 2.5
 * @see #generateDefaultLocations
S
Sam Brannen 已提交
70
 * @see #getResourceSuffixes
A
Arjen Poutsma 已提交
71 72
 * @see #modifyLocations
 */
73 74
public abstract class AbstractContextLoader implements SmartContextLoader {

75
	private static final String[] EMPTY_STRING_ARRAY = new String[0];
J
Juergen Hoeller 已提交
76

77 78
	private static final String LINE_SEPARATOR = System.getProperty("line.separator");

J
Juergen Hoeller 已提交
79 80
	private static final Log logger = LogFactory.getLog(AbstractContextLoader.class);

81

82 83
	// --- SmartContextLoader -----------------------------------------------

84
	/**
85
	 * For backwards compatibility with the {@link ContextLoader} SPI, the
S
Stevo Slavic 已提交
86
	 * default implementation simply delegates to {@link #processLocations(Class, String...)},
87 88 89 90 91 92 93 94
	 * passing it the {@link ContextConfigurationAttributes#getDeclaringClass()
	 * declaring class} and {@link ContextConfigurationAttributes#getLocations()
	 * resource locations} retrieved from the supplied
	 * {@link ContextConfigurationAttributes configuration attributes}. The
	 * processed locations are then
	 * {@link ContextConfigurationAttributes#setLocations(String[]) set} in
	 * the supplied configuration attributes.
	 * <p>Can be overridden in subclasses &mdash; for example, to process
95
	 * annotated classes instead of resource locations.
96
	 * @since 3.1
S
Stevo Slavic 已提交
97
	 * @see #processLocations(Class, String...)
98
	 */
99
	@Override
100
	public void processContextConfiguration(ContextConfigurationAttributes configAttributes) {
101 102
		String[] processedLocations = processLocations(configAttributes.getDeclaringClass(),
			configAttributes.getLocations());
103 104
		configAttributes.setLocations(processedLocations);
	}
A
Arjen Poutsma 已提交
105

106
	/**
107
	 * Prepare the {@link ConfigurableApplicationContext} created by this
108 109 110 111
	 * {@code SmartContextLoader} <i>before</i> bean definitions are read.
	 * <p>The default implementation:
	 * <ul>
	 * <li>Sets the <em>active bean definition profiles</em> from the supplied
112
	 * {@code MergedContextConfiguration} in the
113 114 115 116 117 118 119 120
	 * {@link org.springframework.core.env.Environment Environment} of the
	 * context.</li>
	 * <li>Adds {@link PropertySource PropertySources} for all
	 * {@linkplain MergedContextConfiguration#getPropertySourceLocations()
	 * resource locations} and
	 * {@linkplain MergedContextConfiguration#getPropertySourceProperties()
	 * inlined properties} from the supplied {@code MergedContextConfiguration}
	 * to the {@code Environment} of the context.</li>
121
	 * <li>Determines what (if any) context initializer classes have been supplied
122 123
	 * via the {@code MergedContextConfiguration} and instantiates and
	 * {@linkplain ApplicationContextInitializer#initialize invokes} each with the
124
	 * given application context.</li>
125 126 127 128 129
	 * <ul>
	 * <li>Any {@code ApplicationContextInitializers} implementing
	 * {@link org.springframework.core.Ordered Ordered} or annotated with {@link
	 * org.springframework.core.annotation.Order @Order} will be sorted appropriately.</li>
	 * </ul>
130 131 132
	 * </ul>
	 * @param context the newly created application context
	 * @param mergedConfig the merged context configuration
J
Juergen Hoeller 已提交
133
	 * @since 3.2
134 135 136 137 138 139
	 * @see ApplicationContextInitializer#initialize(ConfigurableApplicationContext)
	 * @see #loadContext(MergedContextConfiguration)
	 * @see ConfigurableApplicationContext#setId
	 */
	protected void prepareContext(ConfigurableApplicationContext context, MergedContextConfiguration mergedConfig) {
		context.getEnvironment().setActiveProfiles(mergedConfig.getActiveProfiles());
140 141 142 143
		addResourcePropertySourcesToEnvironment(context, mergedConfig);
		addInlinedPropertiesToEnvironment(context, mergedConfig);
		invokeApplicationContextInitializers(context, mergedConfig);
	}
144

145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210
	/**
	 * @since 4.1
	 */
	private void addResourcePropertySourcesToEnvironment(ConfigurableApplicationContext context,
			MergedContextConfiguration mergedConfig) {
		try {
			ConfigurableEnvironment environment = context.getEnvironment();
			String[] locations = mergedConfig.getPropertySourceLocations();
			for (String location : locations) {
				String resolvedLocation = environment.resolveRequiredPlaceholders(location);
				Resource resource = context.getResource(resolvedLocation);
				ResourcePropertySource ps = new ResourcePropertySource(resource);
				environment.getPropertySources().addFirst(ps);
			}
		}
		catch (IOException e) {
			throw new IllegalStateException("Failed to add PropertySource to Environment", e);
		}
	}

	/**
	 * @since 4.1
	 */
	private void addInlinedPropertiesToEnvironment(ConfigurableApplicationContext context,
			MergedContextConfiguration mergedConfig) {
		String[] keyValuePairs = mergedConfig.getPropertySourceProperties();
		if (!ObjectUtils.isEmpty(keyValuePairs)) {
			String name = "test properties " + ObjectUtils.nullSafeToString(keyValuePairs);
			MapPropertySource ps = new MapPropertySource(name, extractEnvironmentProperties(keyValuePairs));
			context.getEnvironment().getPropertySources().addFirst(ps);
		}
	}

	/**
	 * Extract environment properties from the supplied key/value pairs.
	 * <p>Parsing of the key/value pairs is achieved by converting all pairs
	 * into a single <em>virtual</em> properties file in memory and delegating
	 * to {@link Properties#load(java.io.Reader)} to parse that virtual file.
	 * <p>This code has been adapted from Spring Boot's
	 * {@link org.springframework.boot.test.SpringApplicationContextLoader SpringApplicationContextLoader}.
	 * @since 4.1
	 */
	private Map<String, Object> extractEnvironmentProperties(String[] keyValuePairs) {
		StringBuilder sb = new StringBuilder();
		for (String keyValuePair : keyValuePairs) {
			sb.append(keyValuePair).append(LINE_SEPARATOR);
		}
		String content = sb.toString();
		Properties props = new Properties();
		try {
			props.load(new StringReader(content));
		}
		catch (IOException e) {
			throw new IllegalStateException("Failed to load test environment properties from: " + content, e);
		}

		Map<String, Object> properties = new HashMap<String, Object>();
		for (String name : props.stringPropertyNames()) {
			properties.put(name, props.getProperty(name));
		}
		return properties;
	}

	@SuppressWarnings("unchecked")
	private void invokeApplicationContextInitializers(ConfigurableApplicationContext context,
			MergedContextConfiguration mergedConfig) {
211
		Set<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>> initializerClasses = mergedConfig.getContextInitializerClasses();
J
Juergen Hoeller 已提交
212
		if (initializerClasses.isEmpty()) {
213 214 215 216
			// no ApplicationContextInitializers have been declared -> nothing to do
			return;
		}

217
		List<ApplicationContextInitializer<ConfigurableApplicationContext>> initializerInstances = new ArrayList<ApplicationContextInitializer<ConfigurableApplicationContext>>();
J
Juergen Hoeller 已提交
218
		Class<?> contextClass = context.getClass();
219 220 221

		for (Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>> initializerClass : initializerClasses) {
			Class<?> initializerContextClass = GenericTypeResolver.resolveTypeArgument(initializerClass,
222
				ApplicationContextInitializer.class);
223
			Assert.isAssignable(initializerContextClass, contextClass, String.format(
224 225 226 227
				"Could not add context initializer [%s] since its generic parameter [%s] "
						+ "is not assignable from the type of application context used by this "
						+ "context loader [%s]: ", initializerClass.getName(), initializerContextClass.getName(),
				contextClass.getName()));
228 229 230
			initializerInstances.add((ApplicationContextInitializer<ConfigurableApplicationContext>) BeanUtils.instantiateClass(initializerClass));
		}

J
Juergen Hoeller 已提交
231
		AnnotationAwareOrderComparator.sort(initializerInstances);
232 233 234 235 236
		for (ApplicationContextInitializer<ConfigurableApplicationContext> initializer : initializerInstances) {
			initializer.initialize(context);
		}
	}

237 238
	// --- ContextLoader -------------------------------------------------------

A
Arjen Poutsma 已提交
239
	/**
S
Sam Brannen 已提交
240 241 242 243 244 245 246 247
	 * If the supplied {@code locations} are {@code null} or <em>empty</em>
	 * and {@link #isGenerateDefaultLocations()} returns {@code true},
	 * default locations will be {@link #generateDefaultLocations(Class)
	 * generated} (i.e., detected) for the specified {@link Class class}
	 * and the configured {@linkplain #getResourceSuffixes() resource suffixes};
	 * otherwise, the supplied {@code locations} will be
	 * {@linkplain #modifyLocations modified} if necessary and returned.
	 *
A
Arjen Poutsma 已提交
248 249 250
	 * @param clazz the class with which the locations are associated: to be
	 * used when generating default locations
	 * @param locations the unmodified locations to use for loading the
251
	 * application context (can be {@code null} or empty)
252
	 * @return a processed array of application context resource locations
253
	 * @since 2.5
254
	 * @see #isGenerateDefaultLocations()
S
Stevo Slavic 已提交
255 256 257 258
	 * @see #generateDefaultLocations(Class)
	 * @see #modifyLocations(Class, String...)
	 * @see org.springframework.test.context.ContextLoader#processLocations(Class, String...)
	 * @see #processContextConfiguration(ContextConfigurationAttributes)
A
Arjen Poutsma 已提交
259
	 */
260
	@Override
A
Arjen Poutsma 已提交
261
	public final String[] processLocations(Class<?> clazz, String... locations) {
262 263
		return (ObjectUtils.isEmpty(locations) && isGenerateDefaultLocations()) ? generateDefaultLocations(clazz)
				: modifyLocations(clazz, locations);
A
Arjen Poutsma 已提交
264 265 266
	}

	/**
267
	 * Generate the default classpath resource locations array based on the
A
Arjen Poutsma 已提交
268
	 * supplied class.
S
Sam Brannen 已提交
269
	 *
270
	 * <p>For example, if the supplied class is {@code com.example.MyTest},
A
Arjen Poutsma 已提交
271
	 * the generated locations will contain a single string with a value of
S
Sam Brannen 已提交
272 273 274 275 276
	 * {@code "classpath:com/example/MyTest<suffix>"}, where {@code <suffix>}
	 * is the value of the first configured
	 * {@linkplain #getResourceSuffixes() resource suffix} for which the
	 * generated location actually exists in the classpath.
	 *
277
	 * <p>As of Spring 3.1, the implementation of this method adheres to the
S
Stevo Slavic 已提交
278
	 * contract defined in the {@link SmartContextLoader} SPI. Specifically,
279 280 281
	 * this method will <em>preemptively</em> verify that the generated default
	 * location actually exists. If it does not exist, this method will log a
	 * warning and return an empty array.
S
Sam Brannen 已提交
282
	 *
A
Arjen Poutsma 已提交
283 284
	 * <p>Subclasses can override this method to implement a different
	 * <em>default location generation</em> strategy.
S
Sam Brannen 已提交
285
	 *
A
Arjen Poutsma 已提交
286 287
	 * @param clazz the class for which the default locations are to be generated
	 * @return an array of default application context resource locations
288
	 * @since 2.5
S
Sam Brannen 已提交
289
	 * @see #getResourceSuffixes()
A
Arjen Poutsma 已提交
290 291 292
	 */
	protected String[] generateDefaultLocations(Class<?> clazz) {
		Assert.notNull(clazz, "Class must not be null");
293

S
Sam Brannen 已提交
294 295 296 297 298 299 300 301 302 303 304 305 306
		String[] suffixes = getResourceSuffixes();
		for (String suffix : suffixes) {
			Assert.hasText(suffix, "Resource suffix must not be empty");
			String resourcePath = ClassUtils.convertClassNameToResourcePath(clazz.getName()) + suffix;
			String prefixedResourcePath = ResourceUtils.CLASSPATH_URL_PREFIX + resourcePath;
			ClassPathResource classPathResource = new ClassPathResource(resourcePath);

			if (classPathResource.exists()) {
				if (logger.isInfoEnabled()) {
					logger.info(String.format("Detected default resource location \"%s\" for test class [%s]",
						prefixedResourcePath, clazz.getName()));
				}
				return new String[] { prefixedResourcePath };
307
			}
S
Sam Brannen 已提交
308 309
			else if (logger.isDebugEnabled()) {
				logger.debug(String.format("Did not detect default resource location for test class [%s]: "
310
						+ "%s does not exist", clazz.getName(), classPathResource));
J
Juergen Hoeller 已提交
311
			}
312
		}
S
Sam Brannen 已提交
313 314 315 316 317 318 319

		if (logger.isInfoEnabled()) {
			logger.info(String.format("Could not detect default resource locations for test class [%s]: "
					+ "no resource found for suffixes %s.", clazz.getName(), ObjectUtils.nullSafeToString(suffixes)));
		}

		return EMPTY_STRING_ARRAY;
A
Arjen Poutsma 已提交
320 321 322
	}

	/**
S
Sam Brannen 已提交
323
	 * Generate a modified version of the supplied locations array and return it.
324 325
	 * <p>The default implementation simply delegates to
	 * {@link TestContextResourceUtils#convertToClasspathResourcePaths}.
A
Arjen Poutsma 已提交
326 327 328 329 330
	 * <p>Subclasses can override this method to implement a different
	 * <em>location modification</em> strategy.
	 * @param clazz the class with which the locations are associated
	 * @param locations the resource locations to be modified
	 * @return an array of modified application context resource locations
331
	 * @since 2.5
A
Arjen Poutsma 已提交
332 333
	 */
	protected String[] modifyLocations(Class<?> clazz, String... locations) {
334
		return TestContextResourceUtils.convertToClasspathResourcePaths(clazz, locations);
A
Arjen Poutsma 已提交
335 336 337 338
	}

	/**
	 * Determine whether or not <em>default</em> resource locations should be
339 340
	 * generated if the {@code locations} provided to
	 * {@link #processLocations(Class, String...)} are {@code null} or empty.
341 342
	 * <p>As of Spring 3.1, the semantics of this method have been overloaded
	 * to include detection of either default resource locations or default
S
Stevo Slavic 已提交
343
	 * configuration classes. Consequently, this method can also be used to
344
	 * determine whether or not <em>default</em> configuration classes should be
345
	 * detected if the {@code classes} present in the
346
	 * {@link ContextConfigurationAttributes configuration attributes} supplied
S
Stevo Slavic 已提交
347
	 * to {@link #processContextConfiguration(ContextConfigurationAttributes)}
348
	 * are {@code null} or empty.
A
Arjen Poutsma 已提交
349
	 * <p>Can be overridden by subclasses to change the default behavior.
350
	 * @return always {@code true} by default
351
	 * @since 2.5
A
Arjen Poutsma 已提交
352 353 354 355 356 357
	 */
	protected boolean isGenerateDefaultLocations() {
		return true;
	}

	/**
S
Sam Brannen 已提交
358 359 360 361 362 363 364 365 366 367
	 * Get the suffix to append to {@link ApplicationContext} resource
	 * locations when detecting default locations.
	 *
	 * <p>Subclasses must provide an implementation of this method that
	 * returns a single suffix. Alternatively subclasses may provide a
	 * <em>no-op</em> implementation of this method and override
	 * {@link #getResourceSuffixes()} in order to provide multiple custom
	 * suffixes.
	 *
	 * @return the resource suffix; never {@code null} or empty
368
	 * @since 2.5
S
Stevo Slavic 已提交
369
	 * @see #generateDefaultLocations(Class)
S
Sam Brannen 已提交
370
	 * @see #getResourceSuffixes()
A
Arjen Poutsma 已提交
371 372 373
	 */
	protected abstract String getResourceSuffix();

S
Sam Brannen 已提交
374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389
	/**
	 * Get the suffixes to append to {@link ApplicationContext} resource
	 * locations when detecting default locations.
	 *
	 * <p>The default implementation simply wraps the value returned by
	 * {@link #getResourceSuffix()} in a single-element array, but this
	 * can be overridden by subclasses in order to support multiple suffixes.
	 *
	 * @return the resource suffixes; never {@code null} or empty
	 * @since 4.1
	 * @see #generateDefaultLocations(Class)
	 */
	protected String[] getResourceSuffixes() {
		return new String[] { getResourceSuffix() };
	}

A
Arjen Poutsma 已提交
390
}