Skip to content

Add possibility to escape property placeholders #30671

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ public class PropertyPlaceholderHelper {

private final boolean ignoreUnresolvablePlaceholders;

private final char escapeCharacter;

/**
* Creates a new {@code PropertyPlaceholderHelper} that uses the supplied prefix and suffix.
Expand All @@ -71,11 +72,12 @@ public class PropertyPlaceholderHelper {
* @param placeholderSuffix the suffix that denotes the end of a placeholder
*/
public PropertyPlaceholderHelper(String placeholderPrefix, String placeholderSuffix) {
this(placeholderPrefix, placeholderSuffix, null, true);
this(placeholderPrefix, placeholderSuffix, null, true, '\\');
}

/**
* Creates a new {@code PropertyPlaceholderHelper} that uses the supplied prefix and suffix.
* Default escape character '\' is used.
* @param placeholderPrefix the prefix that denotes the start of a placeholder
* @param placeholderSuffix the suffix that denotes the end of a placeholder
* @param valueSeparator the separating character between the placeholder variable
Expand All @@ -86,6 +88,23 @@ public PropertyPlaceholderHelper(String placeholderPrefix, String placeholderSuf
public PropertyPlaceholderHelper(String placeholderPrefix, String placeholderSuffix,
@Nullable String valueSeparator, boolean ignoreUnresolvablePlaceholders) {

this(placeholderPrefix, placeholderSuffix, valueSeparator, ignoreUnresolvablePlaceholders, '\\');
}

/**
* Creates a new {@code PropertyPlaceholderHelper} that uses the supplied prefix and suffix.
* @param placeholderPrefix the prefix that denotes the start of a placeholder
* @param placeholderSuffix the suffix that denotes the end of a placeholder
* @param valueSeparator the separating character between the placeholder variable
* and the associated default value, if any
* @param ignoreUnresolvablePlaceholders indicates whether unresolvable placeholders should
* be ignored ({@code true}) or cause an exception ({@code false})
* @param escapeCharacter the escape character that denotes that the following placeholder
* must not be resolved
*/
public PropertyPlaceholderHelper(String placeholderPrefix, String placeholderSuffix,
@Nullable String valueSeparator, boolean ignoreUnresolvablePlaceholders, char escapeCharacter) {

Assert.notNull(placeholderPrefix, "'placeholderPrefix' must not be null");
Assert.notNull(placeholderSuffix, "'placeholderSuffix' must not be null");
this.placeholderPrefix = placeholderPrefix;
Expand All @@ -99,6 +118,7 @@ public PropertyPlaceholderHelper(String placeholderPrefix, String placeholderSuf
}
this.valueSeparator = valueSeparator;
this.ignoreUnresolvablePlaceholders = ignoreUnresolvablePlaceholders;
this.escapeCharacter = escapeCharacter;
}


Expand Down Expand Up @@ -128,92 +148,148 @@ public String replacePlaceholders(String value, PlaceholderResolver placeholderR

protected String parseStringValue(
String value, PlaceholderResolver placeholderResolver, @Nullable Set<String> visitedPlaceholders) {
Property property = new Property(value, placeholderResolver);
property.resolvePlaceholders(visitedPlaceholders);
return property.removeEscapeCharacters();
}

protected class Property {

int startIndex = value.indexOf(this.placeholderPrefix);
if (startIndex == -1) {
return value;
protected final String escapedPrefix = escapeCharacter + placeholderPrefix;

protected final String originalPlaceholder;
protected final StringBuilder result;
protected final PlaceholderResolver placeholderResolver;

protected Property(String value, PlaceholderResolver placeholderResolver) {
this.originalPlaceholder = value;
this.result = new StringBuilder(value);
this.placeholderResolver = placeholderResolver;
}

StringBuilder result = new StringBuilder(value);
while (startIndex != -1) {
int endIndex = findPlaceholderEndIndex(result, startIndex);
if (endIndex != -1) {
String placeholder = result.substring(startIndex + this.placeholderPrefix.length(), endIndex);
String originalPlaceholder = placeholder;
if (visitedPlaceholders == null) {
visitedPlaceholders = new HashSet<>(4);
}
if (!visitedPlaceholders.add(originalPlaceholder)) {
throw new IllegalArgumentException(
"Circular placeholder reference '" + originalPlaceholder + "' in property definitions");
}
// Recursive invocation, parsing placeholders contained in the placeholder key.
placeholder = parseStringValue(placeholder, placeholderResolver, visitedPlaceholders);
// Now obtain the value for the fully resolved key...
String propVal = placeholderResolver.resolvePlaceholder(placeholder);
if (propVal == null && this.valueSeparator != null) {
int separatorIndex = placeholder.indexOf(this.valueSeparator);
if (separatorIndex != -1) {
String actualPlaceholder = placeholder.substring(0, separatorIndex);
String defaultValue = placeholder.substring(separatorIndex + this.valueSeparator.length());
propVal = placeholderResolver.resolvePlaceholder(actualPlaceholder);
if (propVal == null) {
propVal = defaultValue;
protected String resolvePlaceholders(Set<String> visitedPlaceholders) {
int startIndex = findPlaceholderPrefixIndex(0);

while (startIndex != -1) {
int endIndex = findPlaceholderEndIndex(startIndex);
if (endIndex != -1) {
String placeholderWithoutBrackets = this.result.substring(startIndex + placeholderPrefix.length(), endIndex);
visitedPlaceholders = updateVisitedPlaceholders(visitedPlaceholders, placeholderWithoutBrackets);

Property property = new Property(placeholderWithoutBrackets, this.placeholderResolver);
property.resolvePlaceholders(visitedPlaceholders);
String placeholderValue = property.getValue();

if (placeholderValue != null) {
Property valueProperty = new Property(placeholderValue, this.placeholderResolver);
placeholderValue = valueProperty.resolvePlaceholders(visitedPlaceholders);
this.result.replace(startIndex, endIndex + placeholderSuffix.length(), placeholderValue);
if (logger.isTraceEnabled()) {
logger.trace("Resolved placeholder '" + property.result + "'");
}
startIndex = this.result.indexOf(placeholderPrefix, startIndex + placeholderValue.length());

}
}
if (propVal != null) {
// Recursive invocation, parsing placeholders contained in the
// previously resolved placeholder value.
propVal = parseStringValue(propVal, placeholderResolver, visitedPlaceholders);
result.replace(startIndex, endIndex + this.placeholderSuffix.length(), propVal);
if (logger.isTraceEnabled()) {
logger.trace("Resolved placeholder '" + placeholder + "'");
else if (ignoreUnresolvablePlaceholders) {
// Proceed with unprocessed value.
startIndex = this.result.indexOf(placeholderPrefix, endIndex + placeholderSuffix.length());
}
startIndex = result.indexOf(this.placeholderPrefix, startIndex + propVal.length());
}
else if (this.ignoreUnresolvablePlaceholders) {
// Proceed with unprocessed value.
startIndex = result.indexOf(this.placeholderPrefix, endIndex + this.placeholderSuffix.length());
else {
throw new IllegalArgumentException("Could not resolve placeholder '" +
property.result + "'" + " in value \"" + this.originalPlaceholder + "\"");
}
visitedPlaceholders.remove(placeholderWithoutBrackets);
}
else {
throw new IllegalArgumentException("Could not resolve placeholder '" +
placeholder + "'" + " in value \"" + value + "\"");
startIndex = -1;
}
visitedPlaceholders.remove(originalPlaceholder);
}
else {
startIndex = -1;
return this.result.toString();
}

protected Set<String> updateVisitedPlaceholders(Set<String> visitedPlaceholders, String placeholderWithoutBrackets) {
if (visitedPlaceholders == null) {
visitedPlaceholders = new HashSet<>(4);
}
if (!visitedPlaceholders.add(placeholderWithoutBrackets)) {
throw new IllegalArgumentException(
"Circular placeholder reference '" + placeholderWithoutBrackets + "' in property definitions");
}
return visitedPlaceholders;
}
return result.toString();
}

private int findPlaceholderEndIndex(CharSequence buf, int startIndex) {
int index = startIndex + this.placeholderPrefix.length();
int withinNestedPlaceholder = 0;
while (index < buf.length()) {
if (StringUtils.substringMatch(buf, index, this.placeholderSuffix)) {
if (withinNestedPlaceholder > 0) {
withinNestedPlaceholder--;
index = index + this.placeholderSuffix.length();
protected int findPlaceholderPrefixIndex(int startIndex) {
int prefixIndex = this.result.indexOf(placeholderPrefix, startIndex);
//check if prefix is not escaped
if (prefixIndex < 1) {
return prefixIndex;
}
if (this.result.charAt(prefixIndex - 1) == escapeCharacter) {
int endOfEscaped = findPlaceholderEndIndex(prefixIndex);
if (endOfEscaped == -1) {
return -1;
}
return findPlaceholderPrefixIndex(endOfEscaped + 1);
}
return prefixIndex;
}

protected int findPlaceholderEndIndex(int startIndex) {
int index = startIndex + placeholderPrefix.length();
int withinNestedPlaceholder = 0;
while (index < this.result.length()) {
if (StringUtils.substringMatch(this.result, index, placeholderSuffix)) {
if (withinNestedPlaceholder > 0) {
withinNestedPlaceholder--;
index = index + placeholderSuffix.length();
}
else {
return index;
}
}
else if (StringUtils.substringMatch(this.result, index, simplePrefix)) {
withinNestedPlaceholder++;
index = index + simplePrefix.length();
}
else {
return index;
index++;
}
}
else if (StringUtils.substringMatch(buf, index, this.simplePrefix)) {
withinNestedPlaceholder++;
index = index + this.simplePrefix.length();
return -1;
}

protected String getValue() {
String propVal = this.placeholderResolver.resolvePlaceholder(this.result.toString());
if (propVal == null && valueSeparator != null) {
int separatorIndex = this.result.indexOf(valueSeparator);
if (separatorIndex != -1) {
String actualPlaceholder = this.result.substring(0, separatorIndex);
String defaultValue = this.result.substring(separatorIndex + valueSeparator.length());
propVal = this.placeholderResolver.resolvePlaceholder(actualPlaceholder);
if (propVal == null) {
propVal = defaultValue;
}
}
}
else {
index++;
return propVal;
}

protected String removeEscapeCharacters() {
int startIndex = this.result.indexOf(this.escapedPrefix);
while (startIndex != -1) {
int endIndex = findPlaceholderEndIndex(startIndex + 1);
if (endIndex != -1) {
this.result.replace(startIndex, startIndex + this.escapedPrefix.length(), placeholderPrefix);
startIndex = this.result.indexOf(this.escapedPrefix, endIndex + placeholderSuffix.length());
}
else {
break;
}
}
return this.result.toString();
}
return -1;
}


/**
* Strategy interface used to resolve replacement values for placeholders contained in Strings.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,13 @@ void resolvePlaceholders() {
assertThat(resolver.resolvePlaceholders("Replace this ${key}")).isEqualTo("Replace this value");
}

@Test
void ignoreEscapedPlaceholders() {
MutablePropertySources propertySources = new MutablePropertySources();
PropertyResolver resolver = new PropertySourcesPropertyResolver(propertySources);
assertThat(resolver.resolvePlaceholders("Replace this \\${key}")).isEqualTo("Replace this ${key}");
}

@Test
void resolvePlaceholders_withUnresolvable() {
MutablePropertySources propertySources = new MutablePropertySources();
Expand Down Expand Up @@ -299,6 +306,29 @@ void resolveNestedPropertyPlaceholders() {
.withMessageContaining("Circular");
}

@Test
void ignoreNestedEscapedPlaceholders() {
MutablePropertySources ps = new MutablePropertySources();
ps.addFirst(new MockPropertySource()
.withProperty("p1", "v1")
.withProperty("p2", "\\${p1:default}")
.withProperty("p3", "${p2}")
.withProperty("p4", "adc${p0:\\${p1}}")
.withProperty("p5", "adc${\\${p0}:${p1}}")
.withProperty("p6", "adc${p0:def\\${p1}}")
.withProperty("p7", "adc\\${")

);
ConfigurablePropertyResolver pr = new PropertySourcesPropertyResolver(ps);
assertThat(pr.getProperty("p1")).isEqualTo("v1");
assertThat(pr.getProperty("p2")).isEqualTo("${p1:default}");
assertThat(pr.getProperty("p3")).isEqualTo("${p1:default}");
assertThat(pr.getProperty("p4")).isEqualTo("adc${p1}");
assertThat(pr.getProperty("p5")).isEqualTo("adcv1");
assertThat(pr.getProperty("p6")).isEqualTo("adcdef${p1}");
assertThat(pr.getProperty("p7")).isEqualTo("adc\\${");
}

@Test
void ignoreUnresolvableNestedPlaceholdersIsConfigurable() {
MutablePropertySources ps = new MutablePropertySources();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,4 +108,37 @@ void unresolvedPlaceholderAsError() {
helper.replacePlaceholders(text, props));
}

@Test
void escapedPlaceholder() {
String text = "foo=\\${foo},bar=${bar}";
Properties props = new Properties();
props.setProperty("bar", "foo");

assertThat(this.helper.replacePlaceholders(text, props)).isEqualTo("foo=${foo},bar=foo");
}

@Test
void escapedPlaceholderWithRecursionInProperty() {
String text = "foo=${bar}";
Properties props = new Properties();
props.setProperty("bar", "${baz}");
props.setProperty("baz", "\\${bar}");

assertThat(this.helper.replacePlaceholders(text, props)).isEqualTo("foo=${bar}");
}

@Test
void escapedPlaceholderWithRecursionInPlaceholder() {
String text = "foo=\\${b${inner}}";
Properties props = new Properties();
props.setProperty("inner", "ar");

assertThat(this.helper.replacePlaceholders(text, props)).isEqualTo("foo=${b${inner}}");

text = "foo=${b\\${in${insideInner}r}}";
props = new Properties();
props.setProperty("insideInner", "ne");
assertThat(this.helper.replacePlaceholders(text, props)).isEqualTo("foo=${b${in${insideInner}r}}");
}

}