Skip to content

Commit b444b8d

Browse files
committed
Merge pull request #43176 from YangSiJun528
* pr/43176: Polish "Tighten rules around profile naming" Tighten rules around profile naming Closes gh-43176
2 parents 579be1c + 5322352 commit b444b8d

File tree

4 files changed

+111
-20
lines changed

4 files changed

+111
-20
lines changed

spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/SpringBootContextLoaderTests.java

+2-12
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
* @author Stephane Nicoll
6161
* @author Scott Frederick
6262
* @author Madhura Bhave
63+
* @author Sijun Yang
6364
*/
6465
class SpringBootContextLoaderTests {
6566

@@ -127,11 +128,6 @@ void multipleActiveProfiles() {
127128
assertThat(getActiveProfiles(MultipleActiveProfiles.class)).containsExactly("profile1", "profile2");
128129
}
129130

130-
@Test
131-
void activeProfileWithComma() {
132-
assertThat(getActiveProfiles(ActiveProfileWithComma.class)).containsExactly("profile1,2");
133-
}
134-
135131
@Test // gh-28776
136132
void testPropertyValuesShouldTakePrecedenceWhenInlinedPropertiesPresent() {
137133
TestContext context = new ExposedTestContextManager(SimpleConfig.class).getExposedTestContext();
@@ -314,14 +310,8 @@ static class MultipleActiveProfiles {
314310

315311
}
316312

317-
@SpringBootTest(classes = Config.class)
318-
@ActiveProfiles({ "profile1,2" })
319-
static class ActiveProfileWithComma {
320-
321-
}
322-
323313
@SpringBootTest(properties = { "key=myValue" }, classes = Config.class)
324-
@ActiveProfiles({ "profile1,2" })
314+
@ActiveProfiles({ "profile1" })
325315
static class ActiveProfileWithInlinedProperties {
326316

327317
}

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/StandardConfigDataLocationResolver.java

+24-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2024 the original author or authors.
2+
* Copyright 2012-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -54,6 +54,7 @@
5454
* @author Madhura Bhave
5555
* @author Phillip Webb
5656
* @author Scott Frederick
57+
* @author Sijun Yang
5758
* @since 2.4.0
5859
*/
5960
public class StandardConfigDataLocationResolver
@@ -148,6 +149,7 @@ private Set<StandardConfigDataReference> getReferences(ConfigDataLocationResolve
148149
@Override
149150
public List<StandardConfigDataResource> resolveProfileSpecific(ConfigDataLocationResolverContext context,
150151
ConfigDataLocation location, Profiles profiles) {
152+
validateProfiles(profiles);
151153
return resolve(getProfileSpecificReferences(context, location.split(), profiles));
152154
}
153155

@@ -163,6 +165,27 @@ private Set<StandardConfigDataReference> getProfileSpecificReferences(ConfigData
163165
return references;
164166
}
165167

168+
private void validateProfiles(Profiles profiles) {
169+
for (String profile : profiles) {
170+
validateProfile(profile);
171+
}
172+
}
173+
174+
private void validateProfile(String profile) {
175+
Assert.hasText(profile, "'profile' must contain text");
176+
Assert.state(!profile.startsWith("-") && !profile.startsWith("_"),
177+
() -> String.format("Invalid profile '%s': must not start with '-' or '_'", profile));
178+
Assert.state(!profile.endsWith("-") && !profile.endsWith("_"),
179+
() -> String.format("Invalid profile '%s': must not end with '-' or '_'", profile));
180+
profile.codePoints().forEach((codePoint) -> {
181+
if (codePoint == '-' || codePoint == '_' || Character.isLetterOrDigit(codePoint)) {
182+
return;
183+
}
184+
throw new IllegalStateException(
185+
String.format("Invalid profile '%s': must contain only letters or digits or '-' or '_'", profile));
186+
});
187+
}
188+
166189
private String getResourceLocation(ConfigDataLocationResolverContext context,
167190
ConfigDataLocation configDataLocation) {
168191
String resourceLocation = configDataLocation.getNonPrefixedValue(PREFIX);

spring-boot-project/spring-boot/src/test/java/org/springframework/boot/SpringApplicationTests.java

+5-5
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@
165165
* @author Moritz Halbritter
166166
* @author Tadaya Tsuyukubo
167167
* @author Yanming Zhou
168+
* @author Sijun Yang
168169
*/
169170
@ExtendWith(OutputCaptureExtension.class)
170171
class SpringApplicationTests {
@@ -252,13 +253,12 @@ void logsActiveProfilesWithoutProfileAndSingleDefault(CapturedOutput output) {
252253
@Test
253254
void logsActiveProfilesWithoutProfileAndMultipleDefaults(CapturedOutput output) {
254255
MockEnvironment environment = new MockEnvironment();
255-
environment.setDefaultProfiles("p0,p1", "default");
256+
environment.setDefaultProfiles("p0", "default");
256257
SpringApplication application = new SpringApplication(ExampleConfig.class);
257258
application.setWebApplicationType(WebApplicationType.NONE);
258259
application.setEnvironment(environment);
259260
this.context = application.run();
260-
assertThat(output)
261-
.contains("No active profile set, falling back to 2 default profiles: \"p0,p1\", \"default\"");
261+
assertThat(output).contains("No active profile set, falling back to 2 default profiles: \"p0\", \"default\"");
262262
}
263263

264264
@Test
@@ -273,9 +273,9 @@ void logsActiveProfilesWithSingleProfile(CapturedOutput output) {
273273
void logsActiveProfilesWithMultipleProfiles(CapturedOutput output) {
274274
SpringApplication application = new SpringApplication(ExampleConfig.class);
275275
application.setWebApplicationType(WebApplicationType.NONE);
276-
application.setAdditionalProfiles("p1,p2", "p3");
276+
application.setAdditionalProfiles("p1", "p2");
277277
application.run();
278-
assertThat(output).contains("The following 2 profiles are active: \"p1,p2\", \"p3\"");
278+
assertThat(output).contains("The following 2 profiles are active: \"p1\", \"p2\"");
279279
}
280280

281281
@Test

spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/StandardConfigDataLocationResolverTests.java

+80-2
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
* @author Madhura Bhave
4545
* @author Phillip Webb
4646
* @author Moritz Halbritter
47+
* @author Sijun Yang
4748
*/
4849
class StandardConfigDataLocationResolverTests {
4950

@@ -254,8 +255,8 @@ void resolveWhenLocationUsesOptionalExtensionSyntaxResolves() throws Exception {
254255
@Test
255256
void resolveProfileSpecificReturnsProfileSpecificFiles() {
256257
ConfigDataLocation location = ConfigDataLocation.of("classpath:/configdata/properties/");
257-
Profiles profiles = mock(Profiles.class);
258-
given(profiles.iterator()).willReturn(Collections.singletonList("dev").iterator());
258+
this.environment.setActiveProfiles("dev");
259+
Profiles profiles = new Profiles(this.environment, this.environmentBinder, Collections.emptyList());
259260
List<StandardConfigDataResource> locations = this.resolver.resolveProfileSpecific(this.context, location,
260261
profiles);
261262
assertThat(locations).hasSize(1);
@@ -293,6 +294,83 @@ void resolveWhenOptionalAndExtensionIsUnknownShouldNotFail() {
293294
assertThatNoException().isThrownBy(() -> this.resolver.resolve(this.context, location));
294295
}
295296

297+
@Test
298+
void resolveProfileSpecificWhenProfileIsValidShouldNotThrowException() {
299+
ConfigDataLocation location = ConfigDataLocation.of("classpath:/configdata/properties/");
300+
this.environment.setActiveProfiles("dev-test_123");
301+
Profiles profiles = new Profiles(this.environment, this.environmentBinder, Collections.emptyList());
302+
assertThatNoException()
303+
.isThrownBy(() -> this.resolver.resolveProfileSpecific(this.context, location, profiles));
304+
}
305+
306+
@Test
307+
void resolveProfileSpecificWithNonAsciiCharactersShouldNotThrowException() {
308+
ConfigDataLocation location = ConfigDataLocation.of("classpath:/configdata/properties/");
309+
this.environment.setActiveProfiles("dev-테스트_123");
310+
Profiles profiles = new Profiles(this.environment, this.environmentBinder, Collections.emptyList());
311+
assertThatNoException()
312+
.isThrownBy(() -> this.resolver.resolveProfileSpecific(this.context, location, profiles));
313+
}
314+
315+
@Test
316+
void resolveProfileSpecificWithAdditionalValidProfilesShouldNotThrowException() {
317+
ConfigDataLocation location = ConfigDataLocation.of("classpath:/configdata/properties/");
318+
this.environment.setActiveProfiles("dev-test");
319+
Profiles profiles = new Profiles(this.environment, this.environmentBinder, List.of("prod-test", "stage-test"));
320+
assertThatNoException()
321+
.isThrownBy(() -> this.resolver.resolveProfileSpecific(this.context, location, profiles));
322+
}
323+
324+
@Test
325+
void resolveProfileSpecificWhenProfileStartsWithSymbolThrowsException() {
326+
ConfigDataLocation location = ConfigDataLocation.of("classpath:/configdata/properties/");
327+
this.environment.setActiveProfiles("-dev");
328+
Profiles profiles = new Profiles(this.environment, this.environmentBinder, Collections.emptyList());
329+
assertThatIllegalStateException()
330+
.isThrownBy(() -> this.resolver.resolveProfileSpecific(this.context, location, profiles))
331+
.withMessageStartingWith("Invalid profile '-dev': must not start with '-' or '_'");
332+
}
333+
334+
@Test
335+
void resolveProfileSpecificWhenProfileStartsWithUnderscoreThrowsException() {
336+
ConfigDataLocation location = ConfigDataLocation.of("classpath:/configdata/properties/");
337+
this.environment.setActiveProfiles("_dev");
338+
Profiles profiles = new Profiles(this.environment, this.environmentBinder, Collections.emptyList());
339+
assertThatIllegalStateException()
340+
.isThrownBy(() -> this.resolver.resolveProfileSpecific(this.context, location, profiles))
341+
.withMessageStartingWith("Invalid profile '_dev': must not start with '-' or '_'");
342+
}
343+
344+
@Test
345+
void resolveProfileSpecificWhenProfileEndsWithSymbolThrowsException() {
346+
ConfigDataLocation location = ConfigDataLocation.of("classpath:/configdata/properties/");
347+
this.environment.setActiveProfiles("dev-");
348+
Profiles profiles = new Profiles(this.environment, this.environmentBinder, Collections.emptyList());
349+
assertThatIllegalStateException()
350+
.isThrownBy(() -> this.resolver.resolveProfileSpecific(this.context, location, profiles))
351+
.withMessageStartingWith("Invalid profile 'dev-': must not end with '-' or '_'");
352+
}
353+
354+
@Test
355+
void resolveProfileSpecificWhenProfileEndsWithUnderscoreThrowsException() {
356+
ConfigDataLocation location = ConfigDataLocation.of("classpath:/configdata/properties/");
357+
this.environment.setActiveProfiles("dev_");
358+
Profiles profiles = new Profiles(this.environment, this.environmentBinder, Collections.emptyList());
359+
assertThatIllegalStateException()
360+
.isThrownBy(() -> this.resolver.resolveProfileSpecific(this.context, location, profiles))
361+
.withMessageStartingWith("Invalid profile 'dev_': must not end with '-' or '_'");
362+
}
363+
364+
@Test
365+
void resolveProfileSpecificWhenProfileContainsInvalidCharactersThrowsException() {
366+
ConfigDataLocation location = ConfigDataLocation.of("classpath:/configdata/properties/");
367+
this.environment.setActiveProfiles("dev*test");
368+
Profiles profiles = new Profiles(this.environment, this.environmentBinder, Collections.emptyList());
369+
assertThatIllegalStateException()
370+
.isThrownBy(() -> this.resolver.resolveProfileSpecific(this.context, location, profiles))
371+
.withMessageStartingWith("Invalid profile 'dev*test': must contain only letters or digits or '-' or '_'");
372+
}
373+
296374
private String filePath(String... components) {
297375
return "file [" + String.join(File.separator, components) + "]";
298376
}

0 commit comments

Comments
 (0)