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.web;
018
019import javax.servlet.ServletContext;
020
021import org.apache.commons.logging.Log;
022import org.apache.commons.logging.LogFactory;
023
024import org.springframework.beans.factory.support.DefaultListableBeanFactory;
025import org.springframework.context.ApplicationContext;
026import org.springframework.context.ConfigurableApplicationContext;
027import org.springframework.context.annotation.AnnotationConfigUtils;
028import org.springframework.core.io.DefaultResourceLoader;
029import org.springframework.core.io.FileSystemResourceLoader;
030import org.springframework.core.io.ResourceLoader;
031import org.springframework.mock.web.MockServletContext;
032import org.springframework.test.context.MergedContextConfiguration;
033import org.springframework.test.context.support.AbstractContextLoader;
034import org.springframework.util.Assert;
035import org.springframework.web.context.WebApplicationContext;
036import org.springframework.web.context.support.GenericWebApplicationContext;
037
038/**
039 * Abstract, generic extension of {@link AbstractContextLoader} that loads a
040 * {@link GenericWebApplicationContext}.
041 *
042 * <p>If instances of concrete subclasses are invoked via the
043 * {@link org.springframework.test.context.SmartContextLoader SmartContextLoader}
044 * SPI, the context will be loaded from the {@link MergedContextConfiguration}
045 * provided to {@link #loadContext(MergedContextConfiguration)}. In such cases, a
046 * {@code SmartContextLoader} will decide whether to load the context from
047 * <em>locations</em> or <em>annotated classes</em>. Note that {@code
048 * AbstractGenericWebContextLoader} does not support the {@code
049 * loadContext(String... locations)} method from the legacy
050 * {@link org.springframework.test.context.ContextLoader ContextLoader} SPI.
051 *
052 * <p>Concrete subclasses must provide an appropriate implementation of
053 * {@link #loadBeanDefinitions}.
054 *
055 * @author Sam Brannen
056 * @author Phillip Webb
057 * @since 3.2
058 * @see #loadContext(MergedContextConfiguration)
059 * @see #loadContext(String...)
060 */
061public abstract class AbstractGenericWebContextLoader extends AbstractContextLoader {
062
063        protected static final Log logger = LogFactory.getLog(AbstractGenericWebContextLoader.class);
064
065
066        // SmartContextLoader
067
068        /**
069         * Load a Spring {@link WebApplicationContext} from the supplied
070         * {@link MergedContextConfiguration}.
071         * <p>Implementation details:
072         * <ul>
073         * <li>Calls {@link #validateMergedContextConfiguration(WebMergedContextConfiguration)}
074         * to allow subclasses to validate the supplied configuration before proceeding.</li>
075         * <li>Creates a {@link GenericWebApplicationContext} instance.</li>
076         * <li>If the supplied {@code MergedContextConfiguration} references a
077         * {@linkplain MergedContextConfiguration#getParent() parent configuration},
078         * the corresponding {@link MergedContextConfiguration#getParentApplicationContext()
079         * ApplicationContext} will be retrieved and
080         * {@linkplain GenericWebApplicationContext#setParent(ApplicationContext) set as the parent}
081         * for the context created by this method.</li>
082         * <li>Delegates to {@link #configureWebResources} to create the
083         * {@link MockServletContext} and set it in the {@code WebApplicationContext}.</li>
084         * <li>Calls {@link #prepareContext} to allow for customizing the context
085         * before bean definitions are loaded.</li>
086         * <li>Calls {@link #customizeBeanFactory} to allow for customizing the
087         * context's {@code DefaultListableBeanFactory}.</li>
088         * <li>Delegates to {@link #loadBeanDefinitions} to populate the context
089         * from the locations or classes in the supplied {@code MergedContextConfiguration}.</li>
090         * <li>Delegates to {@link AnnotationConfigUtils} for
091         * {@linkplain AnnotationConfigUtils#registerAnnotationConfigProcessors registering}
092         * annotation configuration processors.</li>
093         * <li>Calls {@link #customizeContext} to allow for customizing the context
094         * before it is refreshed.</li>
095         * <li>{@link ConfigurableApplicationContext#refresh Refreshes} the
096         * context and registers a JVM shutdown hook for it.</li>
097         * </ul>
098         * @return a new web application context
099         * @see org.springframework.test.context.SmartContextLoader#loadContext(MergedContextConfiguration)
100         * @see GenericWebApplicationContext
101         */
102        @Override
103        public final ConfigurableApplicationContext loadContext(MergedContextConfiguration mergedConfig) throws Exception {
104                if (!(mergedConfig instanceof WebMergedContextConfiguration)) {
105                        throw new IllegalArgumentException(String.format(
106                                        "Cannot load WebApplicationContext from non-web merged context configuration %s. " +
107                                        "Consider annotating your test class with @WebAppConfiguration.", mergedConfig));
108                }
109                WebMergedContextConfiguration webMergedConfig = (WebMergedContextConfiguration) mergedConfig;
110
111                if (logger.isDebugEnabled()) {
112                        logger.debug(String.format("Loading WebApplicationContext for merged context configuration %s.",
113                                webMergedConfig));
114                }
115
116                validateMergedContextConfiguration(webMergedConfig);
117
118                GenericWebApplicationContext context = new GenericWebApplicationContext();
119
120                ApplicationContext parent = mergedConfig.getParentApplicationContext();
121                if (parent != null) {
122                        context.setParent(parent);
123                }
124                configureWebResources(context, webMergedConfig);
125                prepareContext(context, webMergedConfig);
126                customizeBeanFactory(context.getDefaultListableBeanFactory(), webMergedConfig);
127                loadBeanDefinitions(context, webMergedConfig);
128                AnnotationConfigUtils.registerAnnotationConfigProcessors(context);
129                customizeContext(context, webMergedConfig);
130                context.refresh();
131                context.registerShutdownHook();
132                return context;
133        }
134
135        /**
136         * Validate the supplied {@link WebMergedContextConfiguration} with respect to
137         * what this context loader supports.
138         * <p>The default implementation is a <em>no-op</em> but can be overridden by
139         * subclasses as appropriate.
140         * @param mergedConfig the merged configuration to validate
141         * @throws IllegalStateException if the supplied configuration is not valid
142         * for this context loader
143         * @since 4.0.4
144         */
145        protected void validateMergedContextConfiguration(WebMergedContextConfiguration mergedConfig) {
146                /* no-op */
147        }
148
149        /**
150         * Configures web resources for the supplied web application context (WAC).
151         * <h4>Implementation Details</h4>
152         * <p>If the supplied WAC has no parent or its parent is not a WAC, the
153         * supplied WAC will be configured as the Root WAC (see "<em>Root WAC
154         * Configuration</em>" below).
155         * <p>Otherwise the context hierarchy of the supplied WAC will be traversed
156         * to find the top-most WAC (i.e., the root); and the {@link ServletContext}
157         * of the Root WAC will be set as the {@code ServletContext} for the supplied
158         * WAC.
159         * <h4>Root WAC Configuration</h4>
160         * <ul>
161         * <li>The resource base path is retrieved from the supplied
162         * {@code WebMergedContextConfiguration}.</li>
163         * <li>A {@link ResourceLoader} is instantiated for the {@link MockServletContext}:
164         * if the resource base path is prefixed with "{@code classpath:}", a
165         * {@link DefaultResourceLoader} will be used; otherwise, a
166         * {@link FileSystemResourceLoader} will be used.</li>
167         * <li>A {@code MockServletContext} will be created using the resource base
168         * path and resource loader.</li>
169         * <li>The supplied {@link GenericWebApplicationContext} is then stored in
170         * the {@code MockServletContext} under the
171         * {@link WebApplicationContext#ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE} key.</li>
172         * <li>Finally, the {@code MockServletContext} is set in the
173         * {@code WebApplicationContext}.</li>
174         * @param context the web application context for which to configure the web resources
175         * @param webMergedConfig the merged context configuration to use to load the web application context
176         */
177        protected void configureWebResources(GenericWebApplicationContext context,
178                        WebMergedContextConfiguration webMergedConfig) {
179
180                ApplicationContext parent = context.getParent();
181
182                // If the WebApplicationContext has no parent or the parent is not a WebApplicationContext,
183                // set the current context as the root WebApplicationContext:
184                if (parent == null || (!(parent instanceof WebApplicationContext))) {
185                        String resourceBasePath = webMergedConfig.getResourceBasePath();
186                        ResourceLoader resourceLoader = (resourceBasePath.startsWith(ResourceLoader.CLASSPATH_URL_PREFIX) ?
187                                        new DefaultResourceLoader() : new FileSystemResourceLoader());
188                        ServletContext servletContext = new MockServletContext(resourceBasePath, resourceLoader);
189                        servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, context);
190                        context.setServletContext(servletContext);
191                }
192                else {
193                        ServletContext servletContext = null;
194                        // Find the root WebApplicationContext
195                        while (parent != null) {
196                                if (parent instanceof WebApplicationContext && !(parent.getParent() instanceof WebApplicationContext)) {
197                                        servletContext = ((WebApplicationContext) parent).getServletContext();
198                                        break;
199                                }
200                                parent = parent.getParent();
201                        }
202                        Assert.state(servletContext != null, "Failed to find root WebApplicationContext in the context hierarchy");
203                        context.setServletContext(servletContext);
204                }
205        }
206
207        /**
208         * Customize the internal bean factory of the {@code WebApplicationContext}
209         * created by this context loader.
210         * <p>The default implementation is empty but can be overridden in subclasses
211         * to customize {@code DefaultListableBeanFactory}'s standard settings.
212         * @param beanFactory the bean factory created by this context loader
213         * @param webMergedConfig the merged context configuration to use to load the
214         * web application context
215         * @see #loadContext(MergedContextConfiguration)
216         * @see DefaultListableBeanFactory#setAllowBeanDefinitionOverriding
217         * @see DefaultListableBeanFactory#setAllowEagerClassLoading
218         * @see DefaultListableBeanFactory#setAllowCircularReferences
219         * @see DefaultListableBeanFactory#setAllowRawInjectionDespiteWrapping
220         */
221        protected void customizeBeanFactory(
222                        DefaultListableBeanFactory beanFactory, WebMergedContextConfiguration webMergedConfig) {
223        }
224
225        /**
226         * Load bean definitions into the supplied {@link GenericWebApplicationContext context}
227         * from the locations or classes in the supplied {@code WebMergedContextConfiguration}.
228         * <p>Concrete subclasses must provide an appropriate implementation.
229         * @param context the context into which the bean definitions should be loaded
230         * @param webMergedConfig the merged context configuration to use to load the
231         * web application context
232         * @see #loadContext(MergedContextConfiguration)
233         */
234        protected abstract void loadBeanDefinitions(
235                        GenericWebApplicationContext context, WebMergedContextConfiguration webMergedConfig);
236
237        /**
238         * Customize the {@link GenericWebApplicationContext} created by this context
239         * loader <i>after</i> bean definitions have been loaded into the context but
240         * <i>before</i> the context is refreshed.
241         * <p>The default implementation simply delegates to
242         * {@link AbstractContextLoader#customizeContext(ConfigurableApplicationContext, MergedContextConfiguration)}.
243         * @param context the newly created web application context
244         * @param webMergedConfig the merged context configuration to use to load the
245         * web application context
246         * @see #loadContext(MergedContextConfiguration)
247         * @see #customizeContext(ConfigurableApplicationContext, MergedContextConfiguration)
248         */
249        protected void customizeContext(
250                        GenericWebApplicationContext context, WebMergedContextConfiguration webMergedConfig) {
251
252                super.customizeContext(context, webMergedConfig);
253        }
254
255
256        // ContextLoader
257
258        /**
259         * {@code AbstractGenericWebContextLoader} should be used as a
260         * {@link org.springframework.test.context.SmartContextLoader SmartContextLoader},
261         * not as a legacy {@link org.springframework.test.context.ContextLoader ContextLoader}.
262         * Consequently, this method is not supported.
263         * @see org.springframework.test.context.ContextLoader#loadContext(java.lang.String[])
264         * @throws UnsupportedOperationException in this implementation
265         */
266        @Override
267        public final ApplicationContext loadContext(String... locations) throws Exception {
268                throw new UnsupportedOperationException(
269                                "AbstractGenericWebContextLoader does not support the loadContext(String... locations) method");
270        }
271
272}