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}