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.annotation;
018
019import java.util.ArrayList;
020import java.util.Arrays;
021import java.util.Collection;
022import java.util.Collections;
023import java.util.Comparator;
024import java.util.LinkedHashSet;
025import java.util.LinkedList;
026import java.util.List;
027import java.util.Set;
028import java.util.stream.Collectors;
029import java.util.stream.Stream;
030
031import org.springframework.context.ApplicationContext;
032import org.springframework.context.annotation.Configuration;
033import org.springframework.context.annotation.ImportSelector;
034import org.springframework.core.OrderComparator;
035import org.springframework.core.Ordered;
036import org.springframework.test.context.junit4.SpringRunner;
037import org.springframework.util.Assert;
038import org.springframework.util.ClassUtils;
039
040/**
041 * A set of {@link Configuration @Configuration} classes that can be registered in
042 * {@link ApplicationContext}. Classes can be returned from one or more
043 * {@link Configurations} instances by using {@link #getClasses(Configurations[])}. The
044 * resulting array follows the ordering rules usually applied by the
045 * {@link ApplicationContext} and/or custom {@link ImportSelector} implementations.
046 * <p>
047 * This class is primarily intended for use with tests that need to specify configuration
048 * classes but can't use {@link SpringRunner}.
049 * <p>
050 * Implementations of this class should be annotated with {@code @Order} or implement
051 * {@link Ordered}.
052 *
053 * @author Phillip Webb
054 * @since 2.0.0
055 * @see UserConfigurations
056 */
057public abstract class Configurations {
058
059        private static final Comparator<Object> COMPARATOR = OrderComparator.INSTANCE
060                        .thenComparing((other) -> other.getClass().getName());
061
062        private final Set<Class<?>> classes;
063
064        protected Configurations(Collection<Class<?>> classes) {
065                Assert.notNull(classes, "Classes must not be null");
066                Collection<Class<?>> sorted = sort(classes);
067                this.classes = Collections.unmodifiableSet(new LinkedHashSet<>(sorted));
068        }
069
070        /**
071         * Sort configuration classes into the order that they should be applied.
072         * @param classes the classes to sort
073         * @return a sorted set of classes
074         */
075        protected Collection<Class<?>> sort(Collection<Class<?>> classes) {
076                return classes;
077        }
078
079        protected final Set<Class<?>> getClasses() {
080                return this.classes;
081        }
082
083        /**
084         * Merge configurations from another source of the same type.
085         * @param other the other {@link Configurations} (must be of the same type as this
086         * instance)
087         * @return a new configurations instance (must be of the same type as this instance)
088         */
089        protected Configurations merge(Configurations other) {
090                Set<Class<?>> mergedClasses = new LinkedHashSet<>(getClasses());
091                mergedClasses.addAll(other.getClasses());
092                return merge(mergedClasses);
093        }
094
095        /**
096         * Merge configurations.
097         * @param mergedClasses the merged classes
098         * @return a new configurations instance (must be of the same type as this instance)
099         */
100        protected abstract Configurations merge(Set<Class<?>> mergedClasses);
101
102        /**
103         * Return the classes from all the specified configurations in the order that they
104         * would be registered.
105         * @param configurations the source configuration
106         * @return configuration classes in registration order
107         */
108        public static Class<?>[] getClasses(Configurations... configurations) {
109                return getClasses(Arrays.asList(configurations));
110        }
111
112        /**
113         * Return the classes from all the specified configurations in the order that they
114         * would be registered.
115         * @param configurations the source configuration
116         * @return configuration classes in registration order
117         */
118        public static Class<?>[] getClasses(Collection<Configurations> configurations) {
119                List<Configurations> ordered = new ArrayList<>(configurations);
120                ordered.sort(COMPARATOR);
121                List<Configurations> collated = collate(ordered);
122                LinkedHashSet<Class<?>> classes = collated.stream()
123                                .flatMap(Configurations::streamClasses)
124                                .collect(Collectors.toCollection(LinkedHashSet::new));
125                return ClassUtils.toClassArray(classes);
126        }
127
128        private static Stream<Class<?>> streamClasses(Configurations configurations) {
129                return configurations.getClasses().stream();
130        }
131
132        private static List<Configurations> collate(
133                        List<Configurations> orderedConfigurations) {
134                LinkedList<Configurations> collated = new LinkedList<>();
135                for (Configurations item : orderedConfigurations) {
136                        if (collated.isEmpty() || collated.getLast().getClass() != item.getClass()) {
137                                collated.add(item);
138                        }
139                        else {
140                                collated.set(collated.size() - 1, collated.getLast().merge(item));
141                        }
142                }
143                return collated;
144        }
145
146}