001/*
002 * Copyright 2012-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 *      http://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.boot.test.context;
018
019import java.util.ArrayList;
020import java.util.Arrays;
021import java.util.List;
022
023import org.springframework.beans.BeanUtils;
024import org.springframework.boot.SpringApplication;
025import org.springframework.boot.WebApplicationType;
026import org.springframework.boot.context.properties.bind.Bindable;
027import org.springframework.boot.context.properties.bind.Binder;
028import org.springframework.boot.context.properties.source.ConfigurationPropertySource;
029import org.springframework.boot.context.properties.source.MapConfigurationPropertySource;
030import org.springframework.boot.test.mock.web.SpringBootMockServletContext;
031import org.springframework.boot.test.util.TestPropertyValues;
032import org.springframework.boot.web.reactive.context.GenericReactiveWebApplicationContext;
033import org.springframework.boot.web.servlet.support.ServletContextApplicationContextInitializer;
034import org.springframework.context.ApplicationContext;
035import org.springframework.context.ApplicationContextInitializer;
036import org.springframework.context.ConfigurableApplicationContext;
037import org.springframework.core.Ordered;
038import org.springframework.core.SpringVersion;
039import org.springframework.core.annotation.AnnotatedElementUtils;
040import org.springframework.core.annotation.Order;
041import org.springframework.core.env.ConfigurableEnvironment;
042import org.springframework.core.env.StandardEnvironment;
043import org.springframework.core.io.DefaultResourceLoader;
044import org.springframework.core.io.ResourceLoader;
045import org.springframework.test.context.ContextConfigurationAttributes;
046import org.springframework.test.context.ContextCustomizer;
047import org.springframework.test.context.ContextLoader;
048import org.springframework.test.context.MergedContextConfiguration;
049import org.springframework.test.context.support.AbstractContextLoader;
050import org.springframework.test.context.support.AnnotationConfigContextLoaderUtils;
051import org.springframework.test.context.support.TestPropertySourceUtils;
052import org.springframework.test.context.web.WebMergedContextConfiguration;
053import org.springframework.util.Assert;
054import org.springframework.util.ObjectUtils;
055import org.springframework.util.StringUtils;
056import org.springframework.web.context.support.GenericWebApplicationContext;
057
058/**
059 * A {@link ContextLoader} that can be used to test Spring Boot applications (those that
060 * normally startup using {@link SpringApplication}). Although this loader can be used
061 * directly, most test will instead want to use it with {@link SpringBootTest}.
062 * <p>
063 * The loader supports both standard {@link MergedContextConfiguration} as well as
064 * {@link WebMergedContextConfiguration}. If {@link WebMergedContextConfiguration} is used
065 * the context will either use a mock servlet environment, or start the full embedded web
066 * server.
067 * <p>
068 * If {@code @ActiveProfiles} are provided in the test class they will be used to create
069 * the application context.
070 *
071 * @author Dave Syer
072 * @author Phillip Webb
073 * @author Andy Wilkinson
074 * @author Stephane Nicoll
075 * @author Madhura Bhave
076 * @see SpringBootTest
077 */
078public class SpringBootContextLoader extends AbstractContextLoader {
079
080        @Override
081        public ApplicationContext loadContext(MergedContextConfiguration config)
082                        throws Exception {
083                Class<?>[] configClasses = config.getClasses();
084                String[] configLocations = config.getLocations();
085                Assert.state(
086                                !ObjectUtils.isEmpty(configClasses)
087                                                || !ObjectUtils.isEmpty(configLocations),
088                                () -> "No configuration classes "
089                                                + "or locations found in @SpringApplicationConfiguration. "
090                                                + "For default configuration detection to work you need "
091                                                + "Spring 4.0.3 or better (found " + SpringVersion.getVersion()
092                                                + ").");
093                SpringApplication application = getSpringApplication();
094                application.setMainApplicationClass(config.getTestClass());
095                application.addPrimarySources(Arrays.asList(configClasses));
096                application.getSources().addAll(Arrays.asList(configLocations));
097                ConfigurableEnvironment environment = getEnvironment();
098                if (!ObjectUtils.isEmpty(config.getActiveProfiles())) {
099                        setActiveProfiles(environment, config.getActiveProfiles());
100                }
101                ResourceLoader resourceLoader = (application.getResourceLoader() != null)
102                                ? application.getResourceLoader()
103                                : new DefaultResourceLoader(getClass().getClassLoader());
104                TestPropertySourceUtils.addPropertiesFilesToEnvironment(environment,
105                                resourceLoader, config.getPropertySourceLocations());
106                TestPropertySourceUtils.addInlinedPropertiesToEnvironment(environment,
107                                getInlinedProperties(config));
108                application.setEnvironment(environment);
109                List<ApplicationContextInitializer<?>> initializers = getInitializers(config,
110                                application);
111                if (config instanceof WebMergedContextConfiguration) {
112                        application.setWebApplicationType(WebApplicationType.SERVLET);
113                        if (!isEmbeddedWebEnvironment(config)) {
114                                new WebConfigurer().configure(config, application, initializers);
115                        }
116                }
117                else if (config instanceof ReactiveWebMergedContextConfiguration) {
118                        application.setWebApplicationType(WebApplicationType.REACTIVE);
119                        if (!isEmbeddedWebEnvironment(config)) {
120                                new ReactiveWebConfigurer().configure(application);
121                        }
122                }
123                else {
124                        application.setWebApplicationType(WebApplicationType.NONE);
125                }
126                application.setInitializers(initializers);
127                return application.run();
128        }
129
130        /**
131         * Builds new {@link org.springframework.boot.SpringApplication} instance. You can
132         * override this method to add custom behavior
133         * @return {@link org.springframework.boot.SpringApplication} instance
134         */
135        protected SpringApplication getSpringApplication() {
136                return new SpringApplication();
137        }
138
139        /**
140         * Builds a new {@link ConfigurableEnvironment} instance. You can override this method
141         * to return something other than {@link StandardEnvironment} if necessary.
142         * @return a {@link ConfigurableEnvironment} instance
143         */
144        protected ConfigurableEnvironment getEnvironment() {
145                return new StandardEnvironment();
146        }
147
148        private void setActiveProfiles(ConfigurableEnvironment environment,
149                        String[] profiles) {
150                TestPropertyValues
151                                .of("spring.profiles.active="
152                                                + StringUtils.arrayToCommaDelimitedString(profiles))
153                                .applyTo(environment);
154        }
155
156        protected String[] getInlinedProperties(MergedContextConfiguration config) {
157                ArrayList<String> properties = new ArrayList<>();
158                // JMX bean names will clash if the same bean is used in multiple contexts
159                disableJmx(properties);
160                properties.addAll(Arrays.asList(config.getPropertySourceProperties()));
161                if (!isEmbeddedWebEnvironment(config) && !hasCustomServerPort(properties)) {
162                        properties.add("server.port=-1");
163                }
164                return StringUtils.toStringArray(properties);
165        }
166
167        private void disableJmx(List<String> properties) {
168                properties.add("spring.jmx.enabled=false");
169        }
170
171        private boolean hasCustomServerPort(List<String> properties) {
172                Binder binder = new Binder(convertToConfigurationPropertySource(properties));
173                return binder.bind("server.port", Bindable.of(String.class)).isBound();
174        }
175
176        private ConfigurationPropertySource convertToConfigurationPropertySource(
177                        List<String> properties) {
178                return new MapConfigurationPropertySource(TestPropertySourceUtils
179                                .convertInlinedPropertiesToMap(StringUtils.toStringArray(properties)));
180        }
181
182        /**
183         * Return the {@link ApplicationContextInitializer initializers} that will be applied
184         * to the context. By default this method will adapt {@link ContextCustomizer context
185         * customizers}, add {@link SpringApplication#getInitializers() application
186         * initializers} and add
187         * {@link MergedContextConfiguration#getContextInitializerClasses() initializers
188         * specified on the test}.
189         * @param config the source context configuration
190         * @param application the application instance
191         * @return the initializers to apply
192         * @since 2.0.0
193         */
194        protected List<ApplicationContextInitializer<?>> getInitializers(
195                        MergedContextConfiguration config, SpringApplication application) {
196                List<ApplicationContextInitializer<?>> initializers = new ArrayList<>();
197                for (ContextCustomizer contextCustomizer : config.getContextCustomizers()) {
198                        initializers.add(new ContextCustomizerAdapter(contextCustomizer, config));
199                }
200                initializers.addAll(application.getInitializers());
201                for (Class<? extends ApplicationContextInitializer<?>> initializerClass : config
202                                .getContextInitializerClasses()) {
203                        initializers.add(BeanUtils.instantiateClass(initializerClass));
204                }
205                if (config.getParent() != null) {
206                        initializers.add(new ParentContextApplicationContextInitializer(
207                                        config.getParentApplicationContext()));
208                }
209                return initializers;
210        }
211
212        private boolean isEmbeddedWebEnvironment(MergedContextConfiguration config) {
213                SpringBootTest annotation = AnnotatedElementUtils
214                                .findMergedAnnotation(config.getTestClass(), SpringBootTest.class);
215                if (annotation != null && annotation.webEnvironment().isEmbedded()) {
216                        return true;
217                }
218                return false;
219        }
220
221        @Override
222        public void processContextConfiguration(
223                        ContextConfigurationAttributes configAttributes) {
224                super.processContextConfiguration(configAttributes);
225                if (!configAttributes.hasResources()) {
226                        Class<?>[] defaultConfigClasses = detectDefaultConfigurationClasses(
227                                        configAttributes.getDeclaringClass());
228                        configAttributes.setClasses(defaultConfigClasses);
229                }
230        }
231
232        /**
233         * Detect the default configuration classes for the supplied test class. By default
234         * simply delegates to
235         * {@link AnnotationConfigContextLoaderUtils#detectDefaultConfigurationClasses}.
236         * @param declaringClass the test class that declared {@code @ContextConfiguration}
237         * @return an array of default configuration classes, potentially empty but never
238         * {@code null}
239         * @see AnnotationConfigContextLoaderUtils
240         */
241        protected Class<?>[] detectDefaultConfigurationClasses(Class<?> declaringClass) {
242                return AnnotationConfigContextLoaderUtils
243                                .detectDefaultConfigurationClasses(declaringClass);
244        }
245
246        @Override
247        public ApplicationContext loadContext(String... locations) throws Exception {
248                throw new UnsupportedOperationException("SpringApplicationContextLoader "
249                                + "does not support the loadContext(String...) method");
250        }
251
252        @Override
253        protected String[] getResourceSuffixes() {
254                return new String[] { "-context.xml", "Context.groovy" };
255        }
256
257        @Override
258        protected String getResourceSuffix() {
259                throw new IllegalStateException();
260        }
261
262        /**
263         * Inner class to configure {@link WebMergedContextConfiguration}.
264         */
265        private static class WebConfigurer {
266
267                private static final Class<GenericWebApplicationContext> WEB_CONTEXT_CLASS = GenericWebApplicationContext.class;
268
269                void configure(MergedContextConfiguration configuration,
270                                SpringApplication application,
271                                List<ApplicationContextInitializer<?>> initializers) {
272                        WebMergedContextConfiguration webConfiguration = (WebMergedContextConfiguration) configuration;
273                        addMockServletContext(initializers, webConfiguration);
274                        application.setApplicationContextClass(WEB_CONTEXT_CLASS);
275                }
276
277                private void addMockServletContext(
278                                List<ApplicationContextInitializer<?>> initializers,
279                                WebMergedContextConfiguration webConfiguration) {
280                        SpringBootMockServletContext servletContext = new SpringBootMockServletContext(
281                                        webConfiguration.getResourceBasePath());
282                        initializers.add(0, new ServletContextApplicationContextInitializer(
283                                        servletContext, true));
284                }
285
286        }
287
288        /**
289         * Inner class to configure {@link ReactiveWebMergedContextConfiguration}.
290         */
291        private static class ReactiveWebConfigurer {
292
293                private static final Class<GenericReactiveWebApplicationContext> WEB_CONTEXT_CLASS = GenericReactiveWebApplicationContext.class;
294
295                void configure(SpringApplication application) {
296                        application.setApplicationContextClass(WEB_CONTEXT_CLASS);
297                }
298
299        }
300
301        /**
302         * Adapts a {@link ContextCustomizer} to a {@link ApplicationContextInitializer} so
303         * that it can be triggered via {@link SpringApplication}.
304         */
305        private static class ContextCustomizerAdapter
306                        implements ApplicationContextInitializer<ConfigurableApplicationContext> {
307
308                private final ContextCustomizer contextCustomizer;
309
310                private final MergedContextConfiguration config;
311
312                ContextCustomizerAdapter(ContextCustomizer contextCustomizer,
313                                MergedContextConfiguration config) {
314                        this.contextCustomizer = contextCustomizer;
315                        this.config = config;
316                }
317
318                @Override
319                public void initialize(ConfigurableApplicationContext applicationContext) {
320                        this.contextCustomizer.customizeContext(applicationContext, this.config);
321                }
322
323        }
324
325        @Order(Ordered.HIGHEST_PRECEDENCE)
326        private static class ParentContextApplicationContextInitializer
327                        implements ApplicationContextInitializer<ConfigurableApplicationContext> {
328
329                private final ApplicationContext parent;
330
331                ParentContextApplicationContextInitializer(ApplicationContext parent) {
332                        this.parent = parent;
333                }
334
335                @Override
336                public void initialize(ConfigurableApplicationContext applicationContext) {
337                        applicationContext.setParent(this.parent);
338                }
339
340        }
341
342}