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