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 c35c0486025e..cfa4c8c00f14 100644 --- a/spring-core/src/main/java/org/springframework/util/PropertyPlaceholderHelper.java +++ b/spring-core/src/main/java/org/springframework/util/PropertyPlaceholderHelper.java @@ -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. @@ -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 @@ -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; @@ -99,6 +118,7 @@ public PropertyPlaceholderHelper(String placeholderPrefix, String placeholderSuf } this.valueSeparator = valueSeparator; this.ignoreUnresolvablePlaceholders = ignoreUnresolvablePlaceholders; + this.escapeCharacter = escapeCharacter; } @@ -128,92 +148,148 @@ public String replacePlaceholders(String value, PlaceholderResolver placeholderR protected String parseStringValue( String value, PlaceholderResolver placeholderResolver, @Nullable Set 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 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 updateVisitedPlaceholders(Set 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. */ diff --git a/spring-core/src/test/java/org/springframework/core/env/PropertySourcesPropertyResolverTests.java b/spring-core/src/test/java/org/springframework/core/env/PropertySourcesPropertyResolverTests.java index f51fc97e9a02..5e5cc860afdd 100644 --- a/spring-core/src/test/java/org/springframework/core/env/PropertySourcesPropertyResolverTests.java +++ b/spring-core/src/test/java/org/springframework/core/env/PropertySourcesPropertyResolverTests.java @@ -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(); @@ -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(); diff --git a/spring-core/src/test/java/org/springframework/util/PropertyPlaceholderHelperTests.java b/spring-core/src/test/java/org/springframework/util/PropertyPlaceholderHelperTests.java index 92c1595b57f7..c77704ede7d1 100644 --- a/spring-core/src/test/java/org/springframework/util/PropertyPlaceholderHelperTests.java +++ b/spring-core/src/test/java/org/springframework/util/PropertyPlaceholderHelperTests.java @@ -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}}"); + } + }