Skip to content

Commit 82bc4ff

Browse files
committed
Handle TextPart with escaped separator
This commit harmonizes how a candidate value is parsed to extract its key and default, if any. Rather than returning {@code null} if no default is available, `splitKeyAndValue` now consistently returns a non-null array. This prevents an escaped separator character to be mistakenly identified as a placeholder in certain cases. Closes gh-34289
1 parent 356d5c2 commit 82bc4ff

File tree

2 files changed

+48
-21
lines changed

2 files changed

+48
-21
lines changed

spring-core/src/main/java/org/springframework/util/PlaceholderParser.java

+27-20
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2024 the original author or authors.
2+
* Copyright 2002-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.
@@ -175,9 +175,8 @@ else if (isEscaped(value, startIndex)) { // Not a valid index, accumulate and sk
175175
}
176176

177177
private SimplePlaceholderPart createSimplePlaceholderPart(String text) {
178-
String[] keyAndDefault = splitKeyAndDefault(text);
179-
return ((keyAndDefault != null) ? new SimplePlaceholderPart(text, keyAndDefault[0], keyAndDefault[1]) :
180-
new SimplePlaceholderPart(text, text, null));
178+
ParsedSection section = parseSection(text);
179+
return new SimplePlaceholderPart(text, section.key(), section.fallback());
181180
}
182181

183182
private NestedPlaceholderPart createNestedPlaceholderPart(String text, List<Part> parts) {
@@ -193,28 +192,32 @@ private NestedPlaceholderPart createNestedPlaceholderPart(String text, List<Part
193192
}
194193
else {
195194
String candidate = part.text();
196-
String[] keyAndDefault = splitKeyAndDefault(candidate);
197-
if (keyAndDefault != null) {
198-
keyParts.add(new TextPart(keyAndDefault[0]));
199-
if (keyAndDefault[1] != null) {
200-
defaultParts.add(new TextPart(keyAndDefault[1]));
201-
}
195+
ParsedSection section = parseSection(candidate);
196+
keyParts.add(new TextPart(section.key()));
197+
if (section.fallback() != null) {
198+
defaultParts.add(new TextPart(section.fallback()));
202199
defaultParts.addAll(parts.subList(i + 1, parts.size()));
203200
return new NestedPlaceholderPart(text, keyParts, defaultParts);
204201
}
205-
else {
206-
keyParts.add(part);
207-
}
208202
}
209203
}
210-
// No separator found
211-
return new NestedPlaceholderPart(text, parts, null);
204+
return new NestedPlaceholderPart(text, keyParts, null);
212205
}
213206

214-
@Nullable
215-
private String[] splitKeyAndDefault(String value) {
207+
/**
208+
* Parse an input value that may contain a separator character and return a
209+
* {@link ParsedValue}. If a valid separator character has been identified, the
210+
* given {@code value} is split between a {@code key} and a {@code fallback}. If not,
211+
* only the {@code key} is set.
212+
* <p>
213+
* The returned key may be different from the original value as escaped
214+
* separators, if any, are resolved.
215+
* @param value the value to parse
216+
* @return the parsed section
217+
*/
218+
private ParsedSection parseSection(String value) {
216219
if (this.separator == null || !value.contains(this.separator)) {
217-
return null;
220+
return new ParsedSection(value, null);
218221
}
219222
int position = 0;
220223
int index = value.indexOf(this.separator, position);
@@ -231,11 +234,11 @@ private String[] splitKeyAndDefault(String value) {
231234
buffer.append(value, position, index);
232235
String key = buffer.toString();
233236
String fallback = value.substring(index + this.separator.length());
234-
return new String[] { key, fallback };
237+
return new ParsedSection(key, fallback);
235238
}
236239
}
237240
buffer.append(value, position, value.length());
238-
return new String[] { buffer.toString(), null };
241+
return new ParsedSection(buffer.toString(), null);
239242
}
240243

241244
private static void addText(String value, int start, int end, LinkedList<Part> parts) {
@@ -293,6 +296,10 @@ private boolean isEscaped(String value, int index) {
293296
return (this.escape != null && index > 0 && value.charAt(index - 1) == this.escape);
294297
}
295298

299+
record ParsedSection(String key, @Nullable String fallback) {
300+
301+
}
302+
296303

297304
/**
298305
* Provide the necessary context to handle and resolve underlying placeholders.

spring-core/src/test/java/org/springframework/util/PlaceholderParserTests.java

+21-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2024 the original author or authors.
2+
* Copyright 2002-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.
@@ -278,6 +278,26 @@ class EscapedTests {
278278

279279
private final PlaceholderParser parser = new PlaceholderParser("${", "}", ":", '\\', true);
280280

281+
@ParameterizedTest(name = "{0} -> {1}")
282+
@MethodSource("escapedInNestedPlaceholders")
283+
void escapedSeparatorInNestedPlaceholder(String text, String expected) {
284+
Properties properties = new Properties();
285+
properties.setProperty("app.environment", "qa");
286+
properties.setProperty("app.service", "protocol");
287+
properties.setProperty("protocol://host/qa/name", "protocol://example.com/qa/name");
288+
properties.setProperty("service/host/qa/name", "https://example.com/qa/name");
289+
properties.setProperty("service/host/qa/name:value", "https://example.com/qa/name-value");
290+
assertThat(this.parser.replacePlaceholders(text, properties::getProperty)).isEqualTo(expected);
291+
}
292+
293+
static Stream<Arguments> escapedInNestedPlaceholders() {
294+
return Stream.of(
295+
Arguments.of("${protocol\\://host/${app.environment}/name}", "protocol://example.com/qa/name"),
296+
Arguments.of("${${app.service}\\://host/${app.environment}/name}", "protocol://example.com/qa/name"),
297+
Arguments.of("${service/host/${app.environment}/name:\\value}", "https://example.com/qa/name"),
298+
Arguments.of("${service/host/${name\\:value}/}", "${service/host/${name:value}/}"));
299+
}
300+
281301
@ParameterizedTest(name = "{0} -> {1}")
282302
@MethodSource("escapedPlaceholders")
283303
void escapedPlaceholderIsNotReplaced(String text, String expected) {

0 commit comments

Comments
 (0)