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