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.context.config;
018
019import java.io.IOException;
020import java.util.ArrayList;
021import java.util.Arrays;
022import java.util.Collections;
023import java.util.Deque;
024import java.util.HashMap;
025import java.util.HashSet;
026import java.util.LinkedHashMap;
027import java.util.LinkedHashSet;
028import java.util.LinkedList;
029import java.util.List;
030import java.util.Map;
031import java.util.Set;
032import java.util.function.BiConsumer;
033import java.util.stream.Collectors;
034
035import org.apache.commons.logging.Log;
036
037import org.springframework.beans.BeansException;
038import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
039import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
040import org.springframework.boot.SpringApplication;
041import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent;
042import org.springframework.boot.context.event.ApplicationPreparedEvent;
043import org.springframework.boot.context.properties.bind.Bindable;
044import org.springframework.boot.context.properties.bind.Binder;
045import org.springframework.boot.context.properties.bind.PropertySourcesPlaceholdersResolver;
046import org.springframework.boot.context.properties.source.ConfigurationPropertySources;
047import org.springframework.boot.env.EnvironmentPostProcessor;
048import org.springframework.boot.env.PropertySourceLoader;
049import org.springframework.boot.env.RandomValuePropertySource;
050import org.springframework.boot.logging.DeferredLog;
051import org.springframework.context.ApplicationEvent;
052import org.springframework.context.ConfigurableApplicationContext;
053import org.springframework.context.annotation.ConfigurationClassPostProcessor;
054import org.springframework.context.event.SmartApplicationListener;
055import org.springframework.core.Ordered;
056import org.springframework.core.annotation.AnnotationAwareOrderComparator;
057import org.springframework.core.env.ConfigurableEnvironment;
058import org.springframework.core.env.Environment;
059import org.springframework.core.env.MutablePropertySources;
060import org.springframework.core.env.Profiles;
061import org.springframework.core.env.PropertySource;
062import org.springframework.core.io.DefaultResourceLoader;
063import org.springframework.core.io.Resource;
064import org.springframework.core.io.ResourceLoader;
065import org.springframework.core.io.support.SpringFactoriesLoader;
066import org.springframework.util.Assert;
067import org.springframework.util.CollectionUtils;
068import org.springframework.util.ObjectUtils;
069import org.springframework.util.ResourceUtils;
070import org.springframework.util.StringUtils;
071
072/**
073 * {@link EnvironmentPostProcessor} that configures the context environment by loading
074 * properties from well known file locations. By default properties will be loaded from
075 * 'application.properties' and/or 'application.yml' files in the following locations:
076 * <ul>
077 * <li>classpath:</li>
078 * <li>file:./</li>
079 * <li>classpath:config/</li>
080 * <li>file:./config/:</li>
081 * </ul>
082 * <p>
083 * Alternative search locations and names can be specified using
084 * {@link #setSearchLocations(String)} and {@link #setSearchNames(String)}.
085 * <p>
086 * Additional files will also be loaded based on active profiles. For example if a 'web'
087 * profile is active 'application-web.properties' and 'application-web.yml' will be
088 * considered.
089 * <p>
090 * The 'spring.config.name' property can be used to specify an alternative name to load
091 * and the 'spring.config.location' property can be used to specify alternative search
092 * locations or specific files.
093 * <p>
094 *
095 * @author Dave Syer
096 * @author Phillip Webb
097 * @author Stephane Nicoll
098 * @author Andy Wilkinson
099 * @author EddĂș MelĂ©ndez
100 * @author Madhura Bhave
101 */
102public class ConfigFileApplicationListener
103                implements EnvironmentPostProcessor, SmartApplicationListener, Ordered {
104
105        private static final String DEFAULT_PROPERTIES = "defaultProperties";
106
107        // Note the order is from least to most specific (last one wins)
108        private static final String DEFAULT_SEARCH_LOCATIONS = "classpath:/,classpath:/config/,file:./,file:./config/";
109
110        private static final String DEFAULT_NAMES = "application";
111
112        private static final Set<String> NO_SEARCH_NAMES = Collections.singleton(null);
113
114        private static final Bindable<String[]> STRING_ARRAY = Bindable.of(String[].class);
115
116        /**
117         * The "active profiles" property name.
118         */
119        public static final String ACTIVE_PROFILES_PROPERTY = "spring.profiles.active";
120
121        /**
122         * The "includes profiles" property name.
123         */
124        public static final String INCLUDE_PROFILES_PROPERTY = "spring.profiles.include";
125
126        /**
127         * The "config name" property name.
128         */
129        public static final String CONFIG_NAME_PROPERTY = "spring.config.name";
130
131        /**
132         * The "config location" property name.
133         */
134        public static final String CONFIG_LOCATION_PROPERTY = "spring.config.location";
135
136        /**
137         * The "config additional location" property name.
138         */
139        public static final String CONFIG_ADDITIONAL_LOCATION_PROPERTY = "spring.config.additional-location";
140
141        /**
142         * The default order for the processor.
143         */
144        public static final int DEFAULT_ORDER = Ordered.HIGHEST_PRECEDENCE + 10;
145
146        private final DeferredLog logger = new DeferredLog();
147
148        private String searchLocations;
149
150        private String names;
151
152        private int order = DEFAULT_ORDER;
153
154        @Override
155        public boolean supportsEventType(Class<? extends ApplicationEvent> eventType) {
156                return ApplicationEnvironmentPreparedEvent.class.isAssignableFrom(eventType)
157                                || ApplicationPreparedEvent.class.isAssignableFrom(eventType);
158        }
159
160        @Override
161        public void onApplicationEvent(ApplicationEvent event) {
162                if (event instanceof ApplicationEnvironmentPreparedEvent) {
163                        onApplicationEnvironmentPreparedEvent(
164                                        (ApplicationEnvironmentPreparedEvent) event);
165                }
166                if (event instanceof ApplicationPreparedEvent) {
167                        onApplicationPreparedEvent(event);
168                }
169        }
170
171        private void onApplicationEnvironmentPreparedEvent(
172                        ApplicationEnvironmentPreparedEvent event) {
173                List<EnvironmentPostProcessor> postProcessors = loadPostProcessors();
174                postProcessors.add(this);
175                AnnotationAwareOrderComparator.sort(postProcessors);
176                for (EnvironmentPostProcessor postProcessor : postProcessors) {
177                        postProcessor.postProcessEnvironment(event.getEnvironment(),
178                                        event.getSpringApplication());
179                }
180        }
181
182        List<EnvironmentPostProcessor> loadPostProcessors() {
183                return SpringFactoriesLoader.loadFactories(EnvironmentPostProcessor.class,
184                                getClass().getClassLoader());
185        }
186
187        @Override
188        public void postProcessEnvironment(ConfigurableEnvironment environment,
189                        SpringApplication application) {
190                addPropertySources(environment, application.getResourceLoader());
191        }
192
193        private void onApplicationPreparedEvent(ApplicationEvent event) {
194                this.logger.switchTo(ConfigFileApplicationListener.class);
195                addPostProcessors(((ApplicationPreparedEvent) event).getApplicationContext());
196        }
197
198        /**
199         * Add config file property sources to the specified environment.
200         * @param environment the environment to add source to
201         * @param resourceLoader the resource loader
202         * @see #addPostProcessors(ConfigurableApplicationContext)
203         */
204        protected void addPropertySources(ConfigurableEnvironment environment,
205                        ResourceLoader resourceLoader) {
206                RandomValuePropertySource.addToEnvironment(environment);
207                new Loader(environment, resourceLoader).load();
208        }
209
210        /**
211         * Add appropriate post-processors to post-configure the property-sources.
212         * @param context the context to configure
213         */
214        protected void addPostProcessors(ConfigurableApplicationContext context) {
215                context.addBeanFactoryPostProcessor(
216                                new PropertySourceOrderingPostProcessor(context));
217        }
218
219        public void setOrder(int order) {
220                this.order = order;
221        }
222
223        @Override
224        public int getOrder() {
225                return this.order;
226        }
227
228        /**
229         * Set the search locations that will be considered as a comma-separated list. Each
230         * search location should be a directory path (ending in "/") and it will be prefixed
231         * by the file names constructed from {@link #setSearchNames(String) search names} and
232         * profiles (if any) plus file extensions supported by the properties loaders.
233         * Locations are considered in the order specified, with later items taking precedence
234         * (like a map merge).
235         * @param locations the search locations
236         */
237        public void setSearchLocations(String locations) {
238                Assert.hasLength(locations, "Locations must not be empty");
239                this.searchLocations = locations;
240        }
241
242        /**
243         * Sets the names of the files that should be loaded (excluding file extension) as a
244         * comma-separated list.
245         * @param names the names to load
246         */
247        public void setSearchNames(String names) {
248                Assert.hasLength(names, "Names must not be empty");
249                this.names = names;
250        }
251
252        /**
253         * {@link BeanFactoryPostProcessor} to re-order our property sources below any
254         * {@code @PropertySource} items added by the {@link ConfigurationClassPostProcessor}.
255         */
256        private class PropertySourceOrderingPostProcessor
257                        implements BeanFactoryPostProcessor, Ordered {
258
259                private ConfigurableApplicationContext context;
260
261                PropertySourceOrderingPostProcessor(ConfigurableApplicationContext context) {
262                        this.context = context;
263                }
264
265                @Override
266                public int getOrder() {
267                        return Ordered.HIGHEST_PRECEDENCE;
268                }
269
270                @Override
271                public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)
272                                throws BeansException {
273                        reorderSources(this.context.getEnvironment());
274                }
275
276                private void reorderSources(ConfigurableEnvironment environment) {
277                        PropertySource<?> defaultProperties = environment.getPropertySources()
278                                        .remove(DEFAULT_PROPERTIES);
279                        if (defaultProperties != null) {
280                                environment.getPropertySources().addLast(defaultProperties);
281                        }
282                }
283
284        }
285
286        /**
287         * Loads candidate property sources and configures the active profiles.
288         */
289        private class Loader {
290
291                private final Log logger = ConfigFileApplicationListener.this.logger;
292
293                private final ConfigurableEnvironment environment;
294
295                private final PropertySourcesPlaceholdersResolver placeholdersResolver;
296
297                private final ResourceLoader resourceLoader;
298
299                private final List<PropertySourceLoader> propertySourceLoaders;
300
301                private Deque<Profile> profiles;
302
303                private List<Profile> processedProfiles;
304
305                private boolean activatedProfiles;
306
307                private Map<Profile, MutablePropertySources> loaded;
308
309                private Map<DocumentsCacheKey, List<Document>> loadDocumentsCache = new HashMap<>();
310
311                Loader(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {
312                        this.environment = environment;
313                        this.placeholdersResolver = new PropertySourcesPlaceholdersResolver(
314                                        this.environment);
315                        this.resourceLoader = (resourceLoader != null) ? resourceLoader
316                                        : new DefaultResourceLoader();
317                        this.propertySourceLoaders = SpringFactoriesLoader.loadFactories(
318                                        PropertySourceLoader.class, getClass().getClassLoader());
319                }
320
321                public void load() {
322                        this.profiles = new LinkedList<>();
323                        this.processedProfiles = new LinkedList<>();
324                        this.activatedProfiles = false;
325                        this.loaded = new LinkedHashMap<>();
326                        initializeProfiles();
327                        while (!this.profiles.isEmpty()) {
328                                Profile profile = this.profiles.poll();
329                                if (profile != null && !profile.isDefaultProfile()) {
330                                        addProfileToEnvironment(profile.getName());
331                                }
332                                load(profile, this::getPositiveProfileFilter,
333                                                addToLoaded(MutablePropertySources::addLast, false));
334                                this.processedProfiles.add(profile);
335                        }
336                        resetEnvironmentProfiles(this.processedProfiles);
337                        load(null, this::getNegativeProfileFilter,
338                                        addToLoaded(MutablePropertySources::addFirst, true));
339                        addLoadedPropertySources();
340                }
341
342                /**
343                 * Initialize profile information from both the {@link Environment} active
344                 * profiles and any {@code spring.profiles.active}/{@code spring.profiles.include}
345                 * properties that are already set.
346                 */
347                private void initializeProfiles() {
348                        // The default profile for these purposes is represented as null. We add it
349                        // first so that it is processed first and has lowest priority.
350                        this.profiles.add(null);
351                        Set<Profile> activatedViaProperty = getProfilesActivatedViaProperty();
352                        this.profiles.addAll(getOtherActiveProfiles(activatedViaProperty));
353                        // Any pre-existing active profiles set via property sources (e.g.
354                        // System properties) take precedence over those added in config files.
355                        addActiveProfiles(activatedViaProperty);
356                        if (this.profiles.size() == 1) { // only has null profile
357                                for (String defaultProfileName : this.environment.getDefaultProfiles()) {
358                                        Profile defaultProfile = new Profile(defaultProfileName, true);
359                                        this.profiles.add(defaultProfile);
360                                }
361                        }
362                }
363
364                private Set<Profile> getProfilesActivatedViaProperty() {
365                        if (!this.environment.containsProperty(ACTIVE_PROFILES_PROPERTY)
366                                        && !this.environment.containsProperty(INCLUDE_PROFILES_PROPERTY)) {
367                                return Collections.emptySet();
368                        }
369                        Binder binder = Binder.get(this.environment);
370                        Set<Profile> activeProfiles = new LinkedHashSet<>();
371                        activeProfiles.addAll(getProfiles(binder, INCLUDE_PROFILES_PROPERTY));
372                        activeProfiles.addAll(getProfiles(binder, ACTIVE_PROFILES_PROPERTY));
373                        return activeProfiles;
374                }
375
376                private List<Profile> getOtherActiveProfiles(Set<Profile> activatedViaProperty) {
377                        return Arrays.stream(this.environment.getActiveProfiles()).map(Profile::new)
378                                        .filter((profile) -> !activatedViaProperty.contains(profile))
379                                        .collect(Collectors.toList());
380                }
381
382                void addActiveProfiles(Set<Profile> profiles) {
383                        if (profiles.isEmpty()) {
384                                return;
385                        }
386                        if (this.activatedProfiles) {
387                                if (this.logger.isDebugEnabled()) {
388                                        this.logger.debug("Profiles already activated, '" + profiles
389                                                        + "' will not be applied");
390                                }
391                                return;
392                        }
393                        this.profiles.addAll(profiles);
394                        if (this.logger.isDebugEnabled()) {
395                                this.logger.debug("Activated activeProfiles "
396                                                + StringUtils.collectionToCommaDelimitedString(profiles));
397                        }
398                        this.activatedProfiles = true;
399                        removeUnprocessedDefaultProfiles();
400                }
401
402                private void removeUnprocessedDefaultProfiles() {
403                        this.profiles.removeIf(
404                                        (profile) -> (profile != null && profile.isDefaultProfile()));
405                }
406
407                private DocumentFilter getPositiveProfileFilter(Profile profile) {
408                        return (Document document) -> {
409                                if (profile == null) {
410                                        return ObjectUtils.isEmpty(document.getProfiles());
411                                }
412                                return ObjectUtils.containsElement(document.getProfiles(),
413                                                profile.getName())
414                                                && this.environment
415                                                                .acceptsProfiles(Profiles.of(document.getProfiles()));
416                        };
417                }
418
419                private DocumentFilter getNegativeProfileFilter(Profile profile) {
420                        return (Document document) -> (profile == null
421                                        && !ObjectUtils.isEmpty(document.getProfiles()) && this.environment
422                                                        .acceptsProfiles(Profiles.of(document.getProfiles())));
423                }
424
425                private DocumentConsumer addToLoaded(
426                                BiConsumer<MutablePropertySources, PropertySource<?>> addMethod,
427                                boolean checkForExisting) {
428                        return (profile, document) -> {
429                                if (checkForExisting) {
430                                        for (MutablePropertySources merged : this.loaded.values()) {
431                                                if (merged.contains(document.getPropertySource().getName())) {
432                                                        return;
433                                                }
434                                        }
435                                }
436                                MutablePropertySources merged = this.loaded.computeIfAbsent(profile,
437                                                (k) -> new MutablePropertySources());
438                                addMethod.accept(merged, document.getPropertySource());
439                        };
440                }
441
442                private void load(Profile profile, DocumentFilterFactory filterFactory,
443                                DocumentConsumer consumer) {
444                        getSearchLocations().forEach((location) -> {
445                                boolean isFolder = location.endsWith("/");
446                                Set<String> names = isFolder ? getSearchNames() : NO_SEARCH_NAMES;
447                                names.forEach(
448                                                (name) -> load(location, name, profile, filterFactory, consumer));
449                        });
450                }
451
452                private void load(String location, String name, Profile profile,
453                                DocumentFilterFactory filterFactory, DocumentConsumer consumer) {
454                        if (!StringUtils.hasText(name)) {
455                                for (PropertySourceLoader loader : this.propertySourceLoaders) {
456                                        if (canLoadFileExtension(loader, location)) {
457                                                load(loader, location, profile,
458                                                                filterFactory.getDocumentFilter(profile), consumer);
459                                                return;
460                                        }
461                                }
462                        }
463                        Set<String> processed = new HashSet<>();
464                        for (PropertySourceLoader loader : this.propertySourceLoaders) {
465                                for (String fileExtension : loader.getFileExtensions()) {
466                                        if (processed.add(fileExtension)) {
467                                                loadForFileExtension(loader, location + name, "." + fileExtension,
468                                                                profile, filterFactory, consumer);
469                                        }
470                                }
471                        }
472                }
473
474                private boolean canLoadFileExtension(PropertySourceLoader loader, String name) {
475                        return Arrays.stream(loader.getFileExtensions())
476                                        .anyMatch((fileExtension) -> StringUtils.endsWithIgnoreCase(name,
477                                                        fileExtension));
478                }
479
480                private void loadForFileExtension(PropertySourceLoader loader, String prefix,
481                                String fileExtension, Profile profile,
482                                DocumentFilterFactory filterFactory, DocumentConsumer consumer) {
483                        DocumentFilter defaultFilter = filterFactory.getDocumentFilter(null);
484                        DocumentFilter profileFilter = filterFactory.getDocumentFilter(profile);
485                        if (profile != null) {
486                                // Try profile-specific file & profile section in profile file (gh-340)
487                                String profileSpecificFile = prefix + "-" + profile + fileExtension;
488                                load(loader, profileSpecificFile, profile, defaultFilter, consumer);
489                                load(loader, profileSpecificFile, profile, profileFilter, consumer);
490                                // Try profile specific sections in files we've already processed
491                                for (Profile processedProfile : this.processedProfiles) {
492                                        if (processedProfile != null) {
493                                                String previouslyLoaded = prefix + "-" + processedProfile
494                                                                + fileExtension;
495                                                load(loader, previouslyLoaded, profile, profileFilter, consumer);
496                                        }
497                                }
498                        }
499                        // Also try the profile-specific section (if any) of the normal file
500                        load(loader, prefix + fileExtension, profile, profileFilter, consumer);
501                }
502
503                private void load(PropertySourceLoader loader, String location, Profile profile,
504                                DocumentFilter filter, DocumentConsumer consumer) {
505                        try {
506                                Resource resource = this.resourceLoader.getResource(location);
507                                if (resource == null || !resource.exists()) {
508                                        if (this.logger.isTraceEnabled()) {
509                                                StringBuilder description = getDescription(
510                                                                "Skipped missing config ", location, resource, profile);
511                                                this.logger.trace(description);
512                                        }
513                                        return;
514                                }
515                                if (!StringUtils.hasText(
516                                                StringUtils.getFilenameExtension(resource.getFilename()))) {
517                                        if (this.logger.isTraceEnabled()) {
518                                                StringBuilder description = getDescription(
519                                                                "Skipped empty config extension ", location, resource,
520                                                                profile);
521                                                this.logger.trace(description);
522                                        }
523                                        return;
524                                }
525                                String name = "applicationConfig: [" + location + "]";
526                                List<Document> documents = loadDocuments(loader, name, resource);
527                                if (CollectionUtils.isEmpty(documents)) {
528                                        if (this.logger.isTraceEnabled()) {
529                                                StringBuilder description = getDescription(
530                                                                "Skipped unloaded config ", location, resource, profile);
531                                                this.logger.trace(description);
532                                        }
533                                        return;
534                                }
535                                List<Document> loaded = new ArrayList<>();
536                                for (Document document : documents) {
537                                        if (filter.match(document)) {
538                                                addActiveProfiles(document.getActiveProfiles());
539                                                addIncludedProfiles(document.getIncludeProfiles());
540                                                loaded.add(document);
541                                        }
542                                }
543                                Collections.reverse(loaded);
544                                if (!loaded.isEmpty()) {
545                                        loaded.forEach((document) -> consumer.accept(profile, document));
546                                        if (this.logger.isDebugEnabled()) {
547                                                StringBuilder description = getDescription("Loaded config file ",
548                                                                location, resource, profile);
549                                                this.logger.debug(description);
550                                        }
551                                }
552                        }
553                        catch (Exception ex) {
554                                throw new IllegalStateException("Failed to load property "
555                                                + "source from location '" + location + "'", ex);
556                        }
557                }
558
559                private void addIncludedProfiles(Set<Profile> includeProfiles) {
560                        LinkedList<Profile> existingProfiles = new LinkedList<>(this.profiles);
561                        this.profiles.clear();
562                        this.profiles.addAll(includeProfiles);
563                        this.profiles.removeAll(this.processedProfiles);
564                        this.profiles.addAll(existingProfiles);
565                }
566
567                private List<Document> loadDocuments(PropertySourceLoader loader, String name,
568                                Resource resource) throws IOException {
569                        DocumentsCacheKey cacheKey = new DocumentsCacheKey(loader, resource);
570                        List<Document> documents = this.loadDocumentsCache.get(cacheKey);
571                        if (documents == null) {
572                                List<PropertySource<?>> loaded = loader.load(name, resource);
573                                documents = asDocuments(loaded);
574                                this.loadDocumentsCache.put(cacheKey, documents);
575                        }
576                        return documents;
577                }
578
579                private List<Document> asDocuments(List<PropertySource<?>> loaded) {
580                        if (loaded == null) {
581                                return Collections.emptyList();
582                        }
583                        return loaded.stream().map((propertySource) -> {
584                                Binder binder = new Binder(
585                                                ConfigurationPropertySources.from(propertySource),
586                                                this.placeholdersResolver);
587                                return new Document(propertySource,
588                                                binder.bind("spring.profiles", STRING_ARRAY).orElse(null),
589                                                getProfiles(binder, ACTIVE_PROFILES_PROPERTY),
590                                                getProfiles(binder, INCLUDE_PROFILES_PROPERTY));
591                        }).collect(Collectors.toList());
592                }
593
594                private StringBuilder getDescription(String prefix, String location,
595                                Resource resource, Profile profile) {
596                        StringBuilder result = new StringBuilder(prefix);
597                        try {
598                                if (resource != null) {
599                                        String uri = resource.getURI().toASCIIString();
600                                        result.append("'");
601                                        result.append(uri);
602                                        result.append("' (");
603                                        result.append(location);
604                                        result.append(")");
605                                }
606                        }
607                        catch (IOException ex) {
608                                result.append(location);
609                        }
610                        if (profile != null) {
611                                result.append(" for profile ");
612                                result.append(profile);
613                        }
614                        return result;
615                }
616
617                private Set<Profile> getProfiles(Binder binder, String name) {
618                        return binder.bind(name, STRING_ARRAY).map(this::asProfileSet)
619                                        .orElse(Collections.emptySet());
620                }
621
622                private Set<Profile> asProfileSet(String[] profileNames) {
623                        List<Profile> profiles = new ArrayList<>();
624                        for (String profileName : profileNames) {
625                                profiles.add(new Profile(profileName));
626                        }
627                        return new LinkedHashSet<>(profiles);
628                }
629
630                private void addProfileToEnvironment(String profile) {
631                        for (String activeProfile : this.environment.getActiveProfiles()) {
632                                if (activeProfile.equals(profile)) {
633                                        return;
634                                }
635                        }
636                        this.environment.addActiveProfile(profile);
637                }
638
639                private Set<String> getSearchLocations() {
640                        if (this.environment.containsProperty(CONFIG_LOCATION_PROPERTY)) {
641                                return getSearchLocations(CONFIG_LOCATION_PROPERTY);
642                        }
643                        Set<String> locations = getSearchLocations(
644                                        CONFIG_ADDITIONAL_LOCATION_PROPERTY);
645                        locations.addAll(
646                                        asResolvedSet(ConfigFileApplicationListener.this.searchLocations,
647                                                        DEFAULT_SEARCH_LOCATIONS));
648                        return locations;
649                }
650
651                private Set<String> getSearchLocations(String propertyName) {
652                        Set<String> locations = new LinkedHashSet<>();
653                        if (this.environment.containsProperty(propertyName)) {
654                                for (String path : asResolvedSet(
655                                                this.environment.getProperty(propertyName), null)) {
656                                        if (!path.contains("$")) {
657                                                path = StringUtils.cleanPath(path);
658                                                if (!ResourceUtils.isUrl(path)) {
659                                                        path = ResourceUtils.FILE_URL_PREFIX + path;
660                                                }
661                                        }
662                                        locations.add(path);
663                                }
664                        }
665                        return locations;
666                }
667
668                private Set<String> getSearchNames() {
669                        if (this.environment.containsProperty(CONFIG_NAME_PROPERTY)) {
670                                String property = this.environment.getProperty(CONFIG_NAME_PROPERTY);
671                                return asResolvedSet(property, null);
672                        }
673                        return asResolvedSet(ConfigFileApplicationListener.this.names, DEFAULT_NAMES);
674                }
675
676                private Set<String> asResolvedSet(String value, String fallback) {
677                        List<String> list = Arrays.asList(StringUtils.trimArrayElements(
678                                        StringUtils.commaDelimitedListToStringArray((value != null)
679                                                        ? this.environment.resolvePlaceholders(value) : fallback)));
680                        Collections.reverse(list);
681                        return new LinkedHashSet<>(list);
682                }
683
684                /**
685                 * This ensures that the order of active profiles in the {@link Environment}
686                 * matches the order in which the profiles were processed.
687                 * @param processedProfiles the processed profiles
688                 */
689                private void resetEnvironmentProfiles(List<Profile> processedProfiles) {
690                        String[] names = processedProfiles.stream()
691                                        .filter((profile) -> profile != null && !profile.isDefaultProfile())
692                                        .map(Profile::getName).toArray(String[]::new);
693                        this.environment.setActiveProfiles(names);
694                }
695
696                private void addLoadedPropertySources() {
697                        MutablePropertySources destination = this.environment.getPropertySources();
698                        List<MutablePropertySources> loaded = new ArrayList<>(this.loaded.values());
699                        Collections.reverse(loaded);
700                        String lastAdded = null;
701                        Set<String> added = new HashSet<>();
702                        for (MutablePropertySources sources : loaded) {
703                                for (PropertySource<?> source : sources) {
704                                        if (added.add(source.getName())) {
705                                                addLoadedPropertySource(destination, lastAdded, source);
706                                                lastAdded = source.getName();
707                                        }
708                                }
709                        }
710                }
711
712                private void addLoadedPropertySource(MutablePropertySources destination,
713                                String lastAdded, PropertySource<?> source) {
714                        if (lastAdded == null) {
715                                if (destination.contains(DEFAULT_PROPERTIES)) {
716                                        destination.addBefore(DEFAULT_PROPERTIES, source);
717                                }
718                                else {
719                                        destination.addLast(source);
720                                }
721                        }
722                        else {
723                                destination.addAfter(lastAdded, source);
724                        }
725                }
726
727        }
728
729        /**
730         * A Spring Profile that can be loaded.
731         */
732        private static class Profile {
733
734                private final String name;
735
736                private final boolean defaultProfile;
737
738                Profile(String name) {
739                        this(name, false);
740                }
741
742                Profile(String name, boolean defaultProfile) {
743                        Assert.notNull(name, "Name must not be null");
744                        this.name = name;
745                        this.defaultProfile = defaultProfile;
746                }
747
748                public String getName() {
749                        return this.name;
750                }
751
752                public boolean isDefaultProfile() {
753                        return this.defaultProfile;
754                }
755
756                @Override
757                public boolean equals(Object obj) {
758                        if (obj == this) {
759                                return true;
760                        }
761                        if (obj == null || obj.getClass() != getClass()) {
762                                return false;
763                        }
764                        return ((Profile) obj).name.equals(this.name);
765                }
766
767                @Override
768                public int hashCode() {
769                        return this.name.hashCode();
770                }
771
772                @Override
773                public String toString() {
774                        return this.name;
775                }
776
777        }
778
779        /**
780         * Cache key used to save loading the same document multiple times.
781         */
782        private static class DocumentsCacheKey {
783
784                private final PropertySourceLoader loader;
785
786                private final Resource resource;
787
788                DocumentsCacheKey(PropertySourceLoader loader, Resource resource) {
789                        this.loader = loader;
790                        this.resource = resource;
791                }
792
793                @Override
794                public boolean equals(Object obj) {
795                        if (this == obj) {
796                                return true;
797                        }
798                        if (obj == null || getClass() != obj.getClass()) {
799                                return false;
800                        }
801                        DocumentsCacheKey other = (DocumentsCacheKey) obj;
802                        return this.loader.equals(other.loader)
803                                        && this.resource.equals(other.resource);
804                }
805
806                @Override
807                public int hashCode() {
808                        return this.loader.hashCode() * 31 + this.resource.hashCode();
809                }
810
811        }
812
813        /**
814         * A single document loaded by a {@link PropertySourceLoader}.
815         */
816        private static class Document {
817
818                private final PropertySource<?> propertySource;
819
820                private String[] profiles;
821
822                private final Set<Profile> activeProfiles;
823
824                private final Set<Profile> includeProfiles;
825
826                Document(PropertySource<?> propertySource, String[] profiles,
827                                Set<Profile> activeProfiles, Set<Profile> includeProfiles) {
828                        this.propertySource = propertySource;
829                        this.profiles = profiles;
830                        this.activeProfiles = activeProfiles;
831                        this.includeProfiles = includeProfiles;
832                }
833
834                public PropertySource<?> getPropertySource() {
835                        return this.propertySource;
836                }
837
838                public String[] getProfiles() {
839                        return this.profiles;
840                }
841
842                public Set<Profile> getActiveProfiles() {
843                        return this.activeProfiles;
844                }
845
846                public Set<Profile> getIncludeProfiles() {
847                        return this.includeProfiles;
848                }
849
850                @Override
851                public String toString() {
852                        return this.propertySource.toString();
853                }
854
855        }
856
857        /**
858         * Factory used to create a {@link DocumentFilter}.
859         */
860        @FunctionalInterface
861        private interface DocumentFilterFactory {
862
863                /**
864                 * Create a filter for the given profile.
865                 * @param profile the profile or {@code null}
866                 * @return the filter
867                 */
868                DocumentFilter getDocumentFilter(Profile profile);
869
870        }
871
872        /**
873         * Filter used to restrict when a {@link Document} is loaded.
874         */
875        @FunctionalInterface
876        private interface DocumentFilter {
877
878                boolean match(Document document);
879
880        }
881
882        /**
883         * Consumer used to handle a loaded {@link Document}.
884         */
885        @FunctionalInterface
886        private interface DocumentConsumer {
887
888                void accept(Profile profile, Document document);
889
890        }
891
892}