diff --git a/spring-core/src/main/java/org/springframework/util/PlaceholderParser.java b/spring-core/src/main/java/org/springframework/util/PlaceholderParser.java
index d6cd4356401..d50625012eb 100644
--- a/spring-core/src/main/java/org/springframework/util/PlaceholderParser.java
+++ b/spring-core/src/main/java/org/springframework/util/PlaceholderParser.java
@@ -18,7 +18,6 @@ package org.springframework.util;
import java.util.ArrayList;
import java.util.HashSet;
-import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -36,10 +35,10 @@ import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver;
* that can be resolved using a {@link PlaceholderResolver PlaceholderResolver},
* ${ the prefix, and } the suffix.
*
- *
A placeholder can also have a default value if its key does not represent a
- * known property. The default value is separated from the key using a
- * {@code separator}. For instance {@code ${name:John}} resolves to {@code John} if
- * the placeholder resolver does not provide a value for the {@code name}
+ *
A placeholder can also have a default value if its key does not represent
+ * a known property. The default value is separated from the key using a
+ * {@code separator}. For instance {@code ${name:John}} resolves to {@code John}
+ * if the placeholder resolver does not provide a value for the {@code name}
* property.
*
*
Placeholders can also have a more complex structure, and the resolution of
@@ -50,13 +49,14 @@ import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver;
* must be rendered as is, the placeholder can be escaped using an {@code escape}
* character. For instance {@code \${name}} resolves as {@code ${name}}.
*
- *
The prefix, suffix, separator, and escape characters are configurable. Only
- * the prefix and suffix are mandatory, and the support for default values or
- * escaping is conditional on providing non-null values for them.
+ *
The prefix, suffix, separator, and escape characters are configurable.
+ * Only the prefix and suffix are mandatory, and the support for default values
+ * or escaping is conditional on providing non-null values for them.
*
*
This parser makes sure to resolves placeholders as lazily as possible.
*
* @author Stephane Nicoll
+ * @author Juergen Hoeller
* @since 6.2
*/
final class PlaceholderParser {
@@ -120,51 +120,47 @@ final class PlaceholderParser {
* @return the supplied value with placeholders replaced inline
*/
public String replacePlaceholders(String value, PlaceholderResolver placeholderResolver) {
- Assert.notNull(value, "'value' must not be null");
- ParsedValue parsedValue = parse(value);
+ List parts = parse(value, false);
+ if (parts == null) {
+ return value;
+ }
+ ParsedValue parsedValue = new ParsedValue(value, parts);
PartResolutionContext resolutionContext = new PartResolutionContext(placeholderResolver,
this.prefix, this.suffix, this.ignoreUnresolvablePlaceholders,
candidate -> parse(candidate, false));
return parsedValue.resolve(resolutionContext);
}
- /**
- * Parse the specified value.
- * @param value the value containing the placeholders to be replaced
- * @return the different parts that have been identified
- */
- ParsedValue parse(String value) {
- List parts = parse(value, false);
- return new ParsedValue(value, parts);
- }
-
- private List parse(String value, boolean inPlaceholder) {
- LinkedList parts = new LinkedList<>();
+ private @Nullable List parse(String value, boolean inPlaceholder) {
int startIndex = nextStartPrefix(value, 0);
if (startIndex == -1) {
- Part part = (inPlaceholder ? createSimplePlaceholderPart(value) : new TextPart(value));
- parts.add(part);
- return parts;
+ return null;
}
+ List parts = new ArrayList<>(4);
int position = 0;
while (startIndex != -1) {
int endIndex = nextValidEndPrefix(value, startIndex);
- if (endIndex == -1) { // Not a valid placeholder, consume the prefix and continue
+ if (endIndex == -1) { // Not a valid placeholder, consume the prefix and continue
addText(value, position, startIndex + this.prefix.length(), parts);
position = startIndex + this.prefix.length();
startIndex = nextStartPrefix(value, position);
}
- else if (isEscaped(value, startIndex)) { // Not a valid index, accumulate and skip the escape character
+ else if (isEscaped(value, startIndex)) { // Not a valid index, accumulate and skip the escape character
addText(value, position, startIndex - 1, parts);
addText(value, startIndex, startIndex + this.prefix.length(), parts);
position = startIndex + this.prefix.length();
startIndex = nextStartPrefix(value, position);
}
- else { // Found valid placeholder, recursive parsing
+ else { // Found valid placeholder, recursive parsing
addText(value, position, startIndex, parts);
String placeholder = value.substring(startIndex + this.prefix.length(), endIndex);
List placeholderParts = parse(placeholder, true);
- parts.addAll(placeholderParts);
+ if (placeholderParts == null) {
+ parts.add(createSimplePlaceholderPart(placeholder));
+ }
+ else {
+ parts.addAll(placeholderParts);
+ }
startIndex = nextStartPrefix(value, endIndex + this.suffix.length());
position = endIndex + this.suffix.length();
}
@@ -241,29 +237,6 @@ final class PlaceholderParser {
return new ParsedSection(buffer.toString(), null);
}
- private static void addText(String value, int start, int end, LinkedList parts) {
- if (start > end) {
- return;
- }
- String text = value.substring(start, end);
- if (!text.isEmpty()) {
- if (!parts.isEmpty()) {
- Part current = parts.removeLast();
- if (current instanceof TextPart textPart) {
- parts.add(new TextPart(textPart.text() + text));
- }
- else {
- parts.add(current);
- parts.add(new TextPart(text));
- }
- }
- else {
- parts.add(new TextPart(text));
- }
- }
- }
-
-
private int nextStartPrefix(String value, int index) {
return value.indexOf(this.prefix, index);
}
@@ -296,15 +269,46 @@ final class PlaceholderParser {
return (this.escape != null && index > 0 && value.charAt(index - 1) == this.escape);
}
- record ParsedSection(String key, @Nullable String fallback) {
+ private static void addText(String value, int start, int end, List parts) {
+ if (start >= end) {
+ return;
+ }
+ String text = value.substring(start, end);
+ if (!parts.isEmpty() && parts.get(parts.size() - 1) instanceof TextPart textPart) {
+ parts.set(parts.size() - 1, new TextPart(textPart.text() + text));
+ }
+ else {
+ parts.add(new TextPart(text));
+ }
+ }
+
+
+ /**
+ * A representation of the parsing of an input string.
+ * @param text the raw input string
+ * @param parts the parts that appear in the string, in order
+ */
+ private record ParsedValue(String text, List parts) {
+ public String resolve(PartResolutionContext resolutionContext) {
+ try {
+ return Part.resolveAll(this.parts, resolutionContext);
+ }
+ catch (PlaceholderResolutionException ex) {
+ throw ex.withValue(this.text);
+ }
+ }
+ }
+
+
+ private record ParsedSection(String key, @Nullable String fallback) {
}
/**
* Provide the necessary context to handle and resolve underlying placeholders.
*/
- static class PartResolutionContext implements PlaceholderResolver {
+ private static class PartResolutionContext implements PlaceholderResolver {
private final String prefix;
@@ -319,7 +323,6 @@ final class PlaceholderParser {
@Nullable
private Set visitedPlaceholders;
-
PartResolutionContext(PlaceholderResolver resolver, String prefix, String suffix,
boolean ignoreUnresolvablePlaceholders, Function> parser) {
this.prefix = prefix;
@@ -352,7 +355,7 @@ final class PlaceholderParser {
return this.prefix + text + this.suffix;
}
- public List parse(String text) {
+ public @Nullable List parse(String text) {
return this.parser.apply(text);
}
@@ -367,17 +370,17 @@ final class PlaceholderParser {
}
public void removePlaceholder(String placeholder) {
- Assert.state(this.visitedPlaceholders != null, "Visited placeholders must not be null");
- this.visitedPlaceholders.remove(placeholder);
+ if (this.visitedPlaceholders != null) {
+ this.visitedPlaceholders.remove(placeholder);
+ }
}
-
}
/**
* A part is a section of a String containing placeholders to replace.
*/
- interface Part {
+ private interface Part {
/**
* Resolve this part using the specified {@link PartResolutionContext}.
@@ -408,30 +411,12 @@ final class PlaceholderParser {
}
- /**
- * A representation of the parsing of an input string.
- * @param text the raw input string
- * @param parts the parts that appear in the string, in order
- */
- record ParsedValue(String text, List parts) {
-
- public String resolve(PartResolutionContext resolutionContext) {
- try {
- return Part.resolveAll(this.parts, resolutionContext);
- }
- catch (PlaceholderResolutionException ex) {
- throw ex.withValue(this.text);
- }
- }
- }
-
-
/**
* A base {@link Part} implementation.
*/
- abstract static class AbstractPart implements Part {
+ private abstract static class AbstractPart implements Part {
- private final String text;
+ final String text;
protected AbstractPart(String text) {
this.text = text;
@@ -454,29 +439,19 @@ final class PlaceholderParser {
@Nullable
protected String resolveRecursively(PartResolutionContext resolutionContext, String key) {
String resolvedValue = resolutionContext.resolvePlaceholder(key);
- if (resolvedValue != null) {
- resolutionContext.flagPlaceholderAsVisited(key);
- // Let's check if we need to recursively resolve that value
- List nestedParts = resolutionContext.parse(resolvedValue);
- String value = toText(nestedParts);
- if (!isTextOnly(nestedParts)) {
- value = new ParsedValue(resolvedValue, nestedParts).resolve(resolutionContext);
- }
- resolutionContext.removePlaceholder(key);
- return value;
+ if (resolvedValue == null) {
+ // Not found
+ return null;
}
- // Not found
- return null;
- }
-
- private boolean isTextOnly(List parts) {
- return parts.stream().allMatch(TextPart.class::isInstance);
- }
-
- private String toText(List parts) {
- StringBuilder sb = new StringBuilder();
- parts.forEach(part -> sb.append(part.text()));
- return sb.toString();
+ // Let's check if we need to recursively resolve that value
+ List nestedParts = resolutionContext.parse(resolvedValue);
+ if (nestedParts == null) {
+ return resolvedValue;
+ }
+ resolutionContext.flagPlaceholderAsVisited(key);
+ String value = new ParsedValue(resolvedValue, nestedParts).resolve(resolutionContext);
+ resolutionContext.removePlaceholder(key);
+ return value;
}
}
@@ -484,7 +459,7 @@ final class PlaceholderParser {
/**
* A {@link Part} implementation that does not contain a valid placeholder.
*/
- static class TextPart extends AbstractPart {
+ private static class TextPart extends AbstractPart {
/**
* Create a new instance.
@@ -496,7 +471,7 @@ final class PlaceholderParser {
@Override
public String resolve(PartResolutionContext resolutionContext) {
- return text();
+ return this.text;
}
}
@@ -505,7 +480,7 @@ final class PlaceholderParser {
* A {@link Part} implementation that represents a single placeholder with
* a hard-coded fallback.
*/
- static class SimplePlaceholderPart extends AbstractPart {
+ private static class SimplePlaceholderPart extends AbstractPart {
private final String key;
@@ -533,13 +508,13 @@ final class PlaceholderParser {
else if (this.fallback != null) {
return this.fallback;
}
- return resolutionContext.handleUnresolvablePlaceholder(this.key, text());
+ return resolutionContext.handleUnresolvablePlaceholder(this.key, this.text);
}
@Nullable
private String resolveRecursively(PartResolutionContext resolutionContext) {
- if (!this.text().equals(this.key)) {
- String value = resolveRecursively(resolutionContext, this.text());
+ if (!this.text.equals(this.key)) {
+ String value = resolveRecursively(resolutionContext, this.text);
if (value != null) {
return value;
}
@@ -553,7 +528,7 @@ final class PlaceholderParser {
* A {@link Part} implementation that represents a single placeholder
* containing nested placeholders.
*/
- static class NestedPlaceholderPart extends AbstractPart {
+ private static class NestedPlaceholderPart extends AbstractPart {
private final List keyParts;
@@ -582,7 +557,7 @@ final class PlaceholderParser {
else if (this.defaultParts != null) {
return Part.resolveAll(this.defaultParts, resolutionContext);
}
- return resolutionContext.handleUnresolvablePlaceholder(resolvedKey, text());
+ return resolutionContext.handleUnresolvablePlaceholder(resolvedKey, this.text);
}
}
diff --git a/spring-core/src/main/java/org/springframework/util/PropertyPlaceholderHelper.java b/spring-core/src/main/java/org/springframework/util/PropertyPlaceholderHelper.java
index f262435aba4..856d691fa2c 100644
--- a/spring-core/src/main/java/org/springframework/util/PropertyPlaceholderHelper.java
+++ b/spring-core/src/main/java/org/springframework/util/PropertyPlaceholderHelper.java
@@ -97,7 +97,7 @@ public class PropertyPlaceholderHelper {
* @param properties the {@code Properties} to use for replacement
* @return the supplied value with placeholders replaced inline
*/
- public String replacePlaceholders(String value, final Properties properties) {
+ public String replacePlaceholders(String value, Properties properties) {
Assert.notNull(properties, "'properties' must not be null");
return replacePlaceholders(value, properties::getProperty);
}
@@ -111,9 +111,10 @@ public class PropertyPlaceholderHelper {
*/
public String replacePlaceholders(String value, PlaceholderResolver placeholderResolver) {
Assert.notNull(value, "'value' must not be null");
- return parseStringValue(value, placeholderResolver);
+ return this.parser.replacePlaceholders(value, placeholderResolver);
}
+ @Deprecated(since = "6.2.12", forRemoval = true)
protected String parseStringValue(String value, PlaceholderResolver placeholderResolver) {
return this.parser.replacePlaceholders(value, placeholderResolver);
}
diff --git a/spring-core/src/test/java/org/springframework/util/PlaceholderParserTests.java b/spring-core/src/test/java/org/springframework/util/PlaceholderParserTests.java
index a882a6c56ce..4d7073b7636 100644
--- a/spring-core/src/test/java/org/springframework/util/PlaceholderParserTests.java
+++ b/spring-core/src/test/java/org/springframework/util/PlaceholderParserTests.java
@@ -26,8 +26,6 @@ import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.InOrder;
-import org.springframework.util.PlaceholderParser.ParsedValue;
-import org.springframework.util.PlaceholderParser.TextPart;
import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver;
import static org.assertj.core.api.Assertions.assertThat;
@@ -43,10 +41,11 @@ import static org.mockito.Mockito.verifyNoMoreInteractions;
*
* @author Stephane Nicoll
* @author Sam Brannen
+ * @author Juergen Hoeller
*/
class PlaceholderParserTests {
- @Nested // Tests with only the basic placeholder feature enabled
+ @Nested // Tests with only the basic placeholder feature enabled
class OnlyPlaceholderTests {
private final PlaceholderParser parser = new PlaceholderParser("${", "}", null, null, true);
@@ -82,7 +81,7 @@ class PlaceholderParserTests {
Map properties = Map.of(
"p1", "v1",
"p2", "v2",
- "p3", "${p1}:${p2}", // nested placeholders
+ "p3", "${p1}:${p2}", // nested placeholders
"p4", "${p3}", // deeply nested placeholders
"p5", "${p1}:${p2}:${bogus}"); // unresolvable placeholder
assertThat(this.parser.replacePlaceholders(text, properties::get)).isEqualTo(expected);
@@ -154,14 +153,13 @@ class PlaceholderParserTests {
@Test
void textWithInvalidPlaceholderSyntaxIsMerged() {
String text = "test${of${with${and${";
- ParsedValue parsedValue = this.parser.parse(text);
- assertThat(parsedValue.parts()).singleElement().isInstanceOfSatisfying(TextPart.class,
- textPart -> assertThat(textPart.text()).isEqualTo(text));
+ assertThat(this.parser.replacePlaceholders(text,
+ placeholder -> {throw new UnsupportedOperationException();})).isEqualTo(text);
}
-
}
- @Nested // Tests with the use of a separator
+
+ @Nested // Tests with the use of a separator
class DefaultValueTests {
private final PlaceholderParser parser = new PlaceholderParser("${", "}", ":", null, true);
@@ -195,7 +193,7 @@ class PlaceholderParserTests {
Map properties = Map.of(
"p1", "v1",
"p2", "v2",
- "p3", "${p1}:${p2}", // nested placeholders
+ "p3", "${p1}:${p2}", // nested placeholders
"p4", "${p3}", // deeply nested placeholders
"p5", "${p1}:${p2}:${bogus}", // unresolvable placeholder
"p6", "${p1}:${p2}:${bogus:def}"); // unresolvable w/ default
@@ -259,9 +257,9 @@ class PlaceholderParserTests {
assertThat(this.parser.replacePlaceholders("${invalid:${firstName}}", resolver)).isEqualTo("John");
verifyPlaceholderResolutions(resolver, "invalid", "firstName");
}
-
}
+
/**
* Tests that use the escape character.
*/
@@ -341,9 +339,9 @@ class PlaceholderParserTests {
Arguments.of("${service/host/${app.environment}/name:\\value}", "https://example.com/qa/name"),
Arguments.of("${service/host/${name\\:value}/}", "${service/host/${name:value}/}"));
}
-
}
+
@Nested
class ExceptionTests {
@@ -378,7 +376,6 @@ class PlaceholderParserTests {
.withMessage("Could not resolve placeholder 'bogus' in value \"${p1}:${p2}:${bogus}\" <-- \"${p3}\"")
.withNoCause();
}
-
}