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 53182acf882..b16f741517a 100644 --- a/spring-core/src/test/java/org/springframework/util/PlaceholderParserTests.java +++ b/spring-core/src/test/java/org/springframework/util/PlaceholderParserTests.java @@ -16,7 +16,7 @@ package org.springframework.util; -import java.util.Properties; +import java.util.Map; import java.util.stream.Stream; import org.junit.jupiter.api.Nested; @@ -36,13 +36,13 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; /** * Tests for {@link PlaceholderParser}. * * @author Stephane Nicoll + * @author Sam Brannen */ class PlaceholderParserTests { @@ -54,11 +54,11 @@ class PlaceholderParserTests { @ParameterizedTest(name = "{0} -> {1}") @MethodSource("placeholders") void placeholderIsReplaced(String text, String expected) { - Properties properties = new Properties(); - properties.setProperty("firstName", "John"); - properties.setProperty("nested0", "first"); - properties.setProperty("nested1", "Name"); - assertThat(this.parser.replacePlaceholders(text, properties::getProperty)).isEqualTo(expected); + Map properties = Map.of( + "firstName", "John", + "nested0", "first", + "nested1", "Name"); + assertThat(this.parser.replacePlaceholders(text, properties::get)).isEqualTo(expected); } static Stream placeholders() { @@ -79,13 +79,13 @@ class PlaceholderParserTests { @ParameterizedTest(name = "{0} -> {1}") @MethodSource("nestedPlaceholders") void nestedPlaceholdersAreReplaced(String text, String expected) { - Properties properties = new Properties(); - properties.setProperty("p1", "v1"); - properties.setProperty("p2", "v2"); - properties.setProperty("p3", "${p1}:${p2}"); // nested placeholders - properties.setProperty("p4", "${p3}"); // deeply nested placeholders - properties.setProperty("p5", "${p1}:${p2}:${bogus}"); // unresolvable placeholder - assertThat(this.parser.replacePlaceholders(text, properties::getProperty)).isEqualTo(expected); + Map properties = Map.of( + "p1", "v1", + "p2", "v2", + "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); } static Stream nestedPlaceholders() { @@ -101,19 +101,15 @@ class PlaceholderParserTests { @Test void parseWithSinglePlaceholder() { PlaceholderResolver resolver = mockPlaceholderResolver("firstName", "John"); - assertThat(this.parser.replacePlaceholders("${firstName}", resolver)) - .isEqualTo("John"); - verify(resolver).resolvePlaceholder("firstName"); - verifyNoMoreInteractions(resolver); + assertThat(this.parser.replacePlaceholders("${firstName}", resolver)).isEqualTo("John"); + verifyPlaceholderResolutions(resolver, "firstName"); } @Test void parseWithPlaceholderAndPrefixText() { PlaceholderResolver resolver = mockPlaceholderResolver("firstName", "John"); - assertThat(this.parser.replacePlaceholders("This is ${firstName}", resolver)) - .isEqualTo("This is John"); - verify(resolver).resolvePlaceholder("firstName"); - verifyNoMoreInteractions(resolver); + assertThat(this.parser.replacePlaceholders("This is ${firstName}", resolver)).isEqualTo("This is John"); + verifyPlaceholderResolutions(resolver, "firstName"); } @Test @@ -121,31 +117,25 @@ class PlaceholderParserTests { PlaceholderResolver resolver = mockPlaceholderResolver("firstName", "John", "lastName", "Smith"); assertThat(this.parser.replacePlaceholders("User: ${firstName} - ${lastName}.", resolver)) .isEqualTo("User: John - Smith."); - verify(resolver).resolvePlaceholder("firstName"); - verify(resolver).resolvePlaceholder("lastName"); - verifyNoMoreInteractions(resolver); + verifyPlaceholderResolutions(resolver, "firstName", "lastName"); } @Test void parseWithNestedPlaceholderInKey() { - PlaceholderResolver resolver = mockPlaceholderResolver( - "nested", "Name", "firstName", "John"); - assertThat(this.parser.replacePlaceholders("${first${nested}}", resolver)) - .isEqualTo("John"); + PlaceholderResolver resolver = mockPlaceholderResolver("nested", "Name", "firstName", "John"); + assertThat(this.parser.replacePlaceholders("${first${nested}}", resolver)).isEqualTo("John"); verifyPlaceholderResolutions(resolver, "nested", "firstName"); } @Test void parseWithMultipleNestedPlaceholdersInKey() { - PlaceholderResolver resolver = mockPlaceholderResolver( - "nested0", "first", "nested1", "Name", "firstName", "John"); - assertThat(this.parser.replacePlaceholders("${${nested0}${nested1}}", resolver)) - .isEqualTo("John"); + PlaceholderResolver resolver = mockPlaceholderResolver("nested0", "first", "nested1", "Name", "firstName", "John"); + assertThat(this.parser.replacePlaceholders("${${nested0}${nested1}}", resolver)).isEqualTo("John"); verifyPlaceholderResolutions(resolver, "nested0", "nested1", "firstName"); } @Test - void placeholdersWithSeparatorAreHandledAsIs() { + void placeholderValueContainingSeparatorIsHandledAsIs() { PlaceholderResolver resolver = mockPlaceholderResolver("my:test", "value"); assertThat(this.parser.replacePlaceholders("${my:test}", resolver)).isEqualTo("value"); verifyPlaceholderResolutions(resolver, "my:test"); @@ -153,17 +143,20 @@ class PlaceholderParserTests { @Test void placeholdersWithoutEscapeCharAreNotEscaped() { - PlaceholderResolver resolver = mockPlaceholderResolver("test", "value"); - assertThat(this.parser.replacePlaceholders("\\${test}", resolver)).isEqualTo("\\value"); - verifyPlaceholderResolutions(resolver, "test"); + PlaceholderResolver resolver = mockPlaceholderResolver("p1", "v1", "p2", "v2", "p3", "v3", "p4", "v4"); + assertThat(this.parser.replacePlaceholders("\\${p1}", resolver)).isEqualTo("\\v1"); + assertThat(this.parser.replacePlaceholders("\\\\${p2}", resolver)).isEqualTo("\\\\v2"); + assertThat(this.parser.replacePlaceholders("\\${p3}\\", resolver)).isEqualTo("\\v3\\"); + assertThat(this.parser.replacePlaceholders("a\\${p4}\\z", resolver)).isEqualTo("a\\v4\\z"); + verifyPlaceholderResolutions(resolver, "p1", "p2", "p3", "p4"); } @Test - void textWithInvalidPlaceholderIsMerged() { + 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(parsedValue.parts()).singleElement().isInstanceOfSatisfying(TextPart.class, + textPart -> assertThat(textPart.text()).isEqualTo(text)); } } @@ -176,11 +169,11 @@ class PlaceholderParserTests { @ParameterizedTest(name = "{0} -> {1}") @MethodSource("placeholders") void placeholderIsReplaced(String text, String expected) { - Properties properties = new Properties(); - properties.setProperty("firstName", "John"); - properties.setProperty("nested0", "first"); - properties.setProperty("nested1", "Name"); - assertThat(this.parser.replacePlaceholders(text, properties::getProperty)).isEqualTo(expected); + Map properties = Map.of( + "firstName", "John", + "nested0", "first", + "nested1", "Name"); + assertThat(this.parser.replacePlaceholders(text, properties::get)).isEqualTo(expected); } static Stream placeholders() { @@ -199,14 +192,14 @@ class PlaceholderParserTests { @ParameterizedTest(name = "{0} -> {1}") @MethodSource("nestedPlaceholders") void nestedPlaceholdersAreReplaced(String text, String expected) { - Properties properties = new Properties(); - properties.setProperty("p1", "v1"); - properties.setProperty("p2", "v2"); - properties.setProperty("p3", "${p1}:${p2}"); // nested placeholders - properties.setProperty("p4", "${p3}"); // deeply nested placeholders - properties.setProperty("p5", "${p1}:${p2}:${bogus}"); // unresolvable placeholder - properties.setProperty("p6", "${p1}:${p2}:${bogus:def}"); // unresolvable w/ default - assertThat(this.parser.replacePlaceholders(text, properties::getProperty)).isEqualTo(expected); + Map properties = Map.of( + "p1", "v1", + "p2", "v2", + "p3", "${p1}:${p2}", // nested placeholders + "p4", "${p3}", // deeply nested placeholders + "p5", "${p1}:${p2}:${bogus}", // unresolvable placeholder + "p6", "${p1}:${p2}:${bogus:def}"); // unresolvable w/ default + assertThat(this.parser.replacePlaceholders(text, properties::get)).isEqualTo(expected); } static Stream nestedPlaceholders() { @@ -225,11 +218,11 @@ class PlaceholderParserTests { @ParameterizedTest(name = "{0} -> {1}") @MethodSource("exactMatchPlaceholders") void placeholdersWithExactMatchAreConsidered(String text, String expected) { - Properties properties = new Properties(); - properties.setProperty("prefix://my-service", "example-service"); - properties.setProperty("px", "prefix"); - properties.setProperty("p1", "${prefix://my-service}"); - assertThat(this.parser.replacePlaceholders(text, properties::getProperty)).isEqualTo(expected); + Map properties = Map.of( + "prefix://my-service", "example-service", + "px", "prefix", + "p1", "${prefix://my-service}"); + assertThat(this.parser.replacePlaceholders(text, properties::get)).isEqualTo(expected); } static Stream exactMatchPlaceholders() { @@ -242,74 +235,55 @@ class PlaceholderParserTests { @Test void parseWithKeyEqualsToText() { PlaceholderResolver resolver = mockPlaceholderResolver("firstName", "Steve"); - assertThat(this.parser.replacePlaceholders("${firstName}", resolver)) - .isEqualTo("Steve"); + assertThat(this.parser.replacePlaceholders("${firstName}", resolver)).isEqualTo("Steve"); verifyPlaceholderResolutions(resolver, "firstName"); } @Test void parseWithHardcodedFallback() { PlaceholderResolver resolver = mockPlaceholderResolver(); - assertThat(this.parser.replacePlaceholders("${firstName:Steve}", resolver)) - .isEqualTo("Steve"); + assertThat(this.parser.replacePlaceholders("${firstName:Steve}", resolver)).isEqualTo("Steve"); verifyPlaceholderResolutions(resolver, "firstName:Steve", "firstName"); } @Test void parseWithNestedPlaceholderInKeyUsingFallback() { PlaceholderResolver resolver = mockPlaceholderResolver("firstName", "John"); - assertThat(this.parser.replacePlaceholders("${first${invalid:Name}}", resolver)) - .isEqualTo("John"); + assertThat(this.parser.replacePlaceholders("${first${invalid:Name}}", resolver)).isEqualTo("John"); verifyPlaceholderResolutions(resolver, "invalid:Name", "invalid", "firstName"); } @Test void parseWithFallbackUsingPlaceholder() { PlaceholderResolver resolver = mockPlaceholderResolver("firstName", "John"); - assertThat(this.parser.replacePlaceholders("${invalid:${firstName}}", resolver)) - .isEqualTo("John"); + assertThat(this.parser.replacePlaceholders("${invalid:${firstName}}", resolver)).isEqualTo("John"); verifyPlaceholderResolutions(resolver, "invalid", "firstName"); } } - @Nested // Tests with the use of the escape character + /** + * Tests that use the escape character. + */ + @Nested class EscapedTests { private final PlaceholderParser parser = new PlaceholderParser("${", "}", ":", '\\', true); - @ParameterizedTest(name = "{0} -> {1}") - @MethodSource("escapedInNestedPlaceholders") - void escapedSeparatorInNestedPlaceholder(String text, String expected) { - Properties properties = new Properties(); - properties.setProperty("app.environment", "qa"); - properties.setProperty("app.service", "protocol"); - properties.setProperty("protocol://host/qa/name", "protocol://example.com/qa/name"); - properties.setProperty("service/host/qa/name", "https://example.com/qa/name"); - properties.setProperty("service/host/qa/name:value", "https://example.com/qa/name-value"); - assertThat(this.parser.replacePlaceholders(text, properties::getProperty)).isEqualTo(expected); - } - - static Stream escapedInNestedPlaceholders() { - return Stream.of( - Arguments.of("${protocol\\://host/${app.environment}/name}", "protocol://example.com/qa/name"), - Arguments.of("${${app.service}\\://host/${app.environment}/name}", "protocol://example.com/qa/name"), - Arguments.of("${service/host/${app.environment}/name:\\value}", "https://example.com/qa/name"), - Arguments.of("${service/host/${name\\:value}/}", "${service/host/${name:value}/}")); - } - @ParameterizedTest(name = "{0} -> {1}") @MethodSource("escapedPlaceholders") void escapedPlaceholderIsNotReplaced(String text, String expected) { - PlaceholderResolver resolver = mockPlaceholderResolver( - "firstName", "John", "nested0", "first", "nested1", "Name", + Map properties = Map.of( + "firstName", "John", "${test}", "John", - "p1", "v1", "p2", "\\${p1:default}", "p3", "${p2}", + "p1", "v1", + "p2", "\\${p1:default}", + "p3", "${p2}", "p4", "adc${p0:\\${p1}}", "p5", "adc${\\${p0}:${p1}}", "p6", "adc${p0:def\\${p1}}", "p7", "adc\\${"); - assertThat(this.parser.replacePlaceholders(text, resolver)).isEqualTo(expected); + assertThat(this.parser.replacePlaceholders(text, properties::get)).isEqualTo(expected); } static Stream escapedPlaceholders() { @@ -324,18 +298,21 @@ class PlaceholderParserTests { Arguments.of("${p4}", "adc${p1}"), Arguments.of("${p5}", "adcv1"), Arguments.of("${p6}", "adcdef${p1}"), - Arguments.of("${p7}", "adc\\${")); - + Arguments.of("${p7}", "adc\\${"), + // Double backslash + Arguments.of("DOMAIN\\\\${user.name}", "DOMAIN\\${user.name}"), + // Triple backslash + Arguments.of("triple\\\\\\${backslash}", "triple\\\\${backslash}"), + // Multiple escaped placeholders + Arguments.of("start\\${prop1}middle\\${prop2}end", "start${prop1}middle${prop2}end") + ); } @ParameterizedTest(name = "{0} -> {1}") @MethodSource("escapedSeparators") void escapedSeparatorIsNotReplaced(String text, String expected) { - Properties properties = new Properties(); - properties.setProperty("first:Name", "John"); - properties.setProperty("nested0", "first"); - properties.setProperty("nested1", "Name"); - assertThat(this.parser.replacePlaceholders(text, properties::getProperty)).isEqualTo(expected); + Map properties = Map.of("first:Name", "John"); + assertThat(this.parser.replacePlaceholders(text, properties::get)).isEqualTo(expected); } static Stream escapedSeparators() { @@ -345,6 +322,26 @@ class PlaceholderParserTests { ); } + @ParameterizedTest(name = "{0} -> {1}") + @MethodSource("escapedSeparatorsInNestedPlaceholders") + void escapedSeparatorInNestedPlaceholderIsNotReplaced(String text, String expected) { + Map properties = Map.of( + "app.environment", "qa", + "app.service", "protocol", + "protocol://host/qa/name", "protocol://example.com/qa/name", + "service/host/qa/name", "https://example.com/qa/name", + "service/host/qa/name:value", "https://example.com/qa/name-value"); + assertThat(this.parser.replacePlaceholders(text, properties::get)).isEqualTo(expected); + } + + static Stream escapedSeparatorsInNestedPlaceholders() { + return Stream.of( + Arguments.of("${protocol\\://host/${app.environment}/name}", "protocol://example.com/qa/name"), + Arguments.of("${${app.service}\\://host/${app.environment}/name}", "protocol://example.com/qa/name"), + Arguments.of("${service/host/${app.environment}/name:\\value}", "https://example.com/qa/name"), + Arguments.of("${service/host/${name\\:value}/}", "${service/host/${name:value}/}")); + } + } @Nested @@ -354,34 +351,38 @@ class PlaceholderParserTests { @Test void textWithCircularReference() { - PlaceholderResolver resolver = mockPlaceholderResolver("pL", "${pR}", "pR", "${pL}"); - assertThatThrownBy(() -> this.parser.replacePlaceholders("${pL}", resolver)) + Map properties = Map.of( + "pL", "${pR}", + "pR", "${pL}"); + assertThatThrownBy(() -> this.parser.replacePlaceholders("${pL}", properties::get)) .isInstanceOf(PlaceholderResolutionException.class) .hasMessage("Circular placeholder reference 'pL' in value \"${pL}\" <-- \"${pR}\" <-- \"${pL}\""); } @Test void unresolvablePlaceholderIsReported() { - PlaceholderResolver resolver = mockPlaceholderResolver(); assertThatExceptionOfType(PlaceholderResolutionException.class) - .isThrownBy(() -> this.parser.replacePlaceholders("${bogus}", resolver)) - .withMessage("Could not resolve placeholder 'bogus' in value \"${bogus}\"") + .isThrownBy(() -> this.parser.replacePlaceholders("X${bogus}Z", placeholderName -> null)) + .withMessage("Could not resolve placeholder 'bogus' in value \"X${bogus}Z\"") .withNoCause(); } @Test void unresolvablePlaceholderInNestedPlaceholderIsReportedWithChain() { - PlaceholderResolver resolver = mockPlaceholderResolver("p1", "v1", "p2", "v2", + Map properties = Map.of( + "p1", "v1", + "p2", "v2", "p3", "${p1}:${p2}:${bogus}"); assertThatExceptionOfType(PlaceholderResolutionException.class) - .isThrownBy(() -> this.parser.replacePlaceholders("${p3}", resolver)) + .isThrownBy(() -> this.parser.replacePlaceholders("${p3}", properties::get)) .withMessage("Could not resolve placeholder 'bogus' in value \"${p1}:${p2}:${bogus}\" <-- \"${p3}\"") .withNoCause(); } } - PlaceholderResolver mockPlaceholderResolver(String... pairs) { + + private static PlaceholderResolver mockPlaceholderResolver(String... pairs) { if (pairs.length % 2 == 1) { throw new IllegalArgumentException("size must be even, it is a set of key=value pairs"); } @@ -394,7 +395,7 @@ class PlaceholderParserTests { return resolver; } - void verifyPlaceholderResolutions(PlaceholderResolver mock, String... placeholders) { + private static void verifyPlaceholderResolutions(PlaceholderResolver mock, String... placeholders) { InOrder ordered = inOrder(mock); for (String placeholder : placeholders) { ordered.verify(mock).resolvePlaceholder(placeholder);