From 7613594b3a3fb0f7dd5f3f24b8cc1490482408f8 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Fri, 13 Jun 2025 11:54:05 -0700 Subject: [PATCH] Relax profile naming restrictions and allow opt-out Rework profile name validation logic so that `.` and `+` and `@` can be used in the names. Also provide an opt-out property that can be set to restore earlier Spring Boot behavior. The commit also include an update to the reference documentation. Fixes gh-45947 --- .../reference/pages/features/profiles.adoc | 16 ++ .../context/config/ConfigDataEnvironment.java | 18 ++- .../boot/context/config/Profiles.java | 28 ++-- .../context/config/ProfilesValidator.java | 94 ++++++++++++ .../StandardConfigDataLocationResolver.java | 22 --- ...itional-spring-configuration-metadata.json | 7 + ...ironmentPostProcessorIntegrationTests.java | 30 ++++ .../boot/context/config/ProfilesTests.java | 62 +++++++- .../config/ProfilesValidatorTests.java | 138 ++++++++++++++++++ ...andardConfigDataLocationResolverTests.java | 77 ---------- .../src/main/resources/application.properties | 4 + 11 files changed, 378 insertions(+), 118 deletions(-) create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ProfilesValidator.java create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ProfilesValidatorTests.java diff --git a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/features/profiles.adoc b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/features/profiles.adoc index 885502f3be9..79ecbffb0ec 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/features/profiles.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/features/profiles.adoc @@ -59,6 +59,22 @@ This means that you can specify active profiles in `application.properties` and TIP: See xref:features/external-config.adoc#features.external-config.order[the "`Externalized Configuration`"] for more details on the order in which property sources are considered. +[NOTE] +==== +By default, profile names in Spring Boot may contain letters, numbers, or permitted characters (`-`, `_`, `.`, `+`, `@`). +In addition, they can only start and end with a letter or number. + +This restriction helps to prevent common parsing issues. +if, however, you prefer more flexible profile names you can set configprop:spring.profiles.validate[] to `false` in your `application.properties` or `application.yaml` file: + +[configprops,yaml] +---- +spring: + profiles: + validate: false +---- +==== + [[features.profiles.adding-active-profiles]] diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataEnvironment.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataEnvironment.java index a86028a3f64..c68f967ae84 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataEnvironment.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataEnvironment.java @@ -297,14 +297,16 @@ class ConfigDataEnvironment { ConfigurationPropertySource source = contributor.getConfigurationPropertySource(); if (source != null && !contributor.hasConfigDataOption(ConfigData.Option.IGNORE_PROFILES)) { Binder binder = new Binder(Collections.singleton(source), placeholdersResolver); - binder.bind(Profiles.INCLUDE_PROFILES, STRING_LIST).ifBound((includes) -> { - if (!contributor.isActive(activationContext)) { - InactiveConfigDataAccessException.throwIfPropertyFound(contributor, Profiles.INCLUDE_PROFILES); - InactiveConfigDataAccessException.throwIfPropertyFound(contributor, - Profiles.INCLUDE_PROFILES.append("[0]")); - } - result.addAll(includes); - }); + binder.bind(Profiles.INCLUDE_PROFILES, STRING_LIST, ProfilesValidator.get(binder)) + .ifBound((includes) -> { + if (!contributor.isActive(activationContext)) { + InactiveConfigDataAccessException.throwIfPropertyFound(contributor, + Profiles.INCLUDE_PROFILES); + InactiveConfigDataAccessException.throwIfPropertyFound(contributor, + Profiles.INCLUDE_PROFILES.append("[0]")); + } + result.addAll(includes); + }); } } return result; diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/Profiles.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/Profiles.java index 27cfd9fc793..73b371a0304 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/Profiles.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/Profiles.java @@ -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. @@ -78,27 +78,35 @@ public class Profiles implements Iterable { * @param additionalProfiles any additional active profiles */ Profiles(Environment environment, Binder binder, Collection additionalProfiles) { - this.groups = binder.bind("spring.profiles.group", STRING_STRINGS_MAP).orElseGet(LinkedMultiValueMap::new); - this.activeProfiles = expandProfiles(getActivatedProfiles(environment, binder, additionalProfiles)); - this.defaultProfiles = expandProfiles(getDefaultProfiles(environment, binder)); + ProfilesValidator validator = ProfilesValidator.get(binder); + if (additionalProfiles != null) { + validator.validate(additionalProfiles, () -> "Invalid profile property value found in additional profiles"); + } + this.groups = binder.bind("spring.profiles.group", STRING_STRINGS_MAP, validator) + .orElseGet(LinkedMultiValueMap::new); + this.activeProfiles = expandProfiles(getActivatedProfiles(environment, binder, validator, additionalProfiles)); + this.defaultProfiles = expandProfiles(getDefaultProfiles(environment, binder, validator)); } - private List getActivatedProfiles(Environment environment, Binder binder, + private List getActivatedProfiles(Environment environment, Binder binder, ProfilesValidator validator, Collection additionalProfiles) { - return asUniqueItemList(getProfiles(environment, binder, Type.ACTIVE), additionalProfiles); + return asUniqueItemList(getProfiles(environment, binder, validator, Type.ACTIVE), additionalProfiles); } - private List getDefaultProfiles(Environment environment, Binder binder) { - return asUniqueItemList(getProfiles(environment, binder, Type.DEFAULT)); + private List getDefaultProfiles(Environment environment, Binder binder, ProfilesValidator validator) { + return asUniqueItemList(getProfiles(environment, binder, validator, Type.DEFAULT)); } - private Collection getProfiles(Environment environment, Binder binder, Type type) { + private Collection getProfiles(Environment environment, Binder binder, ProfilesValidator validator, + Type type) { String environmentPropertyValue = environment.getProperty(type.getName()); Set environmentPropertyProfiles = (!StringUtils.hasLength(environmentPropertyValue)) ? Collections.emptySet() : StringUtils.commaDelimitedListToSet(StringUtils.trimAllWhitespace(environmentPropertyValue)); + validator.validate(environmentPropertyProfiles, + () -> "Invalid profile property value found in Envronment under '%s'".formatted(type.getName())); Set environmentProfiles = new LinkedHashSet<>(Arrays.asList(type.get(environment))); - BindResult> boundProfiles = binder.bind(type.getName(), STRING_SET); + BindResult> boundProfiles = binder.bind(type.getName(), STRING_SET, validator); if (hasProgrammaticallySetProfiles(type, environmentPropertyValue, environmentPropertyProfiles, environmentProfiles)) { if (!type.isMergeWithEnvironmentProfiles() || !boundProfiles.isBound()) { diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ProfilesValidator.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ProfilesValidator.java new file mode 100644 index 00000000000..6375d4848f6 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ProfilesValidator.java @@ -0,0 +1,94 @@ +/* + * 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. + * 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.context.config; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Map; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import org.springframework.boot.context.properties.bind.BindContext; +import org.springframework.boot.context.properties.bind.BindHandler; +import org.springframework.boot.context.properties.bind.Bindable; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.boot.context.properties.source.ConfigurationPropertyName; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * {@link BindHandler} that validates profile names. + * + * @author Sijun Yang + * @author Phillip Webb + */ +final class ProfilesValidator implements BindHandler { + + private static final String ALLOWED_CHARS = "-_.+@"; + + private final boolean validate; + + private ProfilesValidator(boolean validate) { + this.validate = validate; + } + + @Override + public Object onSuccess(ConfigurationPropertyName name, Bindable target, BindContext context, Object result) { + validate(result); + return result; + } + + void validate(Object value, Supplier wrappedExceptionMessage) { + try { + validate(value); + } + catch (IllegalStateException ex) { + throw new IllegalStateException(wrappedExceptionMessage.get(), ex); + } + } + + private void validate(Object value) { + if (!this.validate) { + return; + } + if (value instanceof Collection list) { + list.forEach(this::validate); + return; + } + if (value instanceof Map map) { + map.forEach((k, v) -> validate(v)); + return; + } + String profile = (value != null) ? value.toString() : null; + Assert.state(StringUtils.hasText(profile), "Invalid empty profile"); + for (int i = 0; i < profile.length(); i++) { + int codePoint = profile.codePointAt(i); + boolean isAllowedChar = ALLOWED_CHARS.indexOf(codePoint) != -1; + Assert.state(isAllowedChar || Character.isLetterOrDigit(codePoint), + () -> "Profile '%s' must contain a letter, digit or allowed char (%s)".formatted(profile, + Arrays.stream(ALLOWED_CHARS.split("")).collect(Collectors.joining("', '", "'", "'")))); + Assert.state((i > 0 && i < profile.length() - 1) || Character.isLetterOrDigit(codePoint), + () -> "Profile '%s' must start and end with a letter or digit".formatted(profile)); + } + + } + + static ProfilesValidator get(Binder binder) { + return new ProfilesValidator(binder.bind("spring.profiles.validate", Boolean.class).orElse(true)); + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/StandardConfigDataLocationResolver.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/StandardConfigDataLocationResolver.java index ea3390c444f..905d8533c6c 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/StandardConfigDataLocationResolver.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/StandardConfigDataLocationResolver.java @@ -146,7 +146,6 @@ public class StandardConfigDataLocationResolver @Override public List resolveProfileSpecific(ConfigDataLocationResolverContext context, ConfigDataLocation location, Profiles profiles) { - validateProfiles(profiles); return resolve(getProfileSpecificReferences(context, location.split(), profiles)); } @@ -162,27 +161,6 @@ public class StandardConfigDataLocationResolver return references; } - private void validateProfiles(Profiles profiles) { - for (String profile : profiles) { - validateProfile(profile); - } - } - - private void validateProfile(String profile) { - Assert.hasText(profile, "'profile' must contain text"); - Assert.state(!profile.startsWith("-") && !profile.startsWith("_"), - () -> String.format("Invalid profile '%s': must not start with '-' or '_'", profile)); - Assert.state(!profile.endsWith("-") && !profile.endsWith("_"), - () -> String.format("Invalid profile '%s': must not end with '-' or '_'", profile)); - profile.codePoints().forEach((codePoint) -> { - if (codePoint == '-' || codePoint == '_' || Character.isLetterOrDigit(codePoint)) { - return; - } - throw new IllegalStateException( - String.format("Invalid profile '%s': must contain only letters, digits, '-', or '_'", profile)); - }); - } - private String getResourceLocation(ConfigDataLocationResolverContext context, ConfigDataLocation configDataLocation) { String resourceLocation = configDataLocation.getNonPrefixedValue(PREFIX); diff --git a/spring-boot-project/spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-boot-project/spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 4e16aff636a..f01600c74d4 100644 --- a/spring-boot-project/spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-boot-project/spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -614,6 +614,13 @@ "sourceType": "org.springframework.boot.context.config.Profiles", "description": "Unconditionally activate the specified comma-separated list of profiles (or list of profiles if using YAML)." }, + { + "name": "spring.profiles.validate", + "type": "java.lang.Boolean", + "sourceType": "org.springframework.boot.context.config.Profiles", + "description": "Whether profiles should be validated to ensure sensible names are used.", + "defaultValue": true + }, { "name": "spring.reactor.debug-agent.enabled", "type": "java.lang.Boolean", diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigDataEnvironmentPostProcessorIntegrationTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigDataEnvironmentPostProcessorIntegrationTests.java index 0b1c19ba415..43eee55b94b 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigDataEnvironmentPostProcessorIntegrationTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigDataEnvironmentPostProcessorIntegrationTests.java @@ -1242,6 +1242,36 @@ class ConfigDataEnvironmentPostProcessorIntegrationTests { assertThat(environment.getProperty("v2")).isEqualTo("root-p2"); } + @Test + @WithResource(name = "application.properties", content = """ + spring.profiles.active=fa!l + """) + void invalidProfileActivePropertyThrowsException() { + assertThatExceptionOfType(BindException.class).isThrownBy(() -> this.application.run()) + .havingCause() + .withMessageContaining("must contain a letter"); + } + + @Test + @WithResource(name = "application.properties", content = """ + spring.profiles.include=fa!l + """) + void invalidProfileIncludePropertyThrowsException() { + assertThatExceptionOfType(BindException.class).isThrownBy(() -> this.application.run()) + .havingCause() + .withMessageContaining("must contain a letter"); + } + + @Test + @WithResource(name = "application.properties", content = """ + spring.profiles.active=p!1 + spring.profiles.include=p!2 + spring.profiles.validate=false + """) + void unvalidatedProfileProperties() { + assertThatNoException().isThrownBy(() -> this.application.run()); + } + private Condition matchingPropertySource(final String sourceName) { return new Condition<>("environment containing property source " + sourceName) { diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ProfilesTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ProfilesTests.java index 05fc5a7a332..e75ddc4c29a 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ProfilesTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ProfilesTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 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. @@ -20,6 +20,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Set; import org.junit.jupiter.api.Test; @@ -31,12 +32,16 @@ import org.springframework.core.env.Environment; import org.springframework.mock.env.MockEnvironment; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.assertj.core.api.Assertions.assertThatNoException; /** * Tests for {@link Profiles}. * * @author Phillip Webb * @author Madhura Bhave + * @author Sijun Yang */ class ProfilesTests { @@ -418,4 +423,59 @@ class ProfilesTests { assertThat(profiles.getAccepted()).containsExactly("a", "e", "x", "y", "g", "f", "b", "c"); } + @Test + void validNamesArePermitted() { + assertValidName("spring.profiles.active", "ok"); + assertValidName("spring.profiles.default", "ok"); + assertValidName("spring.profiles.group.a", "ok"); + } + + @Test + void invalidNamesAreNotPermitted() { + assertInvalidName("spring.profiles.active", "fa!l"); + assertInvalidName("spring.profiles.default", "fa!l"); + assertInvalidName("spring.profiles.group.a", "fa!l"); + } + + @Test + void invalidNamesWhenValidationDisabledArePermitted() { + MockEnvironment environment = new MockEnvironment(); + environment.setProperty("spring.profiles.validate", "false"); + environment.setProperty("spring.profiles.active", "fa!l"); + Binder binder = Binder.get(environment); + Profiles profiles = new Profiles(environment, binder, null); + assertThat(profiles.getAccepted()).containsExactly("fa!l"); + } + + @Test + void invalidNameInEnvironment() { + MockEnvironment environment = new MockEnvironment(); + environment.setProperty("spring.profiles.active", "fa!l"); + Binder binder = new Binder(); + assertThatIllegalStateException().isThrownBy(() -> new Profiles(environment, binder, null)) + .withMessage("Invalid profile property value found in Envronment under 'spring.profiles.active'"); + } + + @Test + void invalidNameInActive() { + MockEnvironment environment = new MockEnvironment(); + Binder binder = new Binder(); + assertThatIllegalStateException().isThrownBy(() -> new Profiles(environment, binder, Set.of("fa!l"))) + .withMessage("Invalid profile property value found in additional profiles"); + } + + private void assertValidName(String name, String value) { + MockEnvironment environment = new MockEnvironment(); + environment.setProperty(name, value); + Binder binder = Binder.get(environment); + assertThatNoException().isThrownBy(() -> new Profiles(environment, binder, null)); + } + + private void assertInvalidName(String name, String value) { + MockEnvironment environment = new MockEnvironment(); + environment.setProperty(name, value); + Binder binder = Binder.get(environment); + assertThatExceptionOfType(RuntimeException.class).isThrownBy(() -> new Profiles(environment, binder, null)); + } + } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ProfilesValidatorTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ProfilesValidatorTests.java new file mode 100644 index 00000000000..35e3b267683 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ProfilesValidatorTests.java @@ -0,0 +1,138 @@ +/* + * 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. + * 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.context.config; + +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.context.properties.bind.BindException; +import org.springframework.boot.context.properties.bind.Bindable; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.boot.context.properties.source.MapConfigurationPropertySource; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.assertj.core.api.Assertions.assertThatNoException; + +/** + * Tests for {@link ProfilesValidator}. + * + * @author Phillip Webb + * @author Sijun Yang + */ +class ProfilesValidatorTests { + + private static final Bindable STRING = Bindable.of(String.class); + + private static final Bindable> STRING_LIST = Bindable.listOf(String.class); + + private static final Bindable> STRING_STRING_MAP = Bindable.mapOf(String.class, String.class); + + @Test + void validateWhenValid() { + assertValid("test"); + assertValid("dev-test"); + assertValid("dev-test_123"); + assertValid("dev-테스트_123"); + assertValid("d-e_v-t-.e_@@s+t"); + } + + @Test + void validateWhenInvalidThrowsException() { + assertInvalid("-dev"); + assertInvalid("_dev"); + assertInvalid("+dev"); + assertInvalid(".dev"); + assertInvalid("dev_"); + assertInvalid("dev*test"); + } + + @Test + void validateWhenInvalidBoundStringThrowsException() { + assertInvalid(Map.of("profile", "dev*test"), STRING); + } + + @Test + void validateWhenInvalidBoundCollectionThrowsException() { + assertInvalid(Map.of("profile", "dev*test"), STRING_LIST); + } + + @Test + void validateWhenInvalidBoundCollectionFromIndexedThrowsException() { + assertInvalid(Map.of("profile[0]", "ok,", "profile[1]", "dev*test"), STRING_LIST); + } + + @Test + void validateWhenInvalidBoundMapFromIndexedThrowsException() { + assertInvalid(Map.of("profile.foo", "dev*test"), STRING_STRING_MAP); + } + + @Test + void validateWhenInvalidThrowsUsefulExceptionMessage() { + assertThatExceptionOfType(BindException.class).isThrownBy(() -> bind(Map.of("profile", "b*d"))) + .havingCause() + .withMessageContaining( + "Profile 'b*d' must contain a letter, digit or allowed char ('-', '_', '.', '+', '@')"); + } + + @Test + void validateWhenInvalidStartCharacterThrowsUsefulExceptionMessage() { + assertThatExceptionOfType(BindException.class).isThrownBy(() -> bind(Map.of("profile", "_bad"))) + .havingCause() + .withMessageContaining("Profile '_bad' must start and end with a letter or digit"); + } + + @Test + void validateWithWrappedExceptionMessageWhenValid() { + assertThatNoException().isThrownBy(() -> ProfilesValidator.get(new Binder()).validate("ok", () -> "context")); + } + + @Test + void validateWithWrappedExceptionMessageWhenInvalidThrowsException() { + assertThatIllegalStateException() + .isThrownBy(() -> ProfilesValidator.get(new Binder()).validate("b*d", () -> "context")) + .withMessage("context") + .havingCause() + .withMessageContaining("must contain a letter"); + } + + private void assertValid(String value) { + assertThatNoException().isThrownBy(() -> bind(Map.of("profile", value))); + } + + private void assertInvalid(String value) { + assertThatExceptionOfType(BindException.class).isThrownBy(() -> bind(Map.of("profile", value))); + } + + private void assertInvalid(Map map, Bindable target) { + assertThatExceptionOfType(BindException.class).isThrownBy(() -> bind(map, target)); + } + + private void bind(Map map) { + bind(map, STRING); + } + + private void bind(Map map, Bindable target) { + MapConfigurationPropertySource source = new MapConfigurationPropertySource(map); + Binder binder = new Binder(source); + ProfilesValidator validator = ProfilesValidator.get(binder); + binder.bind("profile", target, validator); + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/StandardConfigDataLocationResolverTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/StandardConfigDataLocationResolverTests.java index aaae8c73bee..02538f200d7 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/StandardConfigDataLocationResolverTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/StandardConfigDataLocationResolverTests.java @@ -306,83 +306,6 @@ class StandardConfigDataLocationResolverTests { assertThatNoException().isThrownBy(() -> this.resolver.resolve(this.context, location)); } - @Test - void resolveProfileSpecificWhenProfileIsValidShouldNotThrowException() { - ConfigDataLocation location = ConfigDataLocation.of("classpath:/configdata/properties/"); - this.environment.setActiveProfiles("dev-test_123"); - Profiles profiles = new Profiles(this.environment, this.environmentBinder, Collections.emptyList()); - assertThatNoException() - .isThrownBy(() -> this.resolver.resolveProfileSpecific(this.context, location, profiles)); - } - - @Test - void resolveProfileSpecificWithNonAsciiCharactersShouldNotThrowException() { - ConfigDataLocation location = ConfigDataLocation.of("classpath:/configdata/properties/"); - this.environment.setActiveProfiles("dev-테스트_123"); - Profiles profiles = new Profiles(this.environment, this.environmentBinder, Collections.emptyList()); - assertThatNoException() - .isThrownBy(() -> this.resolver.resolveProfileSpecific(this.context, location, profiles)); - } - - @Test - void resolveProfileSpecificWithAdditionalValidProfilesShouldNotThrowException() { - ConfigDataLocation location = ConfigDataLocation.of("classpath:/configdata/properties/"); - this.environment.setActiveProfiles("dev-test"); - Profiles profiles = new Profiles(this.environment, this.environmentBinder, List.of("prod-test", "stage-test")); - assertThatNoException() - .isThrownBy(() -> this.resolver.resolveProfileSpecific(this.context, location, profiles)); - } - - @Test - void resolveProfileSpecificWhenProfileStartsWithDashThrowsException() { - ConfigDataLocation location = ConfigDataLocation.of("classpath:/configdata/properties/"); - this.environment.setActiveProfiles("-dev"); - Profiles profiles = new Profiles(this.environment, this.environmentBinder, Collections.emptyList()); - assertThatIllegalStateException() - .isThrownBy(() -> this.resolver.resolveProfileSpecific(this.context, location, profiles)) - .withMessageStartingWith("Invalid profile '-dev': must not start with '-' or '_'"); - } - - @Test - void resolveProfileSpecificWhenProfileStartsWithUnderscoreThrowsException() { - ConfigDataLocation location = ConfigDataLocation.of("classpath:/configdata/properties/"); - this.environment.setActiveProfiles("_dev"); - Profiles profiles = new Profiles(this.environment, this.environmentBinder, Collections.emptyList()); - assertThatIllegalStateException() - .isThrownBy(() -> this.resolver.resolveProfileSpecific(this.context, location, profiles)) - .withMessageStartingWith("Invalid profile '_dev': must not start with '-' or '_'"); - } - - @Test - void resolveProfileSpecificWhenProfileEndsWithDashThrowsException() { - ConfigDataLocation location = ConfigDataLocation.of("classpath:/configdata/properties/"); - this.environment.setActiveProfiles("dev-"); - Profiles profiles = new Profiles(this.environment, this.environmentBinder, Collections.emptyList()); - assertThatIllegalStateException() - .isThrownBy(() -> this.resolver.resolveProfileSpecific(this.context, location, profiles)) - .withMessageStartingWith("Invalid profile 'dev-': must not end with '-' or '_'"); - } - - @Test - void resolveProfileSpecificWhenProfileEndsWithUnderscoreThrowsException() { - ConfigDataLocation location = ConfigDataLocation.of("classpath:/configdata/properties/"); - this.environment.setActiveProfiles("dev_"); - Profiles profiles = new Profiles(this.environment, this.environmentBinder, Collections.emptyList()); - assertThatIllegalStateException() - .isThrownBy(() -> this.resolver.resolveProfileSpecific(this.context, location, profiles)) - .withMessageStartingWith("Invalid profile 'dev_': must not end with '-' or '_'"); - } - - @Test - void resolveProfileSpecificWhenProfileContainsInvalidCharactersThrowsException() { - ConfigDataLocation location = ConfigDataLocation.of("classpath:/configdata/properties/"); - this.environment.setActiveProfiles("dev*test"); - Profiles profiles = new Profiles(this.environment, this.environmentBinder, Collections.emptyList()); - assertThatIllegalStateException() - .isThrownBy(() -> this.resolver.resolveProfileSpecific(this.context, location, profiles)) - .withMessageStartingWith("Invalid profile 'dev*test': must contain only letters, digits, '-', or '_'"); - } - private String filePath(String... components) { return "file [" + String.join(File.separator, components) + "]"; } diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-simple/src/main/resources/application.properties b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-simple/src/main/resources/application.properties index 75d136475d8..bfd3a3a2ac8 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-simple/src/main/resources/application.properties +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-simple/src/main/resources/application.properties @@ -1,2 +1,6 @@ test.name=Phil sample.name=Andy + +spring.profiles.validate=false +spring.profiles.active=a+very(silly)!name +