We allow case insensitive enum values in the Environment
but @ConditionalOnProperty
does not really support that.
There is already one usage in the code base, see TestDatabaseAutoConfiguration
.
See also #6083
Comment From: snicoll
Turns out we can implement #6083 in a different way. TestDatabaseAutoConfiguration
uses properties mapping so the case will be right.
Comment From: quaff
Please reconsider, I think it's very common, take Spring AI's ServerProtocol
for example, spring.ai.mcp.server.protocol=STATELESS
works but spring.ai.mcp.server.protocol=stateless
will not works, see:
https://github.com/spring-projects/spring-ai/blob/6880753dd607532c9f542d5ce6fa0c1dbd0ff68e/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-common/src/main/java/org/springframework/ai/mcp/server/common/autoconfigure/McpServerStatelessAutoConfiguration.java#L237
https://github.com/spring-projects/spring-ai/blob/6880753dd607532c9f542d5ce6fa0c1dbd0ff68e/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-common/src/main/java/org/springframework/ai/mcp/server/common/autoconfigure/properties/McpServerProperties.java#L120
Comment From: wilkinsona
That looks like a ~bug in Spring AI to me. I'd recommend that the condition uses stateless
rather than STATELESS
.
We can't easily use relaxed binding with @ConditionalOnProperty
as it knows nothing about the type that the property is bound to. It therefore cannot detect that it's an enum. Applying case-insensitive checks in all cases has the potential to be a breaking change.
An alternative would be to introduce ConditionalOnEnumProperty
that could then perform case-insensitive matching. Given that we have ConditionalOnBooleanProperty
now, that's perhaps not such a wild idea. Something like this:
/*
* Copyright 2012-present 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
*
* https://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.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.env.Environment;
/**
* {@link Conditional @Conditional} that checks if the specified properties have a
* specific enum value. By default the properties must be present in the
* {@link Environment}. They much be equal, using relaxed matching, to the configured
* value. The {@link #havingValue()} and {@link #matchIfMissing()} attributes allow
* further customizations.
* <p>
* If the property is not contained in the {@link Environment} at all, the
* {@link #matchIfMissing()} attribute is consulted. By default missing attributes do not
* match.
*
* @author Andy Wilkinson
* @since TBD
* @see ConditionalOnProperty
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.METHOD })
@Documented
@Conditional(OnPropertyCondition.class)
public @interface ConditionalOnEnumProperty {
/**
* Alias for {@link #name()}.
* @return the names
*/
String[] value() default {};
/**
* A prefix that should be applied to each property. The prefix automatically ends
* with a dot if not specified. A valid prefix is defined by one or more words
* separated with dots (e.g. {@code "acme.system.feature"}).
* @return the prefix
*/
String prefix() default "";
/**
* The name of the properties to test. If a prefix has been defined, it is applied to
* compute the full key of each property. For instance if the prefix is
* {@code app.config} and one value is {@code my-value}, the full key would be
* {@code app.config.my-value}
* <p>
* Use the dashed notation to specify each property, that is all lower case with a "-"
* to separate words (e.g. {@code my-long-property}).
* <p>
* If multiple names are specified, all of the properties have to pass the test for
* the condition to match.
* @return the names
*/
String[] name() default {};
/**
* The type of the enum.
* @return the enum type
*/
Class<? extends Enum<?>> enumType();
/**
* The expected value for the properties.
* @return the expected value
*/
String havingValue();
/**
* Specify if the condition should match if the property is not set. Defaults to
* {@code false}.
* @return if the condition should match if the property is missing
*/
boolean matchIfMissing() default false;
}
Matching would be the same as LenientStringToEnumConverterFactory
. It would also be technically possible to fail at compile time if havingValue
didn't match one of the enum's values.
I'm not sure this is a good idea for a problem that doesn't appear to affect many people. We'll discuss it.
Comment From: quaff
Or introduce boolean ignoreCase() default false;
for @ConditionalOnProperty
, it's not specific to enum.
Comment From: philwebb
There's more to enum matching that just ignoring case. For example, MyEnum.FOO_BAR
will work with a property value of foobar
.
Comment From: philwebb
One other option to consider is adding a type
attribute to @ConditionalProperty
that we could use to convert the string values.
e.g.:
@ConditionalOnProperty(prefix = McpServerProperties.CONFIG_PREFIX, name = "protocol", havingValue = "stateless",
matchIfMissing = false, type=ServerProtocol.class)
We might even be able to get the type from the config metadata (although that feels a bit brittle).
I think I lean towards @ConditionalOnEnumProperty
, but we probably need some more discussion on this before we do anything.