Add Spring @Conditional annotation (SpringBootCondition) to enable/disable Spring bean configuration based on missing properties.

This commit is contained in:
John Blum
2018-10-02 16:10:16 -07:00
parent 80995ef3fc
commit 7199f3c3a1
3 changed files with 355 additions and 0 deletions

View File

@@ -0,0 +1,80 @@
/*
* Copyright 2018 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.geode.boot.autoconfigure.condition;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.context.annotation.Conditional;
import org.springframework.core.annotation.AliasFor;
/**
* The {@link ConditionalOnMissingProperty} annotation is a Spring {@link Conditional} used to conditionally enable
* or disable functionality based on the absence of any declared properties.
*
* @author John Blum
* @see org.springframework.context.annotation.Conditional
* @since 1.0.0
*/
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(OnMissingPropertyCondition.class)
@SuppressWarnings("unused")
public @interface ConditionalOnMissingProperty {
/**
* The {@link String names} of the properties to test.
*
* If a {@link String prefix} has been defined, it is applied to compute the full key of each property.
*
* For instance, if the {@link String prefix} is {@code app.config} and one value is {@code my-value},
* the full key would be {@code app.config.my-value}.
*
* Use dashed notation to specify each property, that is all lower case with a "-" to separate words
* (e.g. {@code my-long-property}).
*
* @return the {@link String names} of the properties to test.
* @see #prefix()
*/
@AliasFor("value")
String[] name() default {};
/**
* A {@link String prefix} that should be applied to each property.
*
* The {@link String prefix} automatically ends with a dot if not specified. A valid {@link String prefix}
* is defined by one or more words separated with dots (e.g. {@code "acme.system.feature"}).
*
* @return the property {@link String prefix}.
* @see #name()
*/
String prefix() default "";
/**
* Alias for {@link #name()}.
*
* @return the {@link String names} of the properties to test.
* @see #name()
*/
@AliasFor("name")
String[] value() default {};
}

View File

@@ -0,0 +1,162 @@
/*
* Copyright 2018 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.geode.boot.autoconfigure.condition;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
import org.apache.shiro.util.Assert;
import org.apache.shiro.util.StringUtils;
import org.springframework.boot.autoconfigure.condition.ConditionMessage;
import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.env.PropertyResolver;
import org.springframework.core.type.AnnotatedTypeMetadata;
import org.springframework.util.MultiValueMap;
/**
* The {@link OnMissingPropertyCondition} class is a {@link SpringBootCondition}, Spring {@link Condition} type
* asserting whether the specified, declared properties are missing.
*
* @author John Blum
* @see org.springframework.boot.autoconfigure.condition.ConditionOutcome
* @see org.springframework.boot.autoconfigure.condition.SpringBootCondition
* @see org.springframework.context.annotation.Condition
* @see org.springframework.context.annotation.ConditionContext
* @see org.springframework.core.annotation.AnnotationAttributes
* @see org.springframework.core.env.PropertyResolver
* @see org.springframework.core.type.AnnotatedTypeMetadata
* @see org.springframework.geode.boot.autoconfigure.condition.ConditionalOnMissingProperty
* @since 1.0.0
*/
public class OnMissingPropertyCondition extends SpringBootCondition {
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
String annotationName = ConditionalOnMissingProperty.class.getName();
Collection<AnnotationAttributes> annotationAttributesCollection =
toAnnotationAttributesFromMultiValueMap(metadata.getAllAnnotationAttributes(annotationName));
PropertyResolver propertyResolver = getPropertyResolver(context);
Collection<String> allMatchingProperties = new ArrayList<>();
annotationAttributesCollection.forEach(annotationAttributes -> {
List<String> propertyNames = collectPropertyNames(annotationAttributes);
allMatchingProperties.addAll(findMatchingProperties(propertyResolver, propertyNames));
});
return determineConditionOutcome(allMatchingProperties);
}
@SuppressWarnings("unchecked")
private <T extends Collection<AnnotationAttributes>> T toAnnotationAttributesFromMultiValueMap(
MultiValueMap<String, Object> map) {
List<AnnotationAttributes> annotationAttributesList = new ArrayList<>();
map.forEach((key, value) -> {
for (int index = 0, size = value.size(); index < size; index++) {
AnnotationAttributes annotationAttributes =
resolveAnnotationAttributes(annotationAttributesList, index);
annotationAttributes.put(key, value.get(index));
}
});
return (T) annotationAttributesList;
}
private AnnotationAttributes resolveAnnotationAttributes(List<AnnotationAttributes> annotationAttributesList,
int index) {
if (index < annotationAttributesList.size()) {
return annotationAttributesList.get(index);
}
else {
AnnotationAttributes newAnnotationAttributes = new AnnotationAttributes();
annotationAttributesList.add(newAnnotationAttributes);
return newAnnotationAttributes;
}
}
private PropertyResolver getPropertyResolver(ConditionContext context) {
return context.getEnvironment();
}
private List<String> collectPropertyNames(AnnotationAttributes annotationAttributes) {
String prefix = getPrefix(annotationAttributes);
String[] names = getNames(annotationAttributes);
return Arrays.stream(names).map(name -> prefix + name).collect(Collectors.toList());
}
private String[] getNames(AnnotationAttributes annotationAttributes) {
String[] names = annotationAttributes.getStringArray("name");
String[] values = annotationAttributes.getStringArray("value");
Assert.isTrue(names.length > 0 || values.length > 0,
String.format("The name or value attribute of @%s is required",
ConditionalOnMissingProperty.class.getSimpleName()));
// TODO remove; not needed when using @AliasFor.
/*
Assert.isTrue(names.length * values.length == 0,
String.format("The name and value attributes of @%s are exclusive",
ConditionalOnMissingProperty.class.getSimpleName()));
*/
return names.length > 0 ? names : values;
}
private String getPrefix(AnnotationAttributes annotationAttributes) {
String prefix = annotationAttributes.getString("prefix");
return StringUtils.hasText(prefix) ? prefix.trim().endsWith(".") ? prefix.trim() : prefix.trim() + "." : "";
}
private Collection<String> findMatchingProperties(PropertyResolver propertyResolver, List<String> propertyNames) {
return propertyNames.stream().filter(propertyResolver::containsProperty).collect(Collectors.toSet());
}
private ConditionOutcome determineConditionOutcome(Collection<String> matchingProperties) {
if (!matchingProperties.isEmpty()) {
return ConditionOutcome.noMatch(ConditionMessage.forCondition(ConditionalOnMissingProperty.class)
.found("property already defined", "properties already defined")
.items(matchingProperties));
}
return ConditionOutcome.match();
}
}

View File

@@ -0,0 +1,113 @@
/*
* Copyright 2018 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.geode.boot.autoconfigure.condition;
import static org.assertj.core.api.Assertions.assertThat;
import java.util.Collections;
import java.util.function.Function;
import org.junit.Test;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.gemfire.tests.integration.SpringBootApplicationIntegrationTestsSupport;
/**
* The ConditionalOnMissingPropertyIntegrationTests class...
*
* @author John Blum
* @since 1.0.0
*/
@SuppressWarnings("unused")
public class ConditionalOnMissingPropertyIntegrationTests extends SpringBootApplicationIntegrationTestsSupport {
private Function<SpringApplicationBuilder, SpringApplicationBuilder> springApplicationBuilderFunction =
Function.identity();
private Function<SpringApplicationBuilder, SpringApplicationBuilder> allMatchingPropertiesFunction = builder -> {
builder.properties(Collections.singletonMap("example.app.config.propOne", ""));
builder.properties(Collections.singletonMap("example.app.config.propTwo", "test"));
return builder;
};
private Function<SpringApplicationBuilder, SpringApplicationBuilder> nonMatchingPropertyFunction = builder -> {
builder.properties(Collections.singletonMap("example.app.cfg.propertyOne", ""));
return builder;
};
private Function<SpringApplicationBuilder, SpringApplicationBuilder> singleMatchingPropertyFunction = builder -> {
builder.properties(Collections.singletonMap("example.app.config.propTwo", "test"));
return builder;
};
@Override
protected SpringApplicationBuilder processBeforeBuild(SpringApplicationBuilder springApplicationBuilder) {
return this.springApplicationBuilderFunction.apply(springApplicationBuilder);
}
private void assertBeanDefinitions(ApplicationContext applicationContext, boolean conditionalBeanExists) {
assertThat(applicationContext.containsBean("conditionalBean")).isEqualTo(conditionalBeanExists);
assertThat(applicationContext.containsBean("unconditionalBean")).isTrue();
}
@Test
public void allBeansExist() {
assertBeanDefinitions(newApplicationContext(TestConfiguration.class), true);
}
@Test
public void allBeansExistWithNonMatchingProperties() {
this.springApplicationBuilderFunction = this.nonMatchingPropertyFunction;
assertBeanDefinitions(newApplicationContext(TestConfiguration.class), true);
}
@Test
public void unconditionalBeanExistsWithAllMatchingProperties() {
this.springApplicationBuilderFunction = this.allMatchingPropertiesFunction;
assertBeanDefinitions(newApplicationContext(TestConfiguration.class), false);
}
@Test
public void unconditionalBeanExistsWithSingleMatchingProperty() {
this.springApplicationBuilderFunction = this.singleMatchingPropertyFunction;
assertBeanDefinitions(newApplicationContext(TestConfiguration.class), false);
}
@Configuration
static class TestConfiguration {
@Bean
@ConditionalOnMissingProperty(prefix = "example.app.config", name = { "propOne", "propTwo" })
Object conditionalBean() {
return "conditional";
}
@Bean
Object unconditionalBean() {
return "unconditional";
}
}
}