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 org.apache.commons.logging.Log;
020import org.apache.commons.logging.LogFactory;
021
022import org.springframework.context.ApplicationContext;
023import org.springframework.lang.Nullable;
024import org.springframework.test.context.ContextConfiguration;
025import org.springframework.test.context.ContextConfigurationAttributes;
026import org.springframework.test.context.ContextLoader;
027import org.springframework.test.context.MergedContextConfiguration;
028import org.springframework.test.context.SmartContextLoader;
029import org.springframework.util.Assert;
030
031/**
032 * {@code AbstractDelegatingSmartContextLoader} serves as an abstract base class
033 * for implementations of the {@link SmartContextLoader} SPI that delegate to a
034 * set of <em>candidate</em> SmartContextLoaders (i.e., one that supports XML
035 * configuration files or Groovy scripts and one that supports annotated classes)
036 * to determine which context loader is appropriate for a given test class's
037 * configuration. Each candidate is given a chance to
038 * {@linkplain #processContextConfiguration process} the
039 * {@link ContextConfigurationAttributes} for each class in the test class hierarchy
040 * that is annotated with {@link ContextConfiguration @ContextConfiguration}, and
041 * the candidate that supports the merged, processed configuration will be used to
042 * actually {@linkplain #loadContext load} the context.
043 *
044 * <p>Any reference to an <em>XML-based loader</em> can be interpreted to mean
045 * a context loader that supports only XML configuration files or one that
046 * supports both XML configuration files and Groovy scripts simultaneously.
047 *
048 * <p>Placing an empty {@code @ContextConfiguration} annotation on a test class signals
049 * that default resource locations (e.g., XML configuration files or Groovy scripts)
050 * or default
051 * {@linkplain org.springframework.context.annotation.Configuration configuration classes}
052 * should be detected. Furthermore, if a specific {@link ContextLoader} or
053 * {@link SmartContextLoader} is not explicitly declared via
054 * {@code @ContextConfiguration}, a concrete subclass of
055 * {@code AbstractDelegatingSmartContextLoader} will be used as the default loader,
056 * thus providing automatic support for either path-based resource locations
057 * (e.g., XML configuration files and Groovy scripts) or annotated classes,
058 * but not both simultaneously.
059 *
060 * <p>As of Spring 3.2, a test class may optionally declare neither path-based
061 * resource locations nor annotated classes and instead declare only {@linkplain
062 * ContextConfiguration#initializers application context initializers}. In such
063 * cases, an attempt will still be made to detect defaults, but their absence will
064 * not result in an exception.
065 *
066 * @author Sam Brannen
067 * @author Phillip Webb
068 * @since 3.2
069 * @see SmartContextLoader
070 */
071public abstract class AbstractDelegatingSmartContextLoader implements SmartContextLoader {
072
073        private static final Log logger = LogFactory.getLog(AbstractDelegatingSmartContextLoader.class);
074
075
076        /**
077         * Get the delegate {@code SmartContextLoader} that supports XML configuration
078         * files and/or Groovy scripts.
079         */
080        protected abstract SmartContextLoader getXmlLoader();
081
082        /**
083         * Get the delegate {@code SmartContextLoader} that supports annotated classes.
084         */
085        protected abstract SmartContextLoader getAnnotationConfigLoader();
086
087
088        // ContextLoader
089
090        /**
091         * {@code AbstractDelegatingSmartContextLoader} does not support the
092         * {@link ContextLoader#processLocations(Class, String...)} method. Call
093         * {@link #processContextConfiguration(ContextConfigurationAttributes)} instead.
094         * @throws UnsupportedOperationException in this implementation
095         */
096        @Override
097        public final String[] processLocations(Class<?> clazz, @Nullable String... locations) {
098                throw new UnsupportedOperationException(
099                                "DelegatingSmartContextLoaders do not support the ContextLoader SPI. " +
100                                                "Call processContextConfiguration(ContextConfigurationAttributes) instead.");
101        }
102
103        /**
104         * {@code AbstractDelegatingSmartContextLoader} does not support the
105         * {@link ContextLoader#loadContext(String...) } method. Call
106         * {@link #loadContext(MergedContextConfiguration)} instead.
107         * @throws UnsupportedOperationException in this implementation
108         */
109        @Override
110        public final ApplicationContext loadContext(String... locations) throws Exception {
111                throw new UnsupportedOperationException(
112                                "DelegatingSmartContextLoaders do not support the ContextLoader SPI. " +
113                                                "Call loadContext(MergedContextConfiguration) instead.");
114        }
115
116
117        // SmartContextLoader
118
119        /**
120         * Delegates to candidate {@code SmartContextLoaders} to process the supplied
121         * {@link ContextConfigurationAttributes}.
122         * <p>Delegation is based on explicit knowledge of the implementations of the
123         * default loaders for {@linkplain #getXmlLoader() XML configuration files and
124         * Groovy scripts} and {@linkplain #getAnnotationConfigLoader() annotated classes}.
125         * Specifically, the delegation algorithm is as follows:
126         * <ul>
127         * <li>If the resource locations or annotated classes in the supplied
128         * {@code ContextConfigurationAttributes} are not empty, the appropriate
129         * candidate loader will be allowed to process the configuration <em>as is</em>,
130         * without any checks for detection of defaults.</li>
131         * <li>Otherwise, the XML-based loader will be allowed to process
132         * the configuration in order to detect default resource locations. If
133         * the XML-based loader detects default resource locations,
134         * an {@code info} message will be logged.</li>
135         * <li>Subsequently, the annotation-based loader will be allowed to
136         * process the configuration in order to detect default configuration classes.
137         * If the annotation-based loader detects default configuration
138         * classes, an {@code info} message will be logged.</li>
139         * </ul>
140         * @param configAttributes the context configuration attributes to process
141         * @throws IllegalArgumentException if the supplied configuration attributes are
142         * {@code null}, or if the supplied configuration attributes include both
143         * resource locations and annotated classes
144         * @throws IllegalStateException if the XML-based loader detects default
145         * configuration classes; if the annotation-based loader detects default
146         * resource locations; if neither candidate loader detects defaults for the supplied
147         * context configuration; or if both candidate loaders detect defaults for the
148         * supplied context configuration
149         */
150        @Override
151        public void processContextConfiguration(final ContextConfigurationAttributes configAttributes) {
152                Assert.notNull(configAttributes, "configAttributes must not be null");
153                Assert.isTrue(!(configAttributes.hasLocations() && configAttributes.hasClasses()),
154                                () -> String.format("Cannot process locations AND classes for context configuration %s: " +
155                                                "configure one or the other, but not both.", configAttributes));
156
157                // If the original locations or classes were not empty, there's no
158                // need to bother with default detection checks; just let the
159                // appropriate loader process the configuration.
160                if (configAttributes.hasLocations()) {
161                        delegateProcessing(getXmlLoader(), configAttributes);
162                }
163                else if (configAttributes.hasClasses()) {
164                        delegateProcessing(getAnnotationConfigLoader(), configAttributes);
165                }
166                else {
167                        // Else attempt to detect defaults...
168
169                        // Let the XML loader process the configuration.
170                        delegateProcessing(getXmlLoader(), configAttributes);
171                        boolean xmlLoaderDetectedDefaults = configAttributes.hasLocations();
172
173                        if (xmlLoaderDetectedDefaults) {
174                                if (logger.isInfoEnabled()) {
175                                        logger.info(String.format("%s detected default locations for context configuration %s.",
176                                                        name(getXmlLoader()), configAttributes));
177                                }
178                        }
179
180                        Assert.state(!configAttributes.hasClasses(), () -> String.format(
181                                        "%s should NOT have detected default configuration classes for context configuration %s.",
182                                        name(getXmlLoader()), configAttributes));
183
184                        // Now let the annotation config loader process the configuration.
185                        delegateProcessing(getAnnotationConfigLoader(), configAttributes);
186
187                        if (configAttributes.hasClasses()) {
188                                if (logger.isInfoEnabled()) {
189                                        logger.info(String.format("%s detected default configuration classes for context configuration %s.",
190                                                        name(getAnnotationConfigLoader()), configAttributes));
191                                }
192                        }
193
194                        Assert.state(xmlLoaderDetectedDefaults || !configAttributes.hasLocations(), () -> String.format(
195                                        "%s should NOT have detected default locations for context configuration %s.",
196                                        name(getAnnotationConfigLoader()), configAttributes));
197
198                        if (configAttributes.hasLocations() && configAttributes.hasClasses()) {
199                                String msg = String.format(
200                                                "Configuration error: both default locations AND default configuration classes " +
201                                                "were detected for context configuration %s; configure one or the other, but not both.",
202                                                configAttributes);
203                                logger.error(msg);
204                                throw new IllegalStateException(msg);
205                        }
206                }
207        }
208
209        /**
210         * Delegates to an appropriate candidate {@code SmartContextLoader} to load
211         * an {@link ApplicationContext}.
212         * <p>Delegation is based on explicit knowledge of the implementations of the
213         * default loaders for {@linkplain #getXmlLoader() XML configuration files and
214         * Groovy scripts} and {@linkplain #getAnnotationConfigLoader() annotated classes}.
215         * Specifically, the delegation algorithm is as follows:
216         * <ul>
217         * <li>If the resource locations in the supplied {@code MergedContextConfiguration}
218         * are not empty and the annotated classes are empty,
219         * the XML-based loader will load the {@code ApplicationContext}.</li>
220         * <li>If the annotated classes in the supplied {@code MergedContextConfiguration}
221         * are not empty and the resource locations are empty,
222         * the annotation-based loader will load the {@code ApplicationContext}.</li>
223         * </ul>
224         * @param mergedConfig the merged context configuration to use to load the application context
225         * @throws IllegalArgumentException if the supplied merged configuration is {@code null}
226         * @throws IllegalStateException if neither candidate loader is capable of loading an
227         * {@code ApplicationContext} from the supplied merged context configuration
228         */
229        @Override
230        public ApplicationContext loadContext(MergedContextConfiguration mergedConfig) throws Exception {
231                Assert.notNull(mergedConfig, "MergedContextConfiguration must not be null");
232
233                Assert.state(!(mergedConfig.hasLocations() && mergedConfig.hasClasses()), () -> String.format(
234                                "Neither %s nor %s supports loading an ApplicationContext from %s: " +
235                                "declare either 'locations' or 'classes' but not both.", name(getXmlLoader()),
236                                name(getAnnotationConfigLoader()), mergedConfig));
237
238                SmartContextLoader[] candidates = {getXmlLoader(), getAnnotationConfigLoader()};
239                for (SmartContextLoader loader : candidates) {
240                        // Determine if each loader can load a context from the mergedConfig. If it
241                        // can, let it; otherwise, keep iterating.
242                        if (supports(loader, mergedConfig)) {
243                                return delegateLoading(loader, mergedConfig);
244                        }
245                }
246
247                // If neither of the candidates supports the mergedConfig based on resources but
248                // ACIs or customizers were declared, then delegate to the annotation config
249                // loader.
250                if (!mergedConfig.getContextInitializerClasses().isEmpty() || !mergedConfig.getContextCustomizers().isEmpty()) {
251                        return delegateLoading(getAnnotationConfigLoader(), mergedConfig);
252                }
253
254                // else...
255                throw new IllegalStateException(String.format(
256                                "Neither %s nor %s was able to load an ApplicationContext from %s.",
257                                name(getXmlLoader()), name(getAnnotationConfigLoader()), mergedConfig));
258        }
259
260
261        private static void delegateProcessing(SmartContextLoader loader, ContextConfigurationAttributes configAttributes) {
262                if (logger.isDebugEnabled()) {
263                        logger.debug(String.format("Delegating to %s to process context configuration %s.",
264                                        name(loader), configAttributes));
265                }
266                loader.processContextConfiguration(configAttributes);
267        }
268
269        private static ApplicationContext delegateLoading(SmartContextLoader loader, MergedContextConfiguration mergedConfig)
270                        throws Exception {
271
272                if (logger.isDebugEnabled()) {
273                        logger.debug(String.format("Delegating to %s to load context from %s.", name(loader), mergedConfig));
274                }
275                return loader.loadContext(mergedConfig);
276        }
277
278        private boolean supports(SmartContextLoader loader, MergedContextConfiguration mergedConfig) {
279                if (loader == getAnnotationConfigLoader()) {
280                        return (mergedConfig.hasClasses() && !mergedConfig.hasLocations());
281                }
282                else {
283                        return (mergedConfig.hasLocations() && !mergedConfig.hasClasses());
284                }
285        }
286
287        private static String name(SmartContextLoader loader) {
288                return loader.getClass().getSimpleName();
289        }
290
291}