Skip to content

Polish OnPropertyCondition #43754

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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<? extends Annotation> 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 conditionReportWhenMatched() {
load(Defaults.class, "test=true");
assertThat(this.context.containsBean("foo")).isTrue();
assertThat(getConditionEvaluationReport()).contains("@ConditionalOnBooleanProperty (test=true) matched");
}

@Test
void conditionReportWhenDoesNotMatch() {
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 conditionReportWhenMatched() {
load(MultiplePropertiesRequiredConfiguration.class, "property1=value1", "property2=value2");
assertThat(this.context.containsBean("foo")).isTrue();
assertThat(getConditionEvaluationReport()).contains("@ConditionalOnProperty ([property1,property2]) matched");
}

@Test
void conditionReportWhenDoesNotMatch() {
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
Loading