From 34420a8768a1d66266c35753f2d350a26f6c63b3 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 5 May 2016 17:33:18 +0100 Subject: [PATCH] Allow @ConditionalOnProperty to be used as a meta-annotation Closes gh-5819 --- .../condition/OnPropertyCondition.java | 70 ++++++++++++++++-- .../condition/ConditionalOnPropertyTests.java | 74 ++++++++++++++++++- 2 files changed, 138 insertions(+), 6 deletions(-) diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/OnPropertyCondition.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/OnPropertyCondition.java index 370eca2080d..9c8192e601e 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/OnPropertyCondition.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/OnPropertyCondition.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2014 the original author or authors. + * Copyright 2012-2016 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. @@ -17,8 +17,10 @@ package org.springframework.boot.autoconfigure.condition; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import org.springframework.boot.bind.RelaxedPropertyResolver; import org.springframework.context.annotation.Condition; @@ -27,6 +29,7 @@ import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.core.env.PropertyResolver; import org.springframework.core.type.AnnotatedTypeMetadata; import org.springframework.util.Assert; +import org.springframework.util.MultiValueMap; import org.springframework.util.StringUtils; /** @@ -35,6 +38,7 @@ import org.springframework.util.StringUtils; * @author Maciej Walkowiak * @author Phillip Webb * @author Stephane Nicoll + * @author Andy Wilkinson * @since 1.1.0 * @see ConditionalOnProperty */ @@ -43,10 +47,57 @@ class OnPropertyCondition extends SpringBootCondition { @Override public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) { + List allAnnotationAttributes = annotationAttributesFromMultiValueMap( + metadata.getAllAnnotationAttributes( + ConditionalOnProperty.class.getName())); + List noMatchOutcomes = findNoMatchOutcomes( + allAnnotationAttributes, context.getEnvironment()); + if (noMatchOutcomes.isEmpty()) { + return ConditionOutcome.match(); + } + return ConditionOutcome.noMatch(getCompositeMessage(noMatchOutcomes)); + } - AnnotationAttributes annotationAttributes = AnnotationAttributes.fromMap( - metadata.getAnnotationAttributes(ConditionalOnProperty.class.getName())); + private List annotationAttributesFromMultiValueMap( + MultiValueMap multiValueMap) { + List> maps = new ArrayList>(); + for (Entry> entry : multiValueMap.entrySet()) { + for (int i = 0; i < entry.getValue().size(); i++) { + Map map; + if (i < maps.size()) { + map = maps.get(i); + } + else { + map = new HashMap(); + maps.add(map); + } + map.put(entry.getKey(), entry.getValue().get(i)); + } + } + List annotationAttributes = new ArrayList( + maps.size()); + for (Map map : maps) { + annotationAttributes.add(AnnotationAttributes.fromMap(map)); + } + return annotationAttributes; + } + private List findNoMatchOutcomes( + List allAnnotationAttributes, + PropertyResolver resolver) { + List noMatchOutcomes = new ArrayList( + allAnnotationAttributes.size()); + for (AnnotationAttributes annotationAttributes : allAnnotationAttributes) { + ConditionOutcome outcome = determineOutcome(annotationAttributes, resolver); + if (!outcome.isMatch()) { + noMatchOutcomes.add(outcome); + } + } + return noMatchOutcomes; + } + + private ConditionOutcome determineOutcome(AnnotationAttributes annotationAttributes, + PropertyResolver resolver) { String prefix = annotationAttributes.getString("prefix").trim(); if (StringUtils.hasText(prefix) && !prefix.endsWith(".")) { prefix = prefix + "."; @@ -56,7 +107,6 @@ class OnPropertyCondition extends SpringBootCondition { boolean relaxedNames = annotationAttributes.getBoolean("relaxedNames"); boolean matchIfMissing = annotationAttributes.getBoolean("matchIfMissing"); - PropertyResolver resolver = context.getEnvironment(); if (relaxedNames) { resolver = new RelaxedPropertyResolver(resolver, prefix); } @@ -91,7 +141,6 @@ class OnPropertyCondition extends SpringBootCondition { message.append("expected '").append(expected).append("' for properties ") .append(expandNames(prefix, nonMatchingProperties)); } - return ConditionOutcome.noMatch(message.toString()); } @@ -122,4 +171,15 @@ class OnPropertyCondition extends SpringBootCondition { return expanded.toString(); } + private String getCompositeMessage(List noMatchOutcomes) { + StringBuilder message = new StringBuilder(); + for (ConditionOutcome noMatchOutcome : noMatchOutcomes) { + if (message.length() > 0) { + message.append(". "); + } + message.append(noMatchOutcome.getMessage().trim()); + } + return message.toString(); + } + } diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnPropertyTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnPropertyTests.java index df39c1576f0..38166cec8a8 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnPropertyTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnPropertyTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2014 the original author or authors. + * Copyright 2012-2016 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. @@ -16,6 +16,11 @@ package org.springframework.boot.autoconfigure.condition; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + import org.junit.After; import org.junit.Rule; import org.junit.Test; @@ -37,6 +42,7 @@ import static org.junit.internal.matchers.ThrowableMessageMatcher.hasMessage; * @author Maciej Walkowiak * @author Stephane Nicoll * @author Phillip Webb + * @author Andy Wilkinson */ public class ConditionalOnPropertyTests { @@ -227,6 +233,44 @@ public class ConditionalOnPropertyTests { load(NameAndValueAttribute.class, "some.property"); } + @Test + public void metaAnnotationConditionMatchesWhenPropertyIsSet() throws Exception { + load(MetaAnnotation.class, "my.feature.enabled=true"); + assertTrue(this.context.containsBean("foo")); + } + + @Test + public void metaAnnotationConditionDoesNotMatchWhenPropertyIsNotSet() + throws Exception { + load(MetaAnnotation.class); + assertFalse(this.context.containsBean("foo")); + } + + @Test + public void metaAndDirectAnnotationConditionDoesNotMatchWhenOnlyDirectPropertyIsSet() { + load(MetaAnnotationAndDirectAnnotation.class, "my.other.feature.enabled=true"); + assertFalse(this.context.containsBean("foo")); + } + + @Test + public void metaAndDirectAnnotationConditionDoesNotMatchWhenOnlyMetaPropertyIsSet() { + load(MetaAnnotationAndDirectAnnotation.class, "my.feature.enabled=true"); + assertFalse(this.context.containsBean("foo")); + } + + @Test + public void metaAndDirectAnnotationConditionDoesNotMatchWhenNeitherPropertyIsSet() { + load(MetaAnnotationAndDirectAnnotation.class); + assertFalse(this.context.containsBean("foo")); + } + + @Test + public void metaAndDirectAnnotationConditionMatchesWhenBothPropertiesAreSet() { + load(MetaAnnotationAndDirectAnnotation.class, "my.feature.enabled=true", + "my.other.feature.enabled=true"); + assertTrue(this.context.containsBean("foo")); + } + private void load(Class config, String... environment) { this.context = new AnnotationConfigApplicationContext(); EnvironmentTestUtils.addEnvironment(this.context, environment); @@ -390,4 +434,32 @@ public class ConditionalOnPropertyTests { } } + + @ConditionalOnMyFeature + protected static class MetaAnnotation { + + @Bean + public String foo() { + return "foo"; + } + + } + + @ConditionalOnMyFeature + @ConditionalOnProperty(prefix = "my.other.feature", name = "enabled", havingValue = "true", matchIfMissing = false) + protected static class MetaAnnotationAndDirectAnnotation { + + @Bean + public String foo() { + return "foo"; + } + + } + + @Retention(RetentionPolicy.RUNTIME) + @Target({ ElementType.TYPE, ElementType.METHOD }) + @ConditionalOnProperty(prefix = "my.feature", name = "enabled", havingValue = "true", matchIfMissing = false) + public @interface ConditionalOnMyFeature { + + } }