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/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/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); /**