Skip to content

Commit

Permalink
Polish OnPropertyCondition
Browse files Browse the repository at this point in the history
Signed-off-by: Dmytro Nosan <[email protected]>
  • Loading branch information
nosan committed Jan 9, 2025
1 parent b48ce96 commit bda2c93
Show file tree
Hide file tree
Showing 3 changed files with 112 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@

package org.springframework.boot.autoconfigure.condition;

import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;

import org.springframework.boot.autoconfigure.condition.ConditionMessage.Style;
Expand All @@ -33,6 +33,7 @@
import org.springframework.core.env.PropertyResolver;
import org.springframework.core.type.AnnotatedTypeMetadata;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;

/**
Expand All @@ -43,23 +44,23 @@
* @author Stephane Nicoll
* @author Andy Wilkinson
* @see ConditionalOnProperty
* @see ConditionalOnBooleanProperty
*/
@Order(Ordered.HIGHEST_PRECEDENCE + 40)
class OnPropertyCondition extends SpringBootCondition {

@Override
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
MergedAnnotations annotations = metadata.getAnnotations();
List<AnnotationAttributes> allAnnotationAttributes = Stream
List<MergedAnnotation<Annotation>> allAnnotations = Stream
.concat(annotations.stream(ConditionalOnProperty.class.getName()),
annotations.stream(ConditionalOnBooleanProperty.class.getName()))
.filter(MergedAnnotationPredicates.unique(MergedAnnotation::getMetaTypes))
.map(MergedAnnotation::asAnnotationAttributes)
.toList();
List<ConditionMessage> noMatch = new ArrayList<>();
List<ConditionMessage> match = new ArrayList<>();
for (AnnotationAttributes annotationAttributes : allAnnotationAttributes) {
ConditionOutcome outcome = determineOutcome(annotationAttributes, context.getEnvironment());
for (MergedAnnotation<Annotation> annotation : allAnnotations) {
ConditionOutcome outcome = determineOutcome(annotation, context.getEnvironment());
(outcome.isMatch() ? match : noMatch).add(outcome.getConditionMessage());
}
if (!noMatch.isEmpty()) {
Expand All @@ -68,27 +69,29 @@ public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeM
return ConditionOutcome.match(ConditionMessage.of(match));
}

private ConditionOutcome determineOutcome(AnnotationAttributes annotationAttributes, PropertyResolver resolver) {
Spec spec = new Spec(annotationAttributes);
private ConditionOutcome determineOutcome(MergedAnnotation<Annotation> annotation, PropertyResolver resolver) {
Class<Annotation> annotationType = annotation.getType();
Spec spec = new Spec(annotationType, annotation.asAnnotationAttributes());
List<String> missingProperties = new ArrayList<>();
List<String> nonMatchingProperties = new ArrayList<>();
spec.collectProperties(resolver, missingProperties, nonMatchingProperties);
if (!missingProperties.isEmpty()) {
return ConditionOutcome.noMatch(ConditionMessage.forCondition(ConditionalOnProperty.class, spec)
return ConditionOutcome.noMatch(ConditionMessage.forCondition(annotationType, spec)
.didNotFind("property", "properties")
.items(Style.QUOTE, missingProperties));
}
if (!nonMatchingProperties.isEmpty()) {
return ConditionOutcome.noMatch(ConditionMessage.forCondition(ConditionalOnProperty.class, spec)
return ConditionOutcome.noMatch(ConditionMessage.forCondition(annotationType, spec)
.found("different value in property", "different value in properties")
.items(Style.QUOTE, nonMatchingProperties));
}
return ConditionOutcome
.match(ConditionMessage.forCondition(ConditionalOnProperty.class, spec).because("matched"));
return ConditionOutcome.match(ConditionMessage.forCondition(annotationType, spec).because("matched"));
}

private static class Spec {

private final Class<?> annotationType;

private final String prefix;

private final String[] names;
Expand All @@ -97,7 +100,8 @@ private static class Spec {

private final boolean matchIfMissing;

Spec(AnnotationAttributes annotationAttributes) {
Spec(Class<? extends Annotation> annotationType, AnnotationAttributes annotationAttributes) {
this.annotationType = annotationType;
this.prefix = (!annotationAttributes.containsKey("prefix")) ? "" : getPrefix(annotationAttributes);
this.names = getNames(annotationAttributes);
this.havingValue = annotationAttributes.get("havingValue").toString();
Expand All @@ -112,13 +116,13 @@ private String getPrefix(AnnotationAttributes annotationAttributes) {
return prefix;
}

private String[] getNames(Map<String, Object> annotationAttributes) {
private String[] getNames(AnnotationAttributes annotationAttributes) {
String[] value = (String[]) annotationAttributes.get("value");
String[] name = (String[]) annotationAttributes.get("name");
Assert.state(value.length > 0 || name.length > 0,
"The name or value attribute of @ConditionalOnProperty must be specified");
Assert.state(value.length == 0 || name.length == 0,
"The name and value attributes of @ConditionalOnProperty are exclusive");
Assert.state(value.length > 0 || name.length > 0, "The name or value attribute of @%s must be specified"
.formatted(ClassUtils.getShortName(this.annotationType)));
Assert.state(value.length == 0 || name.length == 0, "The name and value attributes of @%s are exclusive"
.formatted(ClassUtils.getShortName(this.annotationType)));
return (value.length > 0) ? value : name;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

package org.springframework.boot.autoconfigure.condition;

import java.util.function.Consumer;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;

Expand All @@ -29,6 +31,7 @@
import org.springframework.core.env.StandardEnvironment;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;

/**
* Tests for {@link ConditionalOnBooleanProperty @ConditionalOnBooleanProperty}.
Expand Down Expand Up @@ -144,6 +147,48 @@ void withPrefix() {
assertThat(this.context.containsBean("foo")).isTrue();
}

@Test
void nameOrValueMustBeSpecified() {
assertThatIllegalStateException().isThrownBy(() -> load(NoNameOrValueAttribute.class, "some.property"))
.satisfies(causeMessageContaining(
"The name or value attribute of @ConditionalOnBooleanProperty must be specified"));
}

@Test
void nameAndValueMustNotBeSpecified() {
assertThatIllegalStateException().isThrownBy(() -> load(NameAndValueAttribute.class, "some.property"))
.satisfies(causeMessageContaining(
"The name and value attributes of @ConditionalOnBooleanProperty are exclusive"));
}

@Test
void conditionalReportWhenMatched() {
load(Defaults.class, "test=true");
assertThat(this.context.containsBean("foo")).isTrue();
assertThat(getConditionEvaluationReport()).contains("@ConditionalOnBooleanProperty (test=true) matched");
}

@Test
void conditionalReportWhenDoesNotMatch() {
load(Defaults.class, "test=false");
assertThat(this.context.containsBean("foo")).isFalse();
assertThat(getConditionEvaluationReport())
.contains("@ConditionalOnBooleanProperty (test=true) found different value in property 'test'");
}

private <T extends Exception> Consumer<T> causeMessageContaining(String message) {
return (ex) -> assertThat(ex.getCause()).hasMessageContaining(message);
}

private String getConditionEvaluationReport() {
ConditionEvaluationReport report = ConditionEvaluationReport.get(this.context.getBeanFactory());
StringBuilder builder = new StringBuilder();
report.getConditionAndOutcomesBySource()
.values()
.forEach((outcomes) -> outcomes.forEach((outcome) -> builder.append(outcome.toString()).append('\n')));
return builder.toString();
}

private void load(Class<?> config, String... environment) {
TestPropertyValues.of(environment).applyTo(this.environment);
this.context = new SpringApplicationBuilder(config).environment(this.environment)
Expand Down Expand Up @@ -196,4 +241,26 @@ static class WithPrefix extends BeanConfiguration {

}

@Configuration(proxyBeanMethods = false)
@ConditionalOnBooleanProperty
static class NoNameOrValueAttribute {

@Bean
String foo() {
return "foo";
}

}

@Configuration(proxyBeanMethods = false)
@ConditionalOnBooleanProperty(value = "x", name = "y")
static class NameAndValueAttribute {

@Bean
String foo() {
return "foo";
}

}

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2012-2023 the original author or authors.
* Copyright 2012-2025 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.
Expand Down Expand Up @@ -271,13 +271,36 @@ void metaAndDirectAnnotationWithAliasConditionMatchesWhenBothPropertiesAreSet()
assertThat(this.context.containsBean("foo")).isTrue();
}

@Test
void conditionalReportWhenMatched() {
load(MultiplePropertiesRequiredConfiguration.class, "property1=value1", "property2=value2");
assertThat(this.context.containsBean("foo")).isTrue();
assertThat(getConditionEvaluationReport()).contains("@ConditionalOnProperty ([property1,property2]) matched");
}

@Test
void conditionalReportWhenDoesNotMatch() {
load(MultiplePropertiesRequiredConfiguration.class, "property1=value1");
assertThat(getConditionEvaluationReport())
.contains("@ConditionalOnProperty ([property1,property2]) did not find property 'property2'");
}

private void load(Class<?> config, String... environment) {
TestPropertyValues.of(environment).applyTo(this.environment);
this.context = new SpringApplicationBuilder(config).environment(this.environment)
.web(WebApplicationType.NONE)
.run();
}

private String getConditionEvaluationReport() {
ConditionEvaluationReport report = ConditionEvaluationReport.get(this.context.getBeanFactory());
StringBuilder builder = new StringBuilder();
report.getConditionAndOutcomesBySource()
.values()
.forEach((outcomes) -> outcomes.forEach((outcome) -> builder.append(outcome.toString()).append('\n')));
return builder.toString();
}

@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(name = { "property1", "property2" })
static class MultiplePropertiesRequiredConfiguration {
Expand Down

0 comments on commit bda2c93

Please sign in to comment.