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