From 0326abfe01903c6d71570ff69c79eaa2a82958c3 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Tue, 2 Sep 2014 14:27:33 -0700 Subject: [PATCH] Support for composed "any" condition Add `AnyNestedCondition` which can be used to create a logical 'or' of other conditions contained on nested classes. For example: static class OnJndiOrProperty extends AnyNestedCondition { @ConditionalOnJndi() static class OnJndi { } @ConditionalOnProperty("something") static class OnProperty { } } Fixes gh-1490 --- .../condition/AnyNestedCondition.java | 179 ++++++++++++++++++ .../condition/ConditionOutcome.java | 4 + .../condition/AnyNestedConditionTests.java | 101 ++++++++++ 3 files changed, 284 insertions(+) create mode 100644 spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/AnyNestedCondition.java create mode 100644 spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/AnyNestedConditionTests.java diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/AnyNestedCondition.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/AnyNestedCondition.java new file mode 100644 index 0000000000..abbad780b8 --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/AnyNestedCondition.java @@ -0,0 +1,179 @@ +/* + * Copyright 2012-2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.condition; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.springframework.beans.BeanUtils; +import org.springframework.context.annotation.Condition; +import org.springframework.context.annotation.ConditionContext; +import org.springframework.context.annotation.Conditional; +import org.springframework.context.annotation.ConfigurationCondition; +import org.springframework.core.type.AnnotatedTypeMetadata; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.core.type.classreading.MetadataReaderFactory; +import org.springframework.core.type.classreading.SimpleMetadataReaderFactory; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.util.StringUtils; + +/** + * {@link Condition} that will match when any nested class condition matches. Can be used + * to create composite conditions, for example: + * + *
+ * static class OnJndiOrProperty extends AnyNestedCondition {
+ *
+ *    @ConditionalOnJndi()
+ *    static class OnJndi {
+ *    }
+
+ *    @ConditionalOnProperty("something")
+ *    static class OnProperty {
+ *    }
+ *
+ * }
+ * 
+ * + * @author Phillip Webb + * @since 1.2.0 + */ +public abstract class AnyNestedCondition extends SpringBootCondition implements + ConfigurationCondition { + + private final ConfigurationPhase configurationPhase; + + public AnyNestedCondition(ConfigurationPhase configurationPhase) { + Assert.notNull(configurationPhase, "ConfigurationPhase must not be null"); + this.configurationPhase = configurationPhase; + } + + @Override + public ConfigurationPhase getConfigurationPhase() { + return this.configurationPhase; + } + + @Override + public ConditionOutcome getMatchOutcome(ConditionContext context, + AnnotatedTypeMetadata metadata) { + MemberConditions memberConditions = new MemberConditions(context, getClass() + .getName()); + List outcomes = memberConditions.getMatchOutcomes(); + List match = new ArrayList(); + List nonMatch = new ArrayList(); + for (ConditionOutcome outcome : outcomes) { + if (outcome.isMatch()) { + match.add(outcome); + } + else { + nonMatch.add(outcome); + } + } + return new ConditionOutcome(match.size() > 0, "any match resulted in " + match + + " matches and " + nonMatch + " non matches"); + } + + private static class MemberConditions { + + private final ConditionContext context; + + private final MetadataReaderFactory readerFactory; + + private final Map> memberConditions; + + public MemberConditions(ConditionContext context, String className) { + this.context = context; + this.readerFactory = new SimpleMetadataReaderFactory( + context.getResourceLoader()); + String[] members = getMetadata(className).getMemberClassNames(); + this.memberConditions = getMemberConditions(members); + } + + private Map> getMemberConditions( + String[] members) { + MultiValueMap memberConditions = new LinkedMultiValueMap(); + for (String member : members) { + AnnotationMetadata metadata = getMetadata(member); + for (String[] conditionClasses : getConditionClasses(metadata)) { + for (String conditionClass : conditionClasses) { + Condition condition = getCondition(conditionClass); + memberConditions.add(metadata, condition); + } + } + } + return Collections.unmodifiableMap(memberConditions); + } + + private AnnotationMetadata getMetadata(String className) { + try { + return this.readerFactory.getMetadataReader(className) + .getAnnotationMetadata(); + } + catch (IOException ex) { + throw new IllegalStateException(ex); + } + } + + @SuppressWarnings("unchecked") + private List getConditionClasses(AnnotatedTypeMetadata metadata) { + MultiValueMap attributes = metadata + .getAllAnnotationAttributes(Conditional.class.getName(), true); + Object values = (attributes != null ? attributes.get("value") : null); + return (List) (values != null ? values : Collections.emptyList()); + } + + private Condition getCondition(String conditionClassName) { + Class conditionClass = ClassUtils.resolveClassName(conditionClassName, + this.context.getClassLoader()); + return (Condition) BeanUtils.instantiateClass(conditionClass); + } + + public List getMatchOutcomes() { + List outcomes = new ArrayList(); + for (Map.Entry> entry : this.memberConditions + .entrySet()) { + AnnotationMetadata metadata = entry.getKey(); + for (Condition condition : entry.getValue()) { + outcomes.add(getConditionOutcome(metadata, condition)); + } + } + return Collections.unmodifiableList(outcomes); + } + + private ConditionOutcome getConditionOutcome(AnnotationMetadata metadata, + Condition condition) { + String messagePrefix = "member condition on " + metadata.getClassName(); + if (condition instanceof SpringBootCondition) { + ConditionOutcome outcome = ((SpringBootCondition) condition) + .getMatchOutcome(this.context, metadata); + String message = outcome.getMessage(); + return new ConditionOutcome(outcome.isMatch(), messagePrefix + + (StringUtils.hasLength(message) ? " : " + message : "")); + } + boolean matches = condition.matches(this.context, metadata); + return new ConditionOutcome(matches, messagePrefix); + } + + } + +} diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionOutcome.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionOutcome.java index 015bd58fd1..846895b6dc 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionOutcome.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionOutcome.java @@ -93,4 +93,8 @@ public class ConditionOutcome { return super.equals(obj); } + @Override + public String toString() { + return (this.message == null ? "" : this.message); + } } diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/AnyNestedConditionTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/AnyNestedConditionTests.java new file mode 100644 index 0000000000..ad1fbb1d12 --- /dev/null +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/AnyNestedConditionTests.java @@ -0,0 +1,101 @@ +/* + * Copyright 2012-2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.condition; + +import org.junit.Test; +import org.springframework.boot.test.EnvironmentTestUtils; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; +import org.springframework.context.annotation.Configuration; + +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertThat; + +/** + * Tests for {@link AnyNestedCondition}. + * + * @author Phillip Webb + */ +public class AnyNestedConditionTests { + + @Test + public void neither() throws Exception { + AnnotationConfigApplicationContext context = load(OnPropertyAorBCondition.class); + assertThat(context.containsBean("myBean"), equalTo(false)); + context.close(); + } + + @Test + public void propertyA() throws Exception { + AnnotationConfigApplicationContext context = load(Config.class, "a:a"); + assertThat(context.containsBean("myBean"), equalTo(true)); + context.close(); + } + + @Test + public void propertyB() throws Exception { + AnnotationConfigApplicationContext context = load(Config.class, "b:b"); + assertThat(context.containsBean("myBean"), equalTo(true)); + context.close(); + } + + @Test + public void both() throws Exception { + AnnotationConfigApplicationContext context = load(Config.class, "a:a", "b:b"); + assertThat(context.containsBean("myBean"), equalTo(true)); + context.close(); + } + + private AnnotationConfigApplicationContext load(Class config, String... env) { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + EnvironmentTestUtils.addEnvironment(context, env); + context.register(config); + context.refresh(); + return context; + } + + @Configuration + @Conditional(OnPropertyAorBCondition.class) + public static class Config { + + @Bean + public String myBean() { + return "myBean"; + } + + } + + static class OnPropertyAorBCondition extends AnyNestedCondition { + + public OnPropertyAorBCondition() { + super(ConfigurationPhase.PARSE_CONFIGURATION); + } + + @ConditionalOnProperty("a") + static class HasPropertyA { + + } + + @ConditionalOnProperty("b") + static class HasPropertyB { + + } + + } + +}