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.builder;
018
019import java.util.Arrays;
020import java.util.Collection;
021import java.util.Collections;
022import java.util.HashMap;
023import java.util.LinkedHashMap;
024import java.util.LinkedHashSet;
025import java.util.Map;
026import java.util.Properties;
027import java.util.Set;
028import java.util.concurrent.atomic.AtomicBoolean;
029
030import org.springframework.beans.factory.support.BeanNameGenerator;
031import org.springframework.boot.Banner;
032import org.springframework.boot.SpringApplication;
033import org.springframework.boot.WebApplicationType;
034import org.springframework.boot.convert.ApplicationConversionService;
035import org.springframework.context.ApplicationContext;
036import org.springframework.context.ApplicationContextInitializer;
037import org.springframework.context.ApplicationListener;
038import org.springframework.context.ConfigurableApplicationContext;
039import org.springframework.core.env.ConfigurableEnvironment;
040import org.springframework.core.env.Environment;
041import org.springframework.core.io.ResourceLoader;
042import org.springframework.util.StringUtils;
043
044/**
045 * Builder for {@link SpringApplication} and {@link ApplicationContext} instances with
046 * convenient fluent API and context hierarchy support. Simple example of a context
047 * hierarchy:
048 *
049 * <pre class="code">
050 * new SpringApplicationBuilder(ParentConfig.class).child(ChildConfig.class).run(args);
051 * </pre>
052 *
053 * Another common use case is setting active profiles and default properties to set up the
054 * environment for an application:
055 *
056 * <pre class="code">
057 * new SpringApplicationBuilder(Application.class).profiles(&quot;server&quot;)
058 *              .properties(&quot;transport=local&quot;).run(args);
059 * </pre>
060 *
061 * <p>
062 * If your needs are simpler, consider using the static convenience methods in
063 * SpringApplication instead.
064 *
065 * @author Dave Syer
066 * @author Andy Wilkinson
067 * @see SpringApplication
068 */
069public class SpringApplicationBuilder {
070
071        private final SpringApplication application;
072
073        private ConfigurableApplicationContext context;
074
075        private SpringApplicationBuilder parent;
076
077        private final AtomicBoolean running = new AtomicBoolean(false);
078
079        private final Set<Class<?>> sources = new LinkedHashSet<>();
080
081        private final Map<String, Object> defaultProperties = new LinkedHashMap<>();
082
083        private ConfigurableEnvironment environment;
084
085        private Set<String> additionalProfiles = new LinkedHashSet<>();
086
087        private boolean registerShutdownHookApplied;
088
089        private boolean configuredAsChild = false;
090
091        public SpringApplicationBuilder(Class<?>... sources) {
092                this.application = createSpringApplication(sources);
093        }
094
095        /**
096         * Creates a new {@link org.springframework.boot.SpringApplication} instances from the
097         * given sources. Subclasses may override in order to provide a custom subclass of
098         * {@link org.springframework.boot.SpringApplication}
099         * @param sources the sources
100         * @return the {@link org.springframework.boot.SpringApplication} instance
101         * @since 1.1.0
102         */
103        protected SpringApplication createSpringApplication(Class<?>... sources) {
104                return new SpringApplication(sources);
105        }
106
107        /**
108         * Accessor for the current application context.
109         * @return the current application context (or null if not yet running)
110         */
111        public ConfigurableApplicationContext context() {
112                return this.context;
113        }
114
115        /**
116         * Accessor for the current application.
117         * @return the current application (never null)
118         */
119        public SpringApplication application() {
120                return this.application;
121        }
122
123        /**
124         * Create an application context (and its parent if specified) with the command line
125         * args provided. The parent is run first with the same arguments if has not yet been
126         * started.
127         * @param args the command line arguments
128         * @return an application context created from the current state
129         */
130        public ConfigurableApplicationContext run(String... args) {
131                if (this.running.get()) {
132                        // If already created we just return the existing context
133                        return this.context;
134                }
135                configureAsChildIfNecessary(args);
136                if (this.running.compareAndSet(false, true)) {
137                        synchronized (this.running) {
138                                // If not already running copy the sources over and then run.
139                                this.context = build().run(args);
140                        }
141                }
142                return this.context;
143        }
144
145        private void configureAsChildIfNecessary(String... args) {
146                if (this.parent != null && !this.configuredAsChild) {
147                        this.configuredAsChild = true;
148                        if (!this.registerShutdownHookApplied) {
149                                this.application.setRegisterShutdownHook(false);
150                        }
151                        initializers(new ParentContextApplicationContextInitializer(
152                                        this.parent.run(args)));
153                }
154        }
155
156        /**
157         * Returns a fully configured {@link SpringApplication} that is ready to run.
158         * @return the fully configured {@link SpringApplication}.
159         */
160        public SpringApplication build() {
161                return build(new String[0]);
162        }
163
164        /**
165         * Returns a fully configured {@link SpringApplication} that is ready to run. Any
166         * parent that has been configured will be run with the given {@code args}.
167         * @param args the parent's args
168         * @return the fully configured {@link SpringApplication}.
169         */
170        public SpringApplication build(String... args) {
171                configureAsChildIfNecessary(args);
172                this.application.addPrimarySources(this.sources);
173                return this.application;
174        }
175
176        /**
177         * Create a child application with the provided sources. Default args and environment
178         * are copied down into the child, but everything else is a clean sheet.
179         * @param sources the sources for the application (Spring configuration)
180         * @return the child application builder
181         */
182        public SpringApplicationBuilder child(Class<?>... sources) {
183                SpringApplicationBuilder child = new SpringApplicationBuilder();
184                child.sources(sources);
185
186                // Copy environment stuff from parent to child
187                child.properties(this.defaultProperties).environment(this.environment)
188                                .additionalProfiles(this.additionalProfiles);
189                child.parent = this;
190
191                // It's not possible if embedded web server are enabled to support web contexts as
192                // parents because the servlets cannot be initialized at the right point in
193                // lifecycle.
194                web(WebApplicationType.NONE);
195
196                // Probably not interested in multiple banners
197                bannerMode(Banner.Mode.OFF);
198
199                // Make sure sources get copied over
200                this.application.addPrimarySources(this.sources);
201
202                return child;
203        }
204
205        /**
206         * Add a parent application with the provided sources. Default args and environment
207         * are copied up into the parent, but everything else is a clean sheet.
208         * @param sources the sources for the application (Spring configuration)
209         * @return the parent builder
210         */
211        public SpringApplicationBuilder parent(Class<?>... sources) {
212                if (this.parent == null) {
213                        this.parent = new SpringApplicationBuilder(sources)
214                                        .web(WebApplicationType.NONE).properties(this.defaultProperties)
215                                        .environment(this.environment);
216                }
217                else {
218                        this.parent.sources(sources);
219                }
220                return this.parent;
221        }
222
223        private SpringApplicationBuilder runAndExtractParent(String... args) {
224                if (this.context == null) {
225                        run(args);
226                }
227                if (this.parent != null) {
228                        return this.parent;
229                }
230                throw new IllegalStateException(
231                                "No parent defined yet (please use the other overloaded parent methods to set one)");
232        }
233
234        /**
235         * Add an already running parent context to an existing application.
236         * @param parent the parent context
237         * @return the current builder (not the parent)
238         */
239        public SpringApplicationBuilder parent(ConfigurableApplicationContext parent) {
240                this.parent = new SpringApplicationBuilder();
241                this.parent.context = parent;
242                this.parent.running.set(true);
243                return this;
244        }
245
246        /**
247         * Create a sibling application (one with the same parent). A side effect of calling
248         * this method is that the current application (and its parent) are started without
249         * any arguments if they are not already running. To supply arguments when starting
250         * the current application and its parent use {@link #sibling(Class[], String...)}
251         * instead.
252         * @param sources the sources for the application (Spring configuration)
253         * @return the new sibling builder
254         */
255        public SpringApplicationBuilder sibling(Class<?>... sources) {
256                return runAndExtractParent().child(sources);
257        }
258
259        /**
260         * Create a sibling application (one with the same parent). A side effect of calling
261         * this method is that the current application (and its parent) are started if they
262         * are not already running.
263         * @param sources the sources for the application (Spring configuration)
264         * @param args the command line arguments to use when starting the current app and its
265         * parent
266         * @return the new sibling builder
267         */
268        public SpringApplicationBuilder sibling(Class<?>[] sources, String... args) {
269                return runAndExtractParent(args).child(sources);
270        }
271
272        /**
273         * Explicitly set the context class to be used.
274         * @param cls the context class to use
275         * @return the current builder
276         */
277        public SpringApplicationBuilder contextClass(
278                        Class<? extends ConfigurableApplicationContext> cls) {
279                this.application.setApplicationContextClass(cls);
280                return this;
281        }
282
283        /**
284         * Add more sources (configuration classes and components) to this application.
285         * @param sources the sources to add
286         * @return the current builder
287         */
288        public SpringApplicationBuilder sources(Class<?>... sources) {
289                this.sources.addAll(new LinkedHashSet<>(Arrays.asList(sources)));
290                return this;
291        }
292
293        /**
294         * Flag to explicitly request a specific type of web application. Auto-detected based
295         * on the classpath if not set.
296         * @param webApplicationType the type of web application
297         * @return the current builder
298         * @since 2.0.0
299         */
300        public SpringApplicationBuilder web(WebApplicationType webApplicationType) {
301                this.application.setWebApplicationType(webApplicationType);
302                return this;
303        }
304
305        /**
306         * Flag to indicate the startup information should be logged.
307         * @param logStartupInfo the flag to set. Default true.
308         * @return the current builder
309         */
310        public SpringApplicationBuilder logStartupInfo(boolean logStartupInfo) {
311                this.application.setLogStartupInfo(logStartupInfo);
312                return this;
313        }
314
315        /**
316         * Sets the {@link Banner} instance which will be used to print the banner when no
317         * static banner file is provided.
318         * @param banner the banner to use
319         * @return the current builder
320         */
321        public SpringApplicationBuilder banner(Banner banner) {
322                this.application.setBanner(banner);
323                return this;
324        }
325
326        public SpringApplicationBuilder bannerMode(Banner.Mode bannerMode) {
327                this.application.setBannerMode(bannerMode);
328                return this;
329        }
330
331        /**
332         * Sets if the application is headless and should not instantiate AWT. Defaults to
333         * {@code true} to prevent java icons appearing.
334         * @param headless if the application is headless
335         * @return the current builder
336         */
337        public SpringApplicationBuilder headless(boolean headless) {
338                this.application.setHeadless(headless);
339                return this;
340        }
341
342        /**
343         * Sets if the created {@link ApplicationContext} should have a shutdown hook
344         * registered.
345         * @param registerShutdownHook if the shutdown hook should be registered
346         * @return the current builder
347         */
348        public SpringApplicationBuilder registerShutdownHook(boolean registerShutdownHook) {
349                this.registerShutdownHookApplied = true;
350                this.application.setRegisterShutdownHook(registerShutdownHook);
351                return this;
352        }
353
354        /**
355         * Fixes the main application class that is used to anchor the startup messages.
356         * @param mainApplicationClass the class to use.
357         * @return the current builder
358         */
359        public SpringApplicationBuilder main(Class<?> mainApplicationClass) {
360                this.application.setMainApplicationClass(mainApplicationClass);
361                return this;
362        }
363
364        /**
365         * Flag to indicate that command line arguments should be added to the environment.
366         * @param addCommandLineProperties the flag to set. Default true.
367         * @return the current builder
368         */
369        public SpringApplicationBuilder addCommandLineProperties(
370                        boolean addCommandLineProperties) {
371                this.application.setAddCommandLineProperties(addCommandLineProperties);
372                return this;
373        }
374
375        /**
376         * Flag to indicate if the {@link ApplicationConversionService} should be added to the
377         * application context's {@link Environment}.
378         * @param addConversionService if the conversion service should be added.
379         * @return the current builder
380         * @since 2.1.0
381         */
382        public SpringApplicationBuilder setAddConversionService(
383                        boolean addConversionService) {
384                this.application.setAddConversionService(addConversionService);
385                return this;
386        }
387
388        /**
389         * Default properties for the environment in the form {@code key=value} or
390         * {@code key:value}.
391         * @param defaultProperties the properties to set.
392         * @return the current builder
393         */
394        public SpringApplicationBuilder properties(String... defaultProperties) {
395                return properties(getMapFromKeyValuePairs(defaultProperties));
396        }
397
398        private Map<String, Object> getMapFromKeyValuePairs(String[] properties) {
399                Map<String, Object> map = new HashMap<>();
400                for (String property : properties) {
401                        int index = lowestIndexOf(property, ":", "=");
402                        String key = (index > 0) ? property.substring(0, index) : property;
403                        String value = (index > 0) ? property.substring(index + 1) : "";
404                        map.put(key, value);
405                }
406                return map;
407        }
408
409        private int lowestIndexOf(String property, String... candidates) {
410                int index = -1;
411                for (String candidate : candidates) {
412                        int candidateIndex = property.indexOf(candidate);
413                        if (candidateIndex > 0) {
414                                index = (index != -1) ? Math.min(index, candidateIndex) : candidateIndex;
415                        }
416                }
417                return index;
418        }
419
420        /**
421         * Default properties for the environment in the form {@code key=value} or
422         * {@code key:value}.
423         * @param defaultProperties the properties to set.
424         * @return the current builder
425         */
426        public SpringApplicationBuilder properties(Properties defaultProperties) {
427                return properties(getMapFromProperties(defaultProperties));
428        }
429
430        private Map<String, Object> getMapFromProperties(Properties properties) {
431                Map<String, Object> map = new HashMap<>();
432                for (Object key : Collections.list(properties.propertyNames())) {
433                        map.put((String) key, properties.get(key));
434                }
435                return map;
436        }
437
438        /**
439         * Default properties for the environment. Multiple calls to this method are
440         * cumulative.
441         * @param defaults the default properties
442         * @return the current builder
443         * @see SpringApplicationBuilder#properties(String...)
444         */
445        public SpringApplicationBuilder properties(Map<String, Object> defaults) {
446                this.defaultProperties.putAll(defaults);
447                this.application.setDefaultProperties(this.defaultProperties);
448                if (this.parent != null) {
449                        this.parent.properties(this.defaultProperties);
450                        this.parent.environment(this.environment);
451                }
452                return this;
453        }
454
455        /**
456         * Add to the active Spring profiles for this app (and its parent and children).
457         * @param profiles the profiles to add.
458         * @return the current builder
459         */
460        public SpringApplicationBuilder profiles(String... profiles) {
461                this.additionalProfiles.addAll(Arrays.asList(profiles));
462                this.application.setAdditionalProfiles(
463                                StringUtils.toStringArray(this.additionalProfiles));
464                return this;
465        }
466
467        private SpringApplicationBuilder additionalProfiles(
468                        Collection<String> additionalProfiles) {
469                this.additionalProfiles = new LinkedHashSet<>(additionalProfiles);
470                this.application.setAdditionalProfiles(
471                                StringUtils.toStringArray(this.additionalProfiles));
472                return this;
473        }
474
475        /**
476         * Bean name generator for automatically generated bean names in the application
477         * context.
478         * @param beanNameGenerator the generator to set.
479         * @return the current builder
480         */
481        public SpringApplicationBuilder beanNameGenerator(
482                        BeanNameGenerator beanNameGenerator) {
483                this.application.setBeanNameGenerator(beanNameGenerator);
484                return this;
485        }
486
487        /**
488         * Environment for the application context.
489         * @param environment the environment to set.
490         * @return the current builder
491         */
492        public SpringApplicationBuilder environment(ConfigurableEnvironment environment) {
493                this.application.setEnvironment(environment);
494                this.environment = environment;
495                return this;
496        }
497
498        /**
499         * {@link ResourceLoader} for the application context. If a custom class loader is
500         * needed, this is where it would be added.
501         * @param resourceLoader the resource loader to set.
502         * @return the current builder
503         */
504        public SpringApplicationBuilder resourceLoader(ResourceLoader resourceLoader) {
505                this.application.setResourceLoader(resourceLoader);
506                return this;
507        }
508
509        /**
510         * Add some initializers to the application (applied to the {@link ApplicationContext}
511         * before any bean definitions are loaded).
512         * @param initializers some initializers to add
513         * @return the current builder
514         */
515        public SpringApplicationBuilder initializers(
516                        ApplicationContextInitializer<?>... initializers) {
517                this.application.addInitializers(initializers);
518                return this;
519        }
520
521        /**
522         * Add some listeners to the application (listening for SpringApplication events as
523         * well as regular Spring events once the context is running). Any listeners that are
524         * also {@link ApplicationContextInitializer} will be added to the
525         * {@link #initializers(ApplicationContextInitializer...) initializers} automatically.
526         * @param listeners some listeners to add
527         * @return the current builder
528         */
529        public SpringApplicationBuilder listeners(ApplicationListener<?>... listeners) {
530                this.application.addListeners(listeners);
531                return this;
532        }
533
534}