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.autoconfigure.condition;
018
019import java.util.ArrayList;
020import java.util.Collection;
021import java.util.Collections;
022import java.util.HashSet;
023import java.util.Iterator;
024import java.util.LinkedHashSet;
025import java.util.List;
026import java.util.Map;
027import java.util.Set;
028import java.util.SortedMap;
029import java.util.TreeMap;
030
031import org.springframework.beans.factory.BeanFactory;
032import org.springframework.beans.factory.config.ConfigurableBeanFactory;
033import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
034import org.springframework.context.annotation.Condition;
035import org.springframework.context.annotation.ConditionContext;
036import org.springframework.core.type.AnnotatedTypeMetadata;
037import org.springframework.util.Assert;
038import org.springframework.util.ObjectUtils;
039
040/**
041 * Records condition evaluation details for reporting and logging.
042 *
043 * @author Greg Turnquist
044 * @author Dave Syer
045 * @author Phillip Webb
046 * @author Andy Wilkinson
047 * @author Stephane Nicoll
048 */
049public final class ConditionEvaluationReport {
050
051        private static final String BEAN_NAME = "autoConfigurationReport";
052
053        private static final AncestorsMatchedCondition ANCESTOR_CONDITION = new AncestorsMatchedCondition();
054
055        private final SortedMap<String, ConditionAndOutcomes> outcomes = new TreeMap<>();
056
057        private boolean addedAncestorOutcomes;
058
059        private ConditionEvaluationReport parent;
060
061        private final List<String> exclusions = new ArrayList<>();
062
063        private final Set<String> unconditionalClasses = new HashSet<>();
064
065        /**
066         * Private constructor.
067         * @see #get(ConfigurableListableBeanFactory)
068         */
069        private ConditionEvaluationReport() {
070        }
071
072        /**
073         * Record the occurrence of condition evaluation.
074         * @param source the source of the condition (class or method name)
075         * @param condition the condition evaluated
076         * @param outcome the condition outcome
077         */
078        public void recordConditionEvaluation(String source, Condition condition,
079                        ConditionOutcome outcome) {
080                Assert.notNull(source, "Source must not be null");
081                Assert.notNull(condition, "Condition must not be null");
082                Assert.notNull(outcome, "Outcome must not be null");
083                this.unconditionalClasses.remove(source);
084                if (!this.outcomes.containsKey(source)) {
085                        this.outcomes.put(source, new ConditionAndOutcomes());
086                }
087                this.outcomes.get(source).add(condition, outcome);
088                this.addedAncestorOutcomes = false;
089        }
090
091        /**
092         * Records the names of the classes that have been excluded from condition evaluation.
093         * @param exclusions the names of the excluded classes
094         */
095        public void recordExclusions(Collection<String> exclusions) {
096                Assert.notNull(exclusions, "exclusions must not be null");
097                this.exclusions.addAll(exclusions);
098        }
099
100        /**
101         * Records the names of the classes that are candidates for condition evaluation.
102         * @param evaluationCandidates the names of the classes whose conditions will be
103         * evaluated
104         */
105        public void recordEvaluationCandidates(List<String> evaluationCandidates) {
106                Assert.notNull(evaluationCandidates, "evaluationCandidates must not be null");
107                this.unconditionalClasses.addAll(evaluationCandidates);
108        }
109
110        /**
111         * Returns condition outcomes from this report, grouped by the source.
112         * @return the condition outcomes
113         */
114        public Map<String, ConditionAndOutcomes> getConditionAndOutcomesBySource() {
115                if (!this.addedAncestorOutcomes) {
116                        this.outcomes.forEach((source, sourceOutcomes) -> {
117                                if (!sourceOutcomes.isFullMatch()) {
118                                        addNoMatchOutcomeToAncestors(source);
119                                }
120                        });
121                        this.addedAncestorOutcomes = true;
122                }
123                return Collections.unmodifiableMap(this.outcomes);
124        }
125
126        private void addNoMatchOutcomeToAncestors(String source) {
127                String prefix = source + "$";
128                this.outcomes.forEach((candidateSource, sourceOutcomes) -> {
129                        if (candidateSource.startsWith(prefix)) {
130                                ConditionOutcome outcome = ConditionOutcome.noMatch(ConditionMessage
131                                                .forCondition("Ancestor " + source).because("did not match"));
132                                sourceOutcomes.add(ANCESTOR_CONDITION, outcome);
133                        }
134                });
135        }
136
137        /**
138         * Returns the names of the classes that have been excluded from condition evaluation.
139         * @return the names of the excluded classes
140         */
141        public List<String> getExclusions() {
142                return Collections.unmodifiableList(this.exclusions);
143        }
144
145        /**
146         * Returns the names of the classes that were evaluated but were not conditional.
147         * @return the names of the unconditional classes
148         */
149        public Set<String> getUnconditionalClasses() {
150                Set<String> filtered = new HashSet<>(this.unconditionalClasses);
151                filtered.removeAll(this.exclusions);
152                return Collections.unmodifiableSet(filtered);
153        }
154
155        /**
156         * The parent report (from a parent BeanFactory if there is one).
157         * @return the parent report (or null if there isn't one)
158         */
159        public ConditionEvaluationReport getParent() {
160                return this.parent;
161        }
162
163        /**
164         * Attempt to find the {@link ConditionEvaluationReport} for the specified bean
165         * factory.
166         * @param beanFactory the bean factory (may be {@code null})
167         * @return the {@link ConditionEvaluationReport} or {@code null}
168         */
169        public static ConditionEvaluationReport find(BeanFactory beanFactory) {
170                if (beanFactory != null && beanFactory instanceof ConfigurableBeanFactory) {
171                        return ConditionEvaluationReport
172                                        .get((ConfigurableListableBeanFactory) beanFactory);
173                }
174                return null;
175        }
176
177        /**
178         * Obtain a {@link ConditionEvaluationReport} for the specified bean factory.
179         * @param beanFactory the bean factory
180         * @return an existing or new {@link ConditionEvaluationReport}
181         */
182        public static ConditionEvaluationReport get(
183                        ConfigurableListableBeanFactory beanFactory) {
184                synchronized (beanFactory) {
185                        ConditionEvaluationReport report;
186                        if (beanFactory.containsSingleton(BEAN_NAME)) {
187                                report = beanFactory.getBean(BEAN_NAME, ConditionEvaluationReport.class);
188                        }
189                        else {
190                                report = new ConditionEvaluationReport();
191                                beanFactory.registerSingleton(BEAN_NAME, report);
192                        }
193                        locateParent(beanFactory.getParentBeanFactory(), report);
194                        return report;
195                }
196        }
197
198        private static void locateParent(BeanFactory beanFactory,
199                        ConditionEvaluationReport report) {
200                if (beanFactory != null && report.parent == null
201                                && beanFactory.containsBean(BEAN_NAME)) {
202                        report.parent = beanFactory.getBean(BEAN_NAME,
203                                        ConditionEvaluationReport.class);
204                }
205        }
206
207        public ConditionEvaluationReport getDelta(ConditionEvaluationReport previousReport) {
208                ConditionEvaluationReport delta = new ConditionEvaluationReport();
209                this.outcomes.forEach((source, sourceOutcomes) -> {
210                        ConditionAndOutcomes previous = previousReport.outcomes.get(source);
211                        if (previous == null
212                                        || previous.isFullMatch() != sourceOutcomes.isFullMatch()) {
213                                sourceOutcomes.forEach(
214                                                (conditionAndOutcome) -> delta.recordConditionEvaluation(source,
215                                                                conditionAndOutcome.getCondition(),
216                                                                conditionAndOutcome.getOutcome()));
217                        }
218                });
219                List<String> newExclusions = new ArrayList<>(this.exclusions);
220                newExclusions.removeAll(previousReport.getExclusions());
221                delta.recordExclusions(newExclusions);
222                List<String> newUnconditionalClasses = new ArrayList<>(this.unconditionalClasses);
223                newUnconditionalClasses.removeAll(previousReport.unconditionalClasses);
224                delta.unconditionalClasses.addAll(newUnconditionalClasses);
225                return delta;
226        }
227
228        /**
229         * Provides access to a number of {@link ConditionAndOutcome} items.
230         */
231        public static class ConditionAndOutcomes implements Iterable<ConditionAndOutcome> {
232
233                private final Set<ConditionAndOutcome> outcomes = new LinkedHashSet<>();
234
235                public void add(Condition condition, ConditionOutcome outcome) {
236                        this.outcomes.add(new ConditionAndOutcome(condition, outcome));
237                }
238
239                /**
240                 * Return {@code true} if all outcomes match.
241                 * @return {@code true} if a full match
242                 */
243                public boolean isFullMatch() {
244                        for (ConditionAndOutcome conditionAndOutcomes : this) {
245                                if (!conditionAndOutcomes.getOutcome().isMatch()) {
246                                        return false;
247                                }
248                        }
249                        return true;
250                }
251
252                @Override
253                public Iterator<ConditionAndOutcome> iterator() {
254                        return Collections.unmodifiableSet(this.outcomes).iterator();
255                }
256
257        }
258
259        /**
260         * Provides access to a single {@link Condition} and {@link ConditionOutcome}.
261         */
262        public static class ConditionAndOutcome {
263
264                private final Condition condition;
265
266                private final ConditionOutcome outcome;
267
268                public ConditionAndOutcome(Condition condition, ConditionOutcome outcome) {
269                        this.condition = condition;
270                        this.outcome = outcome;
271                }
272
273                public Condition getCondition() {
274                        return this.condition;
275                }
276
277                public ConditionOutcome getOutcome() {
278                        return this.outcome;
279                }
280
281                @Override
282                public boolean equals(Object obj) {
283                        if (this == obj) {
284                                return true;
285                        }
286                        if (obj == null || getClass() != obj.getClass()) {
287                                return false;
288                        }
289                        ConditionAndOutcome other = (ConditionAndOutcome) obj;
290                        return (ObjectUtils.nullSafeEquals(this.condition.getClass(),
291                                        other.condition.getClass())
292                                        && ObjectUtils.nullSafeEquals(this.outcome, other.outcome));
293                }
294
295                @Override
296                public int hashCode() {
297                        return this.condition.getClass().hashCode() * 31 + this.outcome.hashCode();
298                }
299
300                @Override
301                public String toString() {
302                        return this.condition.getClass() + " " + this.outcome;
303                }
304
305        }
306
307        private static class AncestorsMatchedCondition implements Condition {
308
309                @Override
310                public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
311                        throw new UnsupportedOperationException();
312                }
313
314        }
315
316}