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 &mdash; 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}