diff --git a/framework-docs/modules/ROOT/pages/core/beans/annotation-config/value-annotations.adoc b/framework-docs/modules/ROOT/pages/core/beans/annotation-config/value-annotations.adoc index 967e04af4e..4f5fd95cad 100644 --- a/framework-docs/modules/ROOT/pages/core/beans/annotation-config/value-annotations.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans/annotation-config/value-annotations.adoc @@ -101,8 +101,8 @@ NOTE: When configuring a `PropertySourcesPlaceholderConfigurer` using JavaConfig Using the above configuration ensures Spring initialization failure if any `${}` placeholder could not be resolved. It is also possible to use methods like -`setPlaceholderPrefix`, `setPlaceholderSuffix`, or `setValueSeparator` to customize -placeholders. +`setPlaceholderPrefix`, `setPlaceholderSuffix`, `setValueSeparator`, or +`setEscapeCharacter` to customize placeholders. NOTE: Spring Boot configures by default a `PropertySourcesPlaceholderConfigurer` bean that will get properties from `application.properties` and `application.yml` files. diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/PlaceholderConfigurerSupport.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/PlaceholderConfigurerSupport.java index 91910a2f4a..e357ec061c 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/PlaceholderConfigurerSupport.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/PlaceholderConfigurerSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -100,6 +100,8 @@ public abstract class PlaceholderConfigurerSupport extends PropertyResourceConfi /** Default value separator: {@value}. */ public static final String DEFAULT_VALUE_SEPARATOR = ":"; + /** Default escape character: {@value}. */ + public static final Character DEFAULT_ESCAPE_CHARACTER = '\\'; /** Defaults to {@value #DEFAULT_PLACEHOLDER_PREFIX}. */ protected String placeholderPrefix = DEFAULT_PLACEHOLDER_PREFIX; @@ -111,6 +113,10 @@ public abstract class PlaceholderConfigurerSupport extends PropertyResourceConfi @Nullable protected String valueSeparator = DEFAULT_VALUE_SEPARATOR; + /** Defaults to {@value #DEFAULT_ESCAPE_CHARACTER}. */ + @Nullable + protected Character escapeCharacter = DEFAULT_ESCAPE_CHARACTER; + protected boolean trimValues = false; @Nullable @@ -151,6 +157,17 @@ public abstract class PlaceholderConfigurerSupport extends PropertyResourceConfi this.valueSeparator = valueSeparator; } + /** + * Specify the escape character to use to ignore placeholder prefix + * or value separator, or {@code null} if no escaping should take + * place. + *

Default is {@value #DEFAULT_ESCAPE_CHARACTER}. + * @since 6.2 + */ + public void setEscapeCharacter(@Nullable Character escsEscapeCharacter) { + this.escapeCharacter = escsEscapeCharacter; + } + /** * Specify whether to trim resolved values before applying them, * removing superfluous whitespace from the beginning and end. diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/PropertyPlaceholderConfigurer.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/PropertyPlaceholderConfigurer.java index 0fba4f79c2..d5fe3bf607 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/PropertyPlaceholderConfigurer.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/PropertyPlaceholderConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -234,7 +234,8 @@ public class PropertyPlaceholderConfigurer extends PlaceholderConfigurerSupport public PlaceholderResolvingStringValueResolver(Properties props) { this.helper = new PropertyPlaceholderHelper( - placeholderPrefix, placeholderSuffix, valueSeparator, ignoreUnresolvablePlaceholders); + placeholderPrefix, placeholderSuffix, valueSeparator, + ignoreUnresolvablePlaceholders, escapeCharacter); this.resolver = new PropertyPlaceholderConfigurerResolver(props); } diff --git a/spring-context/src/main/java/org/springframework/context/support/PropertySourcesPlaceholderConfigurer.java b/spring-context/src/main/java/org/springframework/context/support/PropertySourcesPlaceholderConfigurer.java index 1fd84402c1..1035e175e6 100644 --- a/spring-context/src/main/java/org/springframework/context/support/PropertySourcesPlaceholderConfigurer.java +++ b/spring-context/src/main/java/org/springframework/context/support/PropertySourcesPlaceholderConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -193,6 +193,7 @@ public class PropertySourcesPlaceholderConfigurer extends PlaceholderConfigurerS propertyResolver.setPlaceholderPrefix(this.placeholderPrefix); propertyResolver.setPlaceholderSuffix(this.placeholderSuffix); propertyResolver.setValueSeparator(this.valueSeparator); + propertyResolver.setEscapeCharacter(this.escapeCharacter); StringValueResolver valueResolver = strVal -> { String resolved = (this.ignoreUnresolvablePlaceholders ? diff --git a/spring-context/src/test/java/org/springframework/context/annotation/PropertySourceAnnotationTests.java b/spring-context/src/test/java/org/springframework/context/annotation/PropertySourceAnnotationTests.java index 4ec292abd3..df6a0723be 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/PropertySourceAnnotationTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/PropertySourceAnnotationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -40,6 +40,7 @@ import org.springframework.core.env.MutablePropertySources; import org.springframework.core.io.support.EncodedResource; import org.springframework.core.io.support.PropertiesLoaderUtils; import org.springframework.core.io.support.PropertySourceFactory; +import org.springframework.util.PlaceholderResolutionException; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; @@ -132,7 +133,7 @@ class PropertySourceAnnotationTests { void withUnresolvablePlaceholder() { assertThatExceptionOfType(BeanDefinitionStoreException.class) .isThrownBy(() -> new AnnotationConfigApplicationContext(ConfigWithUnresolvablePlaceholder.class)) - .withCauseInstanceOf(IllegalArgumentException.class); + .withCauseInstanceOf(PlaceholderResolutionException.class); } @Test diff --git a/spring-context/src/test/java/org/springframework/context/config/ContextNamespaceHandlerTests.java b/spring-context/src/test/java/org/springframework/context/config/ContextNamespaceHandlerTests.java index 21e6db8d94..532135b383 100644 --- a/spring-context/src/test/java/org/springframework/context/config/ContextNamespaceHandlerTests.java +++ b/spring-context/src/test/java/org/springframework/context/config/ContextNamespaceHandlerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,6 +28,7 @@ import org.springframework.context.support.ClassPathXmlApplicationContext; import org.springframework.context.support.GenericXmlApplicationContext; import org.springframework.core.io.ClassPathResource; import org.springframework.mock.env.MockEnvironment; +import org.springframework.util.PlaceholderResolutionException; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; @@ -136,7 +137,7 @@ class ContextNamespaceHandlerTests { assertThatExceptionOfType(FatalBeanException.class).isThrownBy(() -> new ClassPathXmlApplicationContext("contextNamespaceHandlerTests-location-placeholder.xml", getClass())) .havingRootCause() - .isInstanceOf(IllegalArgumentException.class) + .isInstanceOf(PlaceholderResolutionException.class) .withMessage("Could not resolve placeholder 'foo' in value \"${foo}\""); } diff --git a/spring-context/src/test/java/org/springframework/context/support/PropertySourcesPlaceholderConfigurerTests.java b/spring-context/src/test/java/org/springframework/context/support/PropertySourcesPlaceholderConfigurerTests.java index 33e27414c4..c8bb5d5ef7 100644 --- a/spring-context/src/test/java/org/springframework/context/support/PropertySourcesPlaceholderConfigurerTests.java +++ b/spring-context/src/test/java/org/springframework/context/support/PropertySourcesPlaceholderConfigurerTests.java @@ -37,6 +37,7 @@ import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import org.springframework.core.testfixture.env.MockPropertySource; import org.springframework.mock.env.MockEnvironment; +import org.springframework.util.PlaceholderResolutionException; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; @@ -170,7 +171,7 @@ class PropertySourcesPlaceholderConfigurerTests { assertThatExceptionOfType(BeanDefinitionStoreException.class) .isThrownBy(() -> ppc.postProcessBeanFactory(bf)) .havingCause() - .isExactlyInstanceOf(IllegalArgumentException.class) + .isExactlyInstanceOf(PlaceholderResolutionException.class) .withMessage("Could not resolve placeholder 'my.name' in value \"${my.name}\""); } @@ -201,8 +202,8 @@ class PropertySourcesPlaceholderConfigurerTests { assertThatExceptionOfType(BeanCreationException.class) .isThrownBy(context::refresh) .havingCause() - .isExactlyInstanceOf(IllegalArgumentException.class) - .withMessage("Could not resolve placeholder 'enigma' in value \"${enigma}\""); + .isExactlyInstanceOf(PlaceholderResolutionException.class) + .withMessage("Could not resolve placeholder 'enigma' in value \"${enigma}\" <-- \"${my.key}\""); } @Test diff --git a/spring-core/spring-core.gradle b/spring-core/spring-core.gradle index a947f95c1a..fedd203d55 100644 --- a/spring-core/spring-core.gradle +++ b/spring-core/spring-core.gradle @@ -102,6 +102,7 @@ dependencies { testImplementation("jakarta.xml.bind:jakarta.xml.bind-api") testImplementation("org.jetbrains.kotlinx:kotlinx-serialization-json") testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor") + testImplementation("org.mockito:mockito-core") testImplementation("org.skyscreamer:jsonassert") testImplementation("org.xmlunit:xmlunit-assertj") testImplementation("org.xmlunit:xmlunit-matchers") diff --git a/spring-core/src/main/java/org/springframework/core/env/AbstractEnvironment.java b/spring-core/src/main/java/org/springframework/core/env/AbstractEnvironment.java index e2cc3355c1..ec93302a7b 100644 --- a/spring-core/src/main/java/org/springframework/core/env/AbstractEnvironment.java +++ b/spring-core/src/main/java/org/springframework/core/env/AbstractEnvironment.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -521,6 +521,11 @@ public abstract class AbstractEnvironment implements ConfigurableEnvironment { this.propertyResolver.setValueSeparator(valueSeparator); } + @Override + public void setEscapeCharacter(@Nullable Character escapeCharacter) { + this.propertyResolver.setEscapeCharacter(escapeCharacter); + } + @Override public void setIgnoreUnresolvableNestedPlaceholders(boolean ignoreUnresolvableNestedPlaceholders) { this.propertyResolver.setIgnoreUnresolvableNestedPlaceholders(ignoreUnresolvableNestedPlaceholders); diff --git a/spring-core/src/main/java/org/springframework/core/env/AbstractPropertyResolver.java b/spring-core/src/main/java/org/springframework/core/env/AbstractPropertyResolver.java index c3f29e106a..890cfb0894 100644 --- a/spring-core/src/main/java/org/springframework/core/env/AbstractPropertyResolver.java +++ b/spring-core/src/main/java/org/springframework/core/env/AbstractPropertyResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -61,6 +61,9 @@ public abstract class AbstractPropertyResolver implements ConfigurablePropertyRe @Nullable private String valueSeparator = SystemPropertyUtils.VALUE_SEPARATOR; + @Nullable + private Character escapeCharacter = SystemPropertyUtils.ESCAPE_CHARACTER; + private final Set requiredProperties = new LinkedHashSet<>(); @@ -121,6 +124,19 @@ public abstract class AbstractPropertyResolver implements ConfigurablePropertyRe this.valueSeparator = valueSeparator; } + /** + * Specify the escape character to use to ignore placeholder prefix + * or value separator, or {@code null} if no escaping should take + * place. + *

The default is "\". + * @since 6.2 + * @see org.springframework.util.SystemPropertyUtils#ESCAPE_CHARACTER + */ + @Override + public void setEscapeCharacter(@Nullable Character escapeCharacter) { + this.escapeCharacter = escapeCharacter; + } + /** * Set whether to throw an exception when encountering an unresolvable placeholder * nested within the value of a given property. A {@code false} value indicates strict @@ -232,7 +248,7 @@ public abstract class AbstractPropertyResolver implements ConfigurablePropertyRe private PropertyPlaceholderHelper createPlaceholderHelper(boolean ignoreUnresolvablePlaceholders) { return new PropertyPlaceholderHelper(this.placeholderPrefix, this.placeholderSuffix, - this.valueSeparator, ignoreUnresolvablePlaceholders); + this.valueSeparator, ignoreUnresolvablePlaceholders, this.escapeCharacter); } private String doResolvePlaceholders(String text, PropertyPlaceholderHelper helper) { diff --git a/spring-core/src/main/java/org/springframework/core/env/ConfigurablePropertyResolver.java b/spring-core/src/main/java/org/springframework/core/env/ConfigurablePropertyResolver.java index bb2f9bc79d..362ca6c15e 100644 --- a/spring-core/src/main/java/org/springframework/core/env/ConfigurablePropertyResolver.java +++ b/spring-core/src/main/java/org/springframework/core/env/ConfigurablePropertyResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -74,6 +74,14 @@ public interface ConfigurablePropertyResolver extends PropertyResolver { */ void setValueSeparator(@Nullable String valueSeparator); + /** + * Specify the escape character to use to ignore placeholder prefix + * or value separator, or {@code null} if no escaping should take + * place. + * @since 6.2 + */ + void setEscapeCharacter(@Nullable Character escapeCharacter); + /** * Set whether to throw an exception when encountering an unresolvable placeholder * nested within the value of a given property. A {@code false} value indicates strict diff --git a/spring-core/src/main/java/org/springframework/core/io/support/PropertySourceProcessor.java b/spring-core/src/main/java/org/springframework/core/io/support/PropertySourceProcessor.java index 7559ce8864..02f0370b22 100644 --- a/spring-core/src/main/java/org/springframework/core/io/support/PropertySourceProcessor.java +++ b/spring-core/src/main/java/org/springframework/core/io/support/PropertySourceProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,6 +36,7 @@ import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; import org.springframework.lang.Nullable; import org.springframework.util.Assert; +import org.springframework.util.PlaceholderResolutionException; import org.springframework.util.ReflectionUtils; /** @@ -93,8 +94,8 @@ public class PropertySourceProcessor { } } catch (RuntimeException | IOException ex) { - // Placeholders not resolvable (IllegalArgumentException) or resource not found when trying to open it - if (ignoreResourceNotFound && (ex instanceof IllegalArgumentException || isIgnorableException(ex) || + // Placeholders not resolvable or resource not found when trying to open it + if (ignoreResourceNotFound && (ex instanceof PlaceholderResolutionException || isIgnorableException(ex) || isIgnorableException(ex.getCause()))) { if (logger.isInfoEnabled()) { logger.info("Properties location [" + location + "] not resolvable: " + ex.getMessage()); diff --git a/spring-core/src/main/java/org/springframework/util/PlaceholderParser.java b/spring-core/src/main/java/org/springframework/util/PlaceholderParser.java new file mode 100644 index 0000000000..6836d10d12 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/PlaceholderParser.java @@ -0,0 +1,503 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +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; +import java.util.function.Function; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.lang.Nullable; +import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver; + +/** + * Parser for Strings that have placeholder values in them. In its simplest form, + * a placeholder takes the form of {@code ${name}}, where {@code name} is the key + * 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} + * property. + * + *

Placeholders can also have a more complex structure, and the resolution of + * a given key can involve the resolution of nested placeholders. Default values + * can also have placeholders. + * + *

For situations where the syntax of a valid placeholder match a String that + * 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 of default values or + * escaping are conditional on providing a non-null value for them. + * + *

This parser makes sure to resolves placeholders as lazily as possible. + * + * @author Stephane Nicoll + * @since 6.2 + */ +final class PlaceholderParser { + + private static final Log logger = LogFactory.getLog(PlaceholderParser.class); + + private static final Map wellKnownSimplePrefixes = Map.of( + "}", "{", "]", "[", ")", "("); + + private final String prefix; + + private final String suffix; + + private final String simplePrefix; + + @Nullable + private final String separator; + + private final boolean ignoreUnresolvablePlaceholders; + + @Nullable + private final Character escape; + + /** + * Create an instance using the specified input for the parser. + * + * @param prefix the prefix that denotes the start of a placeholder + * @param suffix the suffix that denotes the end of a placeholder + * @param ignoreUnresolvablePlaceholders whether unresolvable placeholders + * should be ignored ({@code true}) or cause an exception ({@code false}) + * @param separator the separating character between the placeholder + * variable and the associated default value, if any + * @param escape the character to use at the beginning of a placeholder + * to escape it and render it as is + */ + PlaceholderParser(String prefix, String suffix, boolean ignoreUnresolvablePlaceholders, + @Nullable String separator, @Nullable Character escape) { + this.prefix = prefix; + this.suffix = suffix; + String simplePrefixForSuffix = wellKnownSimplePrefixes.get(this.suffix); + if (simplePrefixForSuffix != null && this.prefix.endsWith(simplePrefixForSuffix)) { + this.simplePrefix = simplePrefixForSuffix; + } + else { + this.simplePrefix = this.prefix; + } + this.separator = separator; + this.ignoreUnresolvablePlaceholders = ignoreUnresolvablePlaceholders; + this.escape = escape; + } + + /** + * Replaces all placeholders of format {@code ${name}} with the value returned + * from the supplied {@link PlaceholderResolver}. + * @param value the value containing the placeholders to be replaced + * @param placeholderResolver the {@code PlaceholderResolver} to use for replacement + * @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); + 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<>(); + int startIndex = nextStartPrefix(value, 0); + if (startIndex == -1) { + Part part = inPlaceholder ? createSimplePlaceholderPart(value) + : new TextPart(value); + parts.add(part); + return parts; + } + int position = 0; + while (startIndex != -1) { + int endIndex = nextValidEndPrefix(value, startIndex); + 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 + 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 + addText(value, position, startIndex, parts); + String placeholder = value.substring(startIndex + this.prefix.length(), endIndex); + List placeholderParts = parse(placeholder, true); + parts.addAll(placeholderParts); + startIndex = nextStartPrefix(value, endIndex + this.suffix.length()); + position = endIndex + this.suffix.length(); + } + } + // Add rest of text if necessary + addText(value, position, value.length(), parts); + return inPlaceholder ? List.of(createNestedPlaceholderPart(value, parts)) : parts; + } + + private SimplePlaceholderPart createSimplePlaceholderPart(String text) { + String[] keyAndDefault = splitKeyAndDefault(text); + return (keyAndDefault != null) ? new SimplePlaceholderPart(text, keyAndDefault[0], keyAndDefault[1]) + : new SimplePlaceholderPart(text, text, null); + } + + private NestedPlaceholderPart createNestedPlaceholderPart(String text, List parts) { + if (this.separator == null) { + return new NestedPlaceholderPart(text, parts, null); + } + List keyParts = new ArrayList<>(); + List defaultParts = new ArrayList<>(); + for (int i = 0; i < parts.size(); i++) { + Part part = parts.get(i); + if (!(part instanceof TextPart)) { + keyParts.add(part); + } + else { + String candidate = part.text(); + String[] keyAndDefault = splitKeyAndDefault(candidate); + if (keyAndDefault != null) { + keyParts.add(new TextPart(keyAndDefault[0])); + if (keyAndDefault[1] != null) { + defaultParts.add(new TextPart(keyAndDefault[1])); + } + defaultParts.addAll(parts.subList(i + 1, parts.size())); + return new NestedPlaceholderPart(text, keyParts, defaultParts); + } + else { + keyParts.add(part); + } + } + } + // No separator found + return new NestedPlaceholderPart(text, parts, null); + } + + @Nullable + private String[] splitKeyAndDefault(String value) { + if (this.separator == null || !value.contains(this.separator)) { + return null; + } + int position = 0; + int index = value.indexOf(this.separator, position); + StringBuilder buffer = new StringBuilder(); + while (index != -1) { + if (isEscaped(value, index)) { + // Accumulate, without the escape character. + buffer.append(value, position, index - 1); + buffer.append(value, index, index + this.separator.length()); + position = index + this.separator.length(); + index = value.indexOf(this.separator, position); + } + else { + buffer.append(value, position, index); + String key = buffer.toString(); + String fallback = value.substring(index + this.separator.length()); + return new String[] { key, fallback }; + } + } + buffer.append(value, position, value.length()); + return new String[] { 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); + } + + private int nextValidEndPrefix(String value, int startIndex) { + int index = startIndex + this.prefix.length(); + int withinNestedPlaceholder = 0; + while (index < value.length()) { + if (StringUtils.substringMatch(value, index, this.suffix)) { + if (withinNestedPlaceholder > 0) { + withinNestedPlaceholder--; + index = index + this.suffix.length(); + } + else { + return index; + } + } + else if (StringUtils.substringMatch(value, index, this.simplePrefix)) { + withinNestedPlaceholder++; + index = index + this.simplePrefix.length(); + } + else { + index++; + } + } + return -1; + } + + private boolean isEscaped(String value, int index) { + return (this.escape != null && index > 0 && value.charAt(index - 1) == this.escape); + } + + /** + * Provide the necessary to handle and resolve underlying placeholders. + */ + static class PartResolutionContext implements PlaceholderResolver { + + private final String prefix; + + private final String suffix; + + private final boolean ignoreUnresolvablePlaceholders; + + private final Function> parser; + + private final PlaceholderResolver resolver; + + @Nullable + private Set visitedPlaceholders; + + PartResolutionContext(PlaceholderResolver resolver, String prefix, String suffix, + boolean ignoreUnresolvablePlaceholders, Function> parser) { + this.prefix = prefix; + this.suffix = suffix; + this.ignoreUnresolvablePlaceholders = ignoreUnresolvablePlaceholders; + this.parser = parser; + this.resolver = resolver; + } + + @Override + public String resolvePlaceholder(String placeholderName) { + String value = this.resolver.resolvePlaceholder(placeholderName); + if (value != null && logger.isTraceEnabled()) { + logger.trace("Resolved placeholder '" + placeholderName + "'"); + } + return value; + } + + public String handleUnresolvablePlaceholder(String key, String text) { + if (this.ignoreUnresolvablePlaceholders) { + return toPlaceholderText(key); + } + String originalValue = (!key.equals(text) ? toPlaceholderText(text) : null); + throw new PlaceholderResolutionException( + "Could not resolve placeholder '%s'".formatted(key), key, originalValue); + } + + private String toPlaceholderText(String text) { + return this.prefix + text + this.suffix; + } + + public List parse(String text) { + return this.parser.apply(text); + } + + public void flagPlaceholderAsVisited(String placeholder) { + if (this.visitedPlaceholders == null) { + this.visitedPlaceholders = new HashSet<>(4); + } + if (!this.visitedPlaceholders.add(placeholder)) { + throw new PlaceholderResolutionException( + "Circular placeholder reference '%s'".formatted(placeholder), placeholder, null); + } + } + + public void removePlaceholder(String placeholder) { + this.visitedPlaceholders.remove(placeholder); + } + + } + + + /** + * A part is a section of a String containing placeholders to replace. + */ + interface Part { + + /** + * Resolve this part using the specified {@link PartResolutionContext}. + * @param resolutionContext the context to use + * @return the resolved part + */ + String resolve(PartResolutionContext resolutionContext); + + /** + * Provide a textual representation of this part. + * @return the raw text that this part defines + */ + String text(); + + /** + * Return a String that appends the resolution of the specified parts. + * @param parts the parts to resolve + * @param resolutionContext the context to use for the resolution + * @return a concatenation of the supplied parts with placeholders replaced inline + */ + static String resolveAll(Iterable parts, PartResolutionContext resolutionContext) { + StringBuilder sb = new StringBuilder(); + for (Part part : parts) { + sb.append(part.resolve(resolutionContext)); + } + return sb.toString(); + } + + } + + /** + * 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 {@link Part} implementation that does not contain a valid placeholder. + * @param text the raw (and resolved) text + */ + record TextPart(String text) implements Part { + + @Override + public String resolve(PartResolutionContext resolutionContext) { + return this.text; + } + + } + + /** + * A {@link Part} implementation that represents a single placeholder with + * a hard-coded fallback. + * @param text the raw text + * @param key the key of the placeholder + * @param fallback the fallback to use, if any + */ + record SimplePlaceholderPart(String text, String key, @Nullable String fallback) implements Part { + + @Override + public String resolve(PartResolutionContext resolutionContext) { + String resolvedValue = resolveToText(resolutionContext, this.key); + if (resolvedValue != null) { + return resolvedValue; + } + else if (this.fallback != null) { + return this.fallback; + } + return resolutionContext.handleUnresolvablePlaceholder(this.key, this.text); + } + + @Nullable + private String resolveToText(PartResolutionContext resolutionContext, String text) { + String resolvedValue = resolutionContext.resolvePlaceholder(text); + if (resolvedValue != null) { + resolutionContext.flagPlaceholderAsVisited(text); + // 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(text); + return value; + } + // Not found + return null; + } + + private boolean isTextOnly(List parts) { + return parts.stream().allMatch(part -> part instanceof TextPart); + } + + private String toText(List parts) { + StringBuilder sb = new StringBuilder(); + parts.forEach(part -> sb.append(part.text())); + return sb.toString(); + } + + } + + /** + * A {@link Part} implementation that represents a single placeholder + * containing nested placeholders. + * @param text the raw text of the root placeholder + * @param keyParts the parts of the key + * @param defaultParts the parts of the fallback, if any + */ + record NestedPlaceholderPart(String text, List keyParts, @Nullable List defaultParts) implements Part { + + @Override + public String resolve(PartResolutionContext resolutionContext) { + String resolvedKey = Part.resolveAll(this.keyParts, resolutionContext); + String value = resolutionContext.resolvePlaceholder(resolvedKey); + if (value != null) { + return value; + } + else if (this.defaultParts != null) { + return Part.resolveAll(this.defaultParts, resolutionContext); + } + return resolutionContext.handleUnresolvablePlaceholder(resolvedKey, this.text); + } + + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/PlaceholderResolutionException.java b/spring-core/src/main/java/org/springframework/util/PlaceholderResolutionException.java new file mode 100644 index 0000000000..bc789a9bba --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/PlaceholderResolutionException.java @@ -0,0 +1,100 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.util; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import org.springframework.lang.Nullable; + +/** + * Thrown when the resolution of placeholder failed. This exception provides + * the placeholder as well as the hierarchy of values that led to the issue. + * + * @author Stephane Nicoll + * @since 6.2 + */ +@SuppressWarnings("serial") +public class PlaceholderResolutionException extends RuntimeException { + + private final String reason; + + private final String placeholder; + + private final List values; + + /** + * Create an exception using the specified reason for its message. + * @param reason the reason for the exception, should contain the placeholder + * @param placeholder the placeholder + * @param value the original expression that led to the issue if available + */ + PlaceholderResolutionException(String reason, String placeholder, @Nullable String value) { + this(reason, placeholder, (value != null ? List.of(value) : Collections.emptyList())); + } + + private PlaceholderResolutionException(String reason, String placeholder, List values) { + super(buildMessage(reason, values)); + this.reason = reason; + this.placeholder = placeholder; + this.values = values; + } + + private static String buildMessage(String reason, List values) { + StringBuilder sb = new StringBuilder(); + sb.append(reason); + if (!CollectionUtils.isEmpty(values)) { + String valuesChain = values.stream().map(value -> "\"" + value + "\"") + .collect(Collectors.joining(" <-- ")); + sb.append(" in value %s".formatted(valuesChain)); + } + return sb.toString(); + } + + /** + * Return a {@link PlaceholderResolutionException} that provides + * an additional parent value. + * @param value the parent value to add + * @return a new exception with the parent value added + */ + PlaceholderResolutionException withValue(String value) { + List allValues = new ArrayList<>(this.values); + allValues.add(value); + return new PlaceholderResolutionException(this.reason, this.placeholder, allValues); + } + + /** + * Return the placeholder that could not be resolved. + * @return the unresolvable placeholder + */ + public String getPlaceholder() { + return this.placeholder; + } + + /** + * Return a contextualized list of the resolution attempts that led to this + * exception, where the first element is the value that generated this + * exception. + * @return the stack of values that led to this exception + */ + public List getValues() { + return this.values; + } + +} 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 c35c048602..00b2791b9c 100644 --- a/spring-core/src/main/java/org/springframework/util/PropertyPlaceholderHelper.java +++ b/spring-core/src/main/java/org/springframework/util/PropertyPlaceholderHelper.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,14 +16,7 @@ package org.springframework.util; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; import java.util.Properties; -import java.util.Set; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; import org.springframework.lang.Nullable; @@ -37,31 +30,12 @@ import org.springframework.lang.Nullable; * * @author Juergen Hoeller * @author Rob Harrop + * @author Stephane Nicoll * @since 3.0 */ public class PropertyPlaceholderHelper { - private static final Log logger = LogFactory.getLog(PropertyPlaceholderHelper.class); - - private static final Map wellKnownSimplePrefixes = new HashMap<>(4); - - static { - wellKnownSimplePrefixes.put("}", "{"); - wellKnownSimplePrefixes.put("]", "["); - wellKnownSimplePrefixes.put(")", "("); - } - - - private final String placeholderPrefix; - - private final String placeholderSuffix; - - private final String simplePrefix; - - @Nullable - private final String valueSeparator; - - private final boolean ignoreUnresolvablePlaceholders; + private final PlaceholderParser parser; /** @@ -71,7 +45,7 @@ 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, null); } /** @@ -82,23 +56,35 @@ public class PropertyPlaceholderHelper { * and the associated default value, if any * @param ignoreUnresolvablePlaceholders indicates whether unresolvable placeholders should * be ignored ({@code true}) or cause an exception ({@code false}) + * @deprecated in favor of {@link PropertyPlaceholderHelper#PropertyPlaceholderHelper(String, String, String, boolean, Character)} */ + @Deprecated(since = "6.2", forRemoval = true) public PropertyPlaceholderHelper(String placeholderPrefix, String placeholderSuffix, @Nullable String valueSeparator, boolean ignoreUnresolvablePlaceholders) { + this(placeholderPrefix, placeholderSuffix, valueSeparator, ignoreUnresolvablePlaceholders, null); + } + + /** + * 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 to use to ignore placeholder prefix + * or value separator, if any + * @since 6.2 + */ + public PropertyPlaceholderHelper(String placeholderPrefix, String placeholderSuffix, + @Nullable String valueSeparator, boolean ignoreUnresolvablePlaceholders, + @Nullable Character escapeCharacter) { + Assert.notNull(placeholderPrefix, "'placeholderPrefix' must not be null"); Assert.notNull(placeholderSuffix, "'placeholderSuffix' must not be null"); - this.placeholderPrefix = placeholderPrefix; - this.placeholderSuffix = placeholderSuffix; - String simplePrefixForSuffix = wellKnownSimplePrefixes.get(this.placeholderSuffix); - if (simplePrefixForSuffix != null && this.placeholderPrefix.endsWith(simplePrefixForSuffix)) { - this.simplePrefix = simplePrefixForSuffix; - } - else { - this.simplePrefix = this.placeholderPrefix; - } - this.valueSeparator = valueSeparator; - this.ignoreUnresolvablePlaceholders = ignoreUnresolvablePlaceholders; + this.parser = new PlaceholderParser(placeholderPrefix, placeholderSuffix, + ignoreUnresolvablePlaceholders, valueSeparator, escapeCharacter); } @@ -123,94 +109,11 @@ public class PropertyPlaceholderHelper { */ public String replacePlaceholders(String value, PlaceholderResolver placeholderResolver) { Assert.notNull(value, "'value' must not be null"); - return parseStringValue(value, placeholderResolver, null); + return parseStringValue(value, placeholderResolver); } - protected String parseStringValue( - String value, PlaceholderResolver placeholderResolver, @Nullable Set visitedPlaceholders) { - - int startIndex = value.indexOf(this.placeholderPrefix); - if (startIndex == -1) { - return value; - } - - 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; - } - } - } - 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 + "'"); - } - 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 '" + - placeholder + "'" + " in value \"" + value + "\""); - } - visitedPlaceholders.remove(originalPlaceholder); - } - else { - startIndex = -1; - } - } - 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(); - } - else { - return index; - } - } - else if (StringUtils.substringMatch(buf, index, this.simplePrefix)) { - withinNestedPlaceholder++; - index = index + this.simplePrefix.length(); - } - else { - index++; - } - } - return -1; + protected String parseStringValue(String value, PlaceholderResolver placeholderResolver) { + return this.parser.replacePlaceholders(value, placeholderResolver); } diff --git a/spring-core/src/main/java/org/springframework/util/SystemPropertyUtils.java b/spring-core/src/main/java/org/springframework/util/SystemPropertyUtils.java index 21b58ca18e..8fa95ebff8 100644 --- a/spring-core/src/main/java/org/springframework/util/SystemPropertyUtils.java +++ b/spring-core/src/main/java/org/springframework/util/SystemPropertyUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -44,12 +44,17 @@ public abstract class SystemPropertyUtils { /** Value separator for system property placeholders: {@value}. */ public static final String VALUE_SEPARATOR = ":"; + /** Default escape character: {@value}. */ + public static final Character ESCAPE_CHARACTER = '\\'; + private static final PropertyPlaceholderHelper strictHelper = - new PropertyPlaceholderHelper(PLACEHOLDER_PREFIX, PLACEHOLDER_SUFFIX, VALUE_SEPARATOR, false); + new PropertyPlaceholderHelper(PLACEHOLDER_PREFIX, PLACEHOLDER_SUFFIX, VALUE_SEPARATOR, + false, ESCAPE_CHARACTER); private static final PropertyPlaceholderHelper nonStrictHelper = - new PropertyPlaceholderHelper(PLACEHOLDER_PREFIX, PLACEHOLDER_SUFFIX, VALUE_SEPARATOR, true); + new PropertyPlaceholderHelper(PLACEHOLDER_PREFIX, PLACEHOLDER_SUFFIX, VALUE_SEPARATOR, + true, ESCAPE_CHARACTER); /** 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 f51fc97e9a..d442f6a0cb 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,6 +25,7 @@ import org.junit.jupiter.api.Test; import org.springframework.core.convert.ConverterNotFoundException; import org.springframework.core.testfixture.env.MockPropertySource; +import org.springframework.util.PlaceholderResolutionException; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; @@ -227,7 +228,7 @@ class PropertySourcesPropertyResolverTests { MutablePropertySources propertySources = new MutablePropertySources(); propertySources.addFirst(new MockPropertySource().withProperty("key", "value")); PropertyResolver resolver = new PropertySourcesPropertyResolver(propertySources); - assertThatIllegalArgumentException().isThrownBy(() -> + assertThatExceptionOfType(PlaceholderResolutionException.class).isThrownBy(() -> resolver.resolveRequiredPlaceholders("Replace this ${key} plus ${unknown}")); } @@ -290,11 +291,11 @@ class PropertySourcesPropertyResolverTests { assertThat(pr.getProperty("p2")).isEqualTo("v2"); assertThat(pr.getProperty("p3")).isEqualTo("v1:v2"); assertThat(pr.getProperty("p4")).isEqualTo("v1:v2"); - assertThatIllegalArgumentException().isThrownBy(() -> + assertThatExceptionOfType(PlaceholderResolutionException.class).isThrownBy(() -> pr.getProperty("p5")) .withMessageContaining("Could not resolve placeholder 'bogus' in value \"${p1}:${p2}:${bogus}\""); assertThat(pr.getProperty("p6")).isEqualTo("v1:v2:def"); - assertThatIllegalArgumentException().isThrownBy(() -> + assertThatExceptionOfType(PlaceholderResolutionException.class).isThrownBy(() -> pr.getProperty("pL")) .withMessageContaining("Circular"); } @@ -315,7 +316,7 @@ class PropertySourcesPropertyResolverTests { // placeholders nested within the value of "p4" are unresolvable and cause an // exception by default - assertThatIllegalArgumentException().isThrownBy(() -> + assertThatExceptionOfType(PlaceholderResolutionException.class).isThrownBy(() -> pr.getProperty("p4")) .withMessageContaining("Could not resolve placeholder 'bogus' in value \"${p1}:${p2}:${bogus}\""); @@ -327,7 +328,7 @@ class PropertySourcesPropertyResolverTests { // resolve[Nested]Placeholders methods behave as usual regardless the value of // ignoreUnresolvableNestedPlaceholders assertThat(pr.resolvePlaceholders("${p1}:${p2}:${bogus}")).isEqualTo("v1:v2:${bogus}"); - assertThatIllegalArgumentException().isThrownBy(() -> + assertThatExceptionOfType(PlaceholderResolutionException.class).isThrownBy(() -> pr.resolveRequiredPlaceholders("${p1}:${p2}:${bogus}")) .withMessageContaining("Could not resolve placeholder 'bogus' in value \"${p1}:${p2}:${bogus}\""); } diff --git a/spring-core/src/test/java/org/springframework/core/env/StandardEnvironmentTests.java b/spring-core/src/test/java/org/springframework/core/env/StandardEnvironmentTests.java index cd881a4bd3..f3e05ec8bf 100644 --- a/spring-core/src/test/java/org/springframework/core/env/StandardEnvironmentTests.java +++ b/spring-core/src/test/java/org/springframework/core/env/StandardEnvironmentTests.java @@ -23,8 +23,10 @@ import org.junit.jupiter.api.Test; import org.springframework.core.SpringProperties; import org.springframework.core.testfixture.env.MockPropertySource; +import org.springframework.util.PlaceholderResolutionException; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.springframework.core.env.AbstractEnvironment.ACTIVE_PROFILES_PROPERTY_NAME; import static org.springframework.core.env.AbstractEnvironment.DEFAULT_PROFILES_PROPERTY_NAME; @@ -207,9 +209,9 @@ class StandardEnvironmentTests { void defaultProfileWithCircularPlaceholder() { try { System.setProperty(DEFAULT_PROFILES_PROPERTY_NAME, "${spring.profiles.default}"); - assertThatIllegalArgumentException() + assertThatExceptionOfType(PlaceholderResolutionException.class) .isThrownBy(environment::getDefaultProfiles) - .withMessage("Circular placeholder reference 'spring.profiles.default' in property definitions"); + .withMessageContaining("Circular placeholder reference 'spring.profiles.default'"); } finally { System.clearProperty(DEFAULT_PROFILES_PROPERTY_NAME); diff --git a/spring-core/src/test/java/org/springframework/core/io/ResourceEditorTests.java b/spring-core/src/test/java/org/springframework/core/io/ResourceEditorTests.java index 128d7a44ca..1708749fee 100644 --- a/spring-core/src/test/java/org/springframework/core/io/ResourceEditorTests.java +++ b/spring-core/src/test/java/org/springframework/core/io/ResourceEditorTests.java @@ -21,8 +21,10 @@ import java.beans.PropertyEditor; import org.junit.jupiter.api.Test; import org.springframework.core.env.StandardEnvironment; +import org.springframework.util.PlaceholderResolutionException; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; /** @@ -96,7 +98,7 @@ class ResourceEditorTests { PropertyEditor editor = new ResourceEditor(new DefaultResourceLoader(), new StandardEnvironment(), false); System.setProperty("test.prop", "foo"); try { - assertThatIllegalArgumentException().isThrownBy(() -> { + assertThatExceptionOfType(PlaceholderResolutionException.class).isThrownBy(() -> { editor.setAsText("${test.prop}-${bar}"); editor.getValue(); }); diff --git a/spring-core/src/test/java/org/springframework/core/io/support/PropertySourceProcessorTests.java b/spring-core/src/test/java/org/springframework/core/io/support/PropertySourceProcessorTests.java index 221736ce14..0f3ea2ba62 100644 --- a/spring-core/src/test/java/org/springframework/core/io/support/PropertySourceProcessorTests.java +++ b/spring-core/src/test/java/org/springframework/core/io/support/PropertySourceProcessorTests.java @@ -32,10 +32,12 @@ import org.springframework.core.env.StandardEnvironment; import org.springframework.core.io.DefaultResourceLoader; import org.springframework.core.io.ResourceLoader; import org.springframework.util.ClassUtils; +import org.springframework.util.PlaceholderResolutionException; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatNoException; +import static org.mockito.Mockito.mock; /** * Tests for {@link PropertySourceProcessor}. @@ -73,8 +75,8 @@ class PropertySourceProcessorTests { class FailOnErrorTests { @Test - void processorFailsOnIllegalArgumentException() { - assertProcessorFailsOnError(IllegalArgumentExceptionPropertySourceFactory.class, IllegalArgumentException.class); + void processorFailsOnPlaceholderResolutionException() { + assertProcessorFailsOnError(PlaceholderResolutionExceptionPropertySourceFactory.class, PlaceholderResolutionException.class); } @Test @@ -98,7 +100,7 @@ class PropertySourceProcessorTests { @Test void processorIgnoresIllegalArgumentException() { - assertProcessorIgnoresFailure(IllegalArgumentExceptionPropertySourceFactory.class); + assertProcessorIgnoresFailure(PlaceholderResolutionExceptionPropertySourceFactory.class); } @Test @@ -134,11 +136,11 @@ class PropertySourceProcessorTests { } - private static class IllegalArgumentExceptionPropertySourceFactory implements PropertySourceFactory { + private static class PlaceholderResolutionExceptionPropertySourceFactory implements PropertySourceFactory { @Override public PropertySource createPropertySource(String name, EncodedResource resource) { - throw new IllegalArgumentException("bogus"); + throw mock(PlaceholderResolutionException.class); } } diff --git a/spring-core/src/test/java/org/springframework/core/io/support/ResourceArrayPropertyEditorTests.java b/spring-core/src/test/java/org/springframework/core/io/support/ResourceArrayPropertyEditorTests.java index eabd4e46bf..f747d0cbec 100644 --- a/spring-core/src/test/java/org/springframework/core/io/support/ResourceArrayPropertyEditorTests.java +++ b/spring-core/src/test/java/org/springframework/core/io/support/ResourceArrayPropertyEditorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,9 +24,10 @@ import org.springframework.core.env.StandardEnvironment; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.FileUrlResource; import org.springframework.core.io.Resource; +import org.springframework.util.PlaceholderResolutionException; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; /** * Tests for {@link ResourceArrayPropertyEditor}. @@ -81,7 +82,7 @@ class ResourceArrayPropertyEditorTests { false); System.setProperty("test.prop", "foo"); try { - assertThatIllegalArgumentException().isThrownBy(() -> + assertThatExceptionOfType(PlaceholderResolutionException.class).isThrownBy(() -> editor.setAsText("${test.prop}-${bar}")); } finally { diff --git a/spring-core/src/test/java/org/springframework/util/PlaceholderParserTests.java b/spring-core/src/test/java/org/springframework/util/PlaceholderParserTests.java new file mode 100644 index 0000000000..110383eb68 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/util/PlaceholderParserTests.java @@ -0,0 +1,355 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.util; + +import java.util.Properties; +import java.util.stream.Stream; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +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; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.BDDMockito.given; +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 + */ +class PlaceholderParserTests { + + @Nested // Tests with only the basic placeholder feature enabled + class OnlyPlaceholderTests { + + private final PlaceholderParser parser = new PlaceholderParser("${", "}", true, null, null); + + @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); + } + + static Stream placeholders() { + return Stream.of( + Arguments.of("${firstName}", "John"), + Arguments.of("$${firstName}", "$John"), + Arguments.of("}${firstName}", "}John"), + Arguments.of("${firstName}$", "John$"), + Arguments.of("${firstName}}", "John}"), + Arguments.of("${firstName} ${firstName}", "John John"), + Arguments.of("First name: ${firstName}", "First name: John"), + Arguments.of("${firstName} is the first name", "John is the first name"), + Arguments.of("${first${nested1}}", "John"), + Arguments.of("${${nested0}${nested1}}", "John") + ); + } + + @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); + } + + static Stream nestedPlaceholders() { + return Stream.of( + Arguments.of("${p1}:${p2}", "v1:v2"), + Arguments.of("${p3}", "v1:v2"), + Arguments.of("${p4}", "v1:v2"), + Arguments.of("${p5}", "v1:v2:${bogus}"), + Arguments.of("${p0${p0}}", "${p0${p0}}") + ); + } + + @Test + void parseWithSinglePlaceholder() { + PlaceholderResolver resolver = mockPlaceholderResolver("firstName", "John"); + assertThat(this.parser.replacePlaceholders("${firstName}", resolver)) + .isEqualTo("John"); + verify(resolver).resolvePlaceholder("firstName"); + verifyNoMoreInteractions(resolver); + } + + @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); + } + + @Test + void parseWithMultiplePlaceholdersAndText() { + 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); + } + + @Test + void parseWithNestedPlaceholderInKey() { + 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"); + verifyPlaceholderResolutions(resolver, "nested0", "nested1", "firstName"); + } + + @Test + void placeholdersWithSeparatorAreHandledAsIs() { + PlaceholderResolver resolver = mockPlaceholderResolver("my:test", "value"); + assertThat(this.parser.replacePlaceholders("${my:test}", resolver)).isEqualTo("value"); + verifyPlaceholderResolutions(resolver, "my:test"); + } + + @Test + void placeholdersWithoutEscapeCharAreNotEscaped() { + PlaceholderResolver resolver = mockPlaceholderResolver("test", "value"); + assertThat(this.parser.replacePlaceholders("\\${test}", resolver)).isEqualTo("\\value"); + verifyPlaceholderResolutions(resolver, "test"); + } + + @Test + void textWithInvalidPlaceholderIsMerged() { + 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)); + } + + } + + @Nested // Tests with the use of a separator + class DefaultValueTests { + + private final PlaceholderParser parser = new PlaceholderParser("${", "}", true, ":", null); + + @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); + } + + static Stream placeholders() { + return Stream.of( + Arguments.of("${invalid:John}", "John"), + Arguments.of("${first${invalid:Name}}", "John"), + Arguments.of("${invalid:${firstName}}", "John"), + Arguments.of("${invalid:${${nested0}${nested1}}}", "John"), + Arguments.of("${invalid:$${firstName}}", "$John"), + Arguments.of("${invalid: }${firstName}", " John"), + Arguments.of("${invalid:}", ""), + Arguments.of("${:}", "") + ); + } + + @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); + } + + static Stream nestedPlaceholders() { + return Stream.of( + Arguments.of("${p6}", "v1:v2:def"), + Arguments.of("${invalid:${p1}:${p2}}", "v1:v2"), + Arguments.of("${invalid:${p3}}", "v1:v2"), + Arguments.of("${invalid:${p4}}", "v1:v2"), + Arguments.of("${invalid:${p5}}", "v1:v2:${bogus}"), + Arguments.of("${invalid:${p6}}", "v1:v2:def") + ); + } + + @Test + void parseWithHardcodedFallback() { + PlaceholderResolver resolver = mockPlaceholderResolver(); + assertThat(this.parser.replacePlaceholders("${firstName:Steve}", resolver)) + .isEqualTo("Steve"); + verifyPlaceholderResolutions(resolver, "firstName"); + } + + @Test + void parseWithNestedPlaceholderInKeyUsingFallback() { + PlaceholderResolver resolver = mockPlaceholderResolver("firstName", "John"); + assertThat(this.parser.replacePlaceholders("${first${invalid:Name}}", resolver)) + .isEqualTo("John"); + verifyPlaceholderResolutions(resolver, "invalid", "firstName"); + } + + @Test + void parseWithFallbackUsingPlaceholder() { + PlaceholderResolver resolver = mockPlaceholderResolver("firstName", "John"); + assertThat(this.parser.replacePlaceholders("${invalid:${firstName}}", resolver)) + .isEqualTo("John"); + verifyPlaceholderResolutions(resolver, "invalid", "firstName"); + } + + } + + @Nested // Tests with the use of the escape character + class EscapedTests { + + private final PlaceholderParser parser = new PlaceholderParser("${", "}", true, ":", '\\'); + + @ParameterizedTest(name = "{0} -> {1}") + @MethodSource("escapedPlaceholders") + void escapedPlaceholderIsNotReplaced(String text, String expected) { + PlaceholderResolver resolver = mockPlaceholderResolver( + "firstName", "John", "nested0", "first", "nested1", "Name", + "${test}", "John", + "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); + } + + static Stream escapedPlaceholders() { + return Stream.of( + Arguments.of("\\${firstName}", "${firstName}"), + Arguments.of("First name: \\${firstName}", "First name: ${firstName}"), + Arguments.of("$\\${firstName}", "$${firstName}"), + Arguments.of("\\}${firstName}", "\\}John"), + Arguments.of("${\\${test}}", "John"), + Arguments.of("${p2}", "${p1:default}"), + Arguments.of("${p3}", "${p1:default}"), + Arguments.of("${p4}", "adc${p1}"), + Arguments.of("${p5}", "adcv1"), + Arguments.of("${p6}", "adcdef${p1}"), + Arguments.of("${p7}", "adc\\${")); + + } + + @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); + } + + static Stream escapedSeparators() { + return Stream.of( + Arguments.of("${first\\:Name}", "John"), + Arguments.of("${last\\:Name}", "${last:Name}") + ); + } + + } + + @Nested + class ExceptionTests { + + private final PlaceholderParser parser = new PlaceholderParser("${", "}", false, ":", null); + + @Test + void textWithCircularReference() { + PlaceholderResolver resolver = mockPlaceholderResolver("pL", "${pR}", "pR", "${pL}"); + assertThatThrownBy(() -> this.parser.replacePlaceholders("${pL}", resolver)) + .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}\"") + .withNoCause(); + } + + @Test + void unresolvablePlaceholderInNestedPlaceholderIsReportedWithChain() { + PlaceholderResolver resolver = mockPlaceholderResolver("p1", "v1", "p2", "v2", + "p3", "${p1}:${p2}:${bogus}"); + assertThatExceptionOfType(PlaceholderResolutionException.class) + .isThrownBy(() -> this.parser.replacePlaceholders("${p3}", resolver)) + .withMessage("Could not resolve placeholder 'bogus' in value \"${p1}:${p2}:${bogus}\" <-- \"${p3}\"") + .withNoCause(); + } + + } + + PlaceholderResolver mockPlaceholderResolver(String... pairs) { + if (pairs.length % 2 == 1) { + throw new IllegalArgumentException("size must be even, it is a set of key=value pairs"); + } + PlaceholderResolver resolver = mock(PlaceholderResolver.class); + for (int i = 0; i < pairs.length; i += 2) { + String key = pairs[i]; + String value = pairs[i + 1]; + given(resolver.resolvePlaceholder(key)).willReturn(value); + } + return resolver; + } + + void verifyPlaceholderResolutions(PlaceholderResolver mock, String... placeholders) { + for (String placeholder : placeholders) { + verify(mock).resolvePlaceholder(placeholder); + } + verifyNoMoreInteractions(mock); + } + +} 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 429df4d0a4..2b97330046 100644 --- a/spring-core/src/test/java/org/springframework/util/PropertyPlaceholderHelperTests.java +++ b/spring-core/src/test/java/org/springframework/util/PropertyPlaceholderHelperTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,7 +19,6 @@ package org.springframework.util; import java.util.Properties; import java.util.stream.Stream; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -29,11 +28,11 @@ import org.junit.jupiter.params.provider.MethodSource; import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; /** * Tests for {@link PropertyPlaceholderHelper}. @@ -116,16 +115,15 @@ class PropertyPlaceholderHelperTests { Properties props = new Properties(); props.setProperty("foo", "bar"); - PropertyPlaceholderHelper helper = new PropertyPlaceholderHelper("${", "}", null, false); - assertThatIllegalArgumentException().isThrownBy(() -> + PropertyPlaceholderHelper helper = new PropertyPlaceholderHelper("${", "}", null, false, null); + assertThatExceptionOfType(PlaceholderResolutionException.class).isThrownBy(() -> helper.replacePlaceholders(text, props)); - } @Nested class DefaultValueTests { - private final PropertyPlaceholderHelper helper = new PropertyPlaceholderHelper("${", "}", ":", true); + private final PropertyPlaceholderHelper helper = new PropertyPlaceholderHelper("${", "}", ":", true, null); @ParameterizedTest(name = "{0} -> {1}") @MethodSource("defaultValues") @@ -137,12 +135,11 @@ class PropertyPlaceholderHelperTests { } @Test - @Disabled("gh-26268") void defaultValueIsNotEvaluatedEarly() { PlaceholderResolver resolver = mockPlaceholderResolver("one", "1"); - assertThat(this.helper.replacePlaceholders("This is ${one:or${two}}",resolver)).isEqualTo("This is 1"); + assertThat(this.helper.replacePlaceholders("This is ${one:or${two}}", resolver)).isEqualTo("This is 1"); verify(resolver).resolvePlaceholder("one"); - verifyNoMoreInteractions(resolver); + verify(resolver, never()).resolvePlaceholder("two"); } static Stream defaultValues() { diff --git a/spring-core/src/test/java/org/springframework/util/SystemPropertyUtilsTests.java b/spring-core/src/test/java/org/springframework/util/SystemPropertyUtilsTests.java index 6761a94d66..b3170f7ee1 100644 --- a/spring-core/src/test/java/org/springframework/util/SystemPropertyUtilsTests.java +++ b/spring-core/src/test/java/org/springframework/util/SystemPropertyUtilsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,7 +21,7 @@ import java.util.Map; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; /** * @author Rob Harrop @@ -97,7 +97,7 @@ class SystemPropertyUtilsTests { @Test void replaceWithNoDefault() { - assertThatIllegalArgumentException().isThrownBy(() -> + assertThatExceptionOfType(PlaceholderResolutionException.class).isThrownBy(() -> SystemPropertyUtils.resolvePlaceholders("${test.prop}")); } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/support/SendToMethodReturnValueHandler.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/support/SendToMethodReturnValueHandler.java index 319297e18c..88a65e9c22 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/support/SendToMethodReturnValueHandler.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/support/SendToMethodReturnValueHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -68,7 +68,7 @@ public class SendToMethodReturnValueHandler implements HandlerMethodReturnValueH private String defaultUserDestinationPrefix = "/queue"; - private final PropertyPlaceholderHelper placeholderHelper = new PropertyPlaceholderHelper("{", "}", null, false); + private final PropertyPlaceholderHelper placeholderHelper = new PropertyPlaceholderHelper("{", "}", null, false, null); @Nullable private MessageHeaderInitializer headerInitializer; diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/setup/StandaloneMockMvcBuilder.java b/spring-test/src/main/java/org/springframework/test/web/servlet/setup/StandaloneMockMvcBuilder.java index d47c3bb94e..40b3d74a2d 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/setup/StandaloneMockMvcBuilder.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/setup/StandaloneMockMvcBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -586,7 +586,7 @@ public class StandaloneMockMvcBuilder extends AbstractMockMvcBuilder values) { - this.helper = new PropertyPlaceholderHelper("${", "}", ":", false); + this.helper = new PropertyPlaceholderHelper("${", "}", ":", false, null); this.resolver = values::get; } diff --git a/spring-web/src/main/java/org/springframework/web/util/ServletContextPropertyUtils.java b/spring-web/src/main/java/org/springframework/web/util/ServletContextPropertyUtils.java index 6ce2c6e1a3..3c8f9cce2b 100644 --- a/spring-web/src/main/java/org/springframework/web/util/ServletContextPropertyUtils.java +++ b/spring-web/src/main/java/org/springframework/web/util/ServletContextPropertyUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -39,11 +39,13 @@ public abstract class ServletContextPropertyUtils { private static final PropertyPlaceholderHelper strictHelper = new PropertyPlaceholderHelper(SystemPropertyUtils.PLACEHOLDER_PREFIX, - SystemPropertyUtils.PLACEHOLDER_SUFFIX, SystemPropertyUtils.VALUE_SEPARATOR, false); + SystemPropertyUtils.PLACEHOLDER_SUFFIX, SystemPropertyUtils.VALUE_SEPARATOR, + false, SystemPropertyUtils.ESCAPE_CHARACTER); private static final PropertyPlaceholderHelper nonStrictHelper = new PropertyPlaceholderHelper(SystemPropertyUtils.PLACEHOLDER_PREFIX, - SystemPropertyUtils.PLACEHOLDER_SUFFIX, SystemPropertyUtils.VALUE_SEPARATOR, true); + SystemPropertyUtils.PLACEHOLDER_SUFFIX, SystemPropertyUtils.VALUE_SEPARATOR, + true, SystemPropertyUtils.ESCAPE_CHARACTER); /**