From e3aa5b6b1154345c231acc7950d74cd56d7420c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Fri, 29 Dec 2023 17:41:26 +0100 Subject: [PATCH] Use new implementation in PropertyPlaceholderHelper This commit removes the previous implementation in favor of the new PlaceholderParser. The only noticeable side effect is that the exception is no longer an IllegalArgumentException, but rather the dedicated PlaceholderResolutionException. See gh-9628 --- .../annotation-config/value-annotations.adoc | 4 +- .../config/PlaceholderConfigurerSupport.java | 19 ++- .../config/PropertyPlaceholderConfigurer.java | 5 +- .../PropertySourcesPlaceholderConfigurer.java | 3 +- .../PropertySourceAnnotationTests.java | 5 +- .../config/ContextNamespaceHandlerTests.java | 5 +- ...ertySourcesPlaceholderConfigurerTests.java | 7 +- spring-core/spring-core.gradle | 1 + .../core/env/AbstractEnvironment.java | 7 +- .../core/env/AbstractPropertyResolver.java | 20 ++- .../env/ConfigurablePropertyResolver.java | 10 +- .../io/support/PropertySourceProcessor.java | 7 +- .../util/PropertyPlaceholderHelper.java | 157 ++++-------------- .../util/SystemPropertyUtils.java | 11 +- .../PropertySourcesPropertyResolverTests.java | 13 +- .../core/env/StandardEnvironmentTests.java | 6 +- .../core/io/ResourceEditorTests.java | 4 +- .../support/PropertySourceProcessorTests.java | 12 +- .../ResourceArrayPropertyEditorTests.java | 7 +- .../util/PropertyPlaceholderHelperTests.java | 19 +-- .../util/SystemPropertyUtilsTests.java | 6 +- .../SendToMethodReturnValueHandler.java | 4 +- .../setup/StandaloneMockMvcBuilder.java | 4 +- .../web/util/ServletContextPropertyUtils.java | 8 +- 24 files changed, 156 insertions(+), 188 deletions(-) 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 967e04af4e7..4f5fd95cadb 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 91910a2f4a2..e357ec061c9 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 0fba4f79c22..d5fe3bf607d 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 1fd84402c12..1035e175e61 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 4ec292abd3d..df6a0723be3 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 21e6db8d941..532135b383a 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 33e27414c46..c8bb5d5ef70 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 a947f95c1ae..fedd203d553 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 e2cc3355c12..ec93302a7be 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 c3f29e106ad..890cfb08947 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 bb2f9bc79d1..362ca6c15e1 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 7559ce88641..02f0370b225 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/PropertyPlaceholderHelper.java b/spring-core/src/main/java/org/springframework/util/PropertyPlaceholderHelper.java index c35c0486025..00b2791b9cb 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); - } - - 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(); + return parseStringValue(value, placeholderResolver); } - 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 21b58ca18eb..8fa95ebff85 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 f51fc97e9a0..d442f6a0cb7 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 cd881a4bd3d..f3e05ec8bf9 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 128d7a44cad..1708749fee0 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 221736ce14c..0f3ea2ba62f 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 eabd4e46bf6..f747d0cbec0 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/PropertyPlaceholderHelperTests.java b/spring-core/src/test/java/org/springframework/util/PropertyPlaceholderHelperTests.java index 429df4d0a44..2b973300460 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 6761a94d668..b3170f7ee11 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 319297e18cb..88a65e9c22d 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 d47c3bb94ef..40b3d74a2df 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 6ce2c6e1a39..3c8f9cce2b9 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); /**