diff --git a/framework-docs/modules/ROOT/pages/appendix.adoc b/framework-docs/modules/ROOT/pages/appendix.adoc index 9a8c9048c05..6e7e5cecd0e 100644 --- a/framework-docs/modules/ROOT/pages/appendix.adoc +++ b/framework-docs/modules/ROOT/pages/appendix.adoc @@ -103,6 +103,14 @@ for details. {spring-framework-api}++/objenesis/SpringObjenesis.html#IGNORE_OBJENESIS_PROPERTY_NAME++[`SpringObjenesis`] for details. +| `spring.placeholder.escapeCharacter.default` +| The default escape character for property placeholder support. If not set, `'\'` will +be used. Can be set to a custom escape character or an empty string to disable support +for an escape character. The default escape character be explicitly overridden in +`PropertySourcesPlaceholderConfigurer` and subclasses of `AbstractPropertyResolver`. See +{spring-framework-api}++/core/env/AbstractPropertyResolver.html#DEFAULT_PLACEHOLDER_ESCAPE_CHARACTER_PROPERTY_NAME++[`AbstractPropertyResolver`] +for details. + | `spring.test.aot.processing.failOnError` | A boolean flag that controls whether errors encountered during AOT processing in the _Spring TestContext Framework_ should result in an exception that fails the overall process. 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 72e70005d0c..d91aaafb19b 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,11 @@ 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`, `setValueSeparator`, or -`setEscapeCharacter` to customize placeholders. +`setPlaceholderPrefix()`, `setPlaceholderSuffix()`, `setValueSeparator()`, or +`setEscapeCharacter()` to customize the placeholder syntax. In addition, the default +escape character can be changed or disabled globally by setting the +`spring.placeholder.escapeCharacter.default` property via a JVM system property (or via +the xref:appendix.adoc#appendix-spring-properties[`SpringProperties`] mechanism). NOTE: Spring Boot configures by default a `PropertySourcesPlaceholderConfigurer` bean that will get properties from `application.properties` and `application.yml` files. diff --git a/framework-docs/modules/ROOT/pages/core/beans/factory-extension.adoc b/framework-docs/modules/ROOT/pages/core/beans/factory-extension.adoc index 571eba4d686..56641fd847e 100644 --- a/framework-docs/modules/ROOT/pages/core/beans/factory-extension.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans/factory-extension.adoc @@ -314,7 +314,7 @@ Thus, marking it for lazy initialization will be ignored, and the [[beans-factory-placeholderconfigurer]] -=== Example: The Class Name Substitution `PropertySourcesPlaceholderConfigurer` +=== Example: Property Placeholder Substitution with `PropertySourcesPlaceholderConfigurer` You can use the `PropertySourcesPlaceholderConfigurer` to externalize property values from a bean definition in a separate file by using the standard Java `Properties` format. @@ -341,7 +341,7 @@ with placeholder values is defined: The example shows properties configured from an external `Properties` file. At runtime, a `PropertySourcesPlaceholderConfigurer` is applied to the metadata that replaces some -properties of the DataSource. The values to replace are specified as placeholders of the +properties of the `DataSource`. The values to replace are specified as placeholders of the form pass:q[`${property-name}`], which follows the Ant, log4j, and JSP EL style. The actual values come from another file in the standard Java `Properties` format: @@ -355,10 +355,13 @@ jdbc.password=root ---- Therefore, the `${jdbc.username}` string is replaced at runtime with the value, 'sa', and -the same applies for other placeholder values that match keys in the properties file. -The `PropertySourcesPlaceholderConfigurer` checks for placeholders in most properties and -attributes of a bean definition. Furthermore, you can customize the placeholder prefix, suffix, -default value separator, and escape character. +the same applies for other placeholder values that match keys in the properties file. The +`PropertySourcesPlaceholderConfigurer` checks for placeholders in most properties and +attributes of a bean definition. Furthermore, you can customize the placeholder prefix, +suffix, default value separator, and escape character. In addition, the default escape +character can be changed or disabled globally by setting the +`spring.placeholder.escapeCharacter.default` property via a JVM system property (or via +the xref:appendix.adoc#appendix-spring-properties[`SpringProperties`] mechanism). With the `context` namespace, you can configure property placeholders with a dedicated configuration element. You can provide one or more locations as a diff --git a/framework-docs/modules/ROOT/pages/languages/kotlin/spring-projects-in.adoc b/framework-docs/modules/ROOT/pages/languages/kotlin/spring-projects-in.adoc index a4425c6849f..2a8670de074 100644 --- a/framework-docs/modules/ROOT/pages/languages/kotlin/spring-projects-in.adoc +++ b/framework-docs/modules/ROOT/pages/languages/kotlin/spring-projects-in.adoc @@ -190,7 +190,7 @@ NOTE: If you use Spring Boot, you should probably use instead of `@Value` annotations. As an alternative, you can customize the property placeholder prefix by declaring the -following configuration bean: +following `PropertySourcesPlaceholderConfigurer` bean: [source,kotlin,indent=0] ---- @@ -200,8 +200,10 @@ following configuration bean: } ---- -You can customize existing code (such as Spring Boot actuators or `@LocalServerPort`) -that uses the `${...}` syntax, with configuration beans, as the following example shows: +You can support components (such as Spring Boot actuators or `@LocalServerPort`) that use +the standard `${...}` syntax alongside components that use the custom `%{...}` syntax by +declaring multiple `PropertySourcesPlaceholderConfigurer` beans, as the following example +shows: [source,kotlin,indent=0] ---- @@ -215,6 +217,9 @@ that uses the `${...}` syntax, with configuration beans, as the following exampl fun defaultPropertyConfigurer() = PropertySourcesPlaceholderConfigurer() ---- +In addition, the default escape character can be changed or disabled globally by setting +the `spring.placeholder.escapeCharacter.default` property via a JVM system property (or +via the xref:appendix.adoc#appendix-spring-properties[`SpringProperties`] mechanism). [[checked-exceptions]] 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 6d1fd92d97b..49f21f9044b 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 @@ -20,6 +20,7 @@ import org.springframework.beans.factory.BeanDefinitionStoreException; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.BeanNameAware; +import org.springframework.core.env.AbstractPropertyResolver; import org.springframework.lang.Nullable; import org.springframework.util.StringValueResolver; import org.springframework.util.SystemPropertyUtils; @@ -85,6 +86,7 @@ import org.springframework.util.SystemPropertyUtils; * * @author Chris Beams * @author Juergen Hoeller + * @author Sam Brannen * @since 3.1 * @see PropertyPlaceholderConfigurer * @see org.springframework.context.support.PropertySourcesPlaceholderConfigurer @@ -101,7 +103,11 @@ public abstract class PlaceholderConfigurerSupport extends PropertyResourceConfi /** Default value separator: {@value}. */ public static final String DEFAULT_VALUE_SEPARATOR = SystemPropertyUtils.VALUE_SEPARATOR; - /** Default escape character: {@code '\'}. */ + /** + * Default escape character: {@code '\'}. + * @since 6.2 + * @see AbstractPropertyResolver#getDefaultEscapeCharacter() + */ public static final Character DEFAULT_ESCAPE_CHARACTER = SystemPropertyUtils.ESCAPE_CHARACTER; @@ -115,9 +121,11 @@ public abstract class PlaceholderConfigurerSupport extends PropertyResourceConfi @Nullable protected String valueSeparator = DEFAULT_VALUE_SEPARATOR; - /** Defaults to {@link #DEFAULT_ESCAPE_CHARACTER}. */ + /** + * The default is determined by {@link AbstractPropertyResolver#getDefaultEscapeCharacter()}. + */ @Nullable - protected Character escapeCharacter = DEFAULT_ESCAPE_CHARACTER; + protected Character escapeCharacter = AbstractPropertyResolver.getDefaultEscapeCharacter(); protected boolean trimValues = false; @@ -164,6 +172,7 @@ public abstract class PlaceholderConfigurerSupport extends PropertyResourceConfi * {@linkplain #setPlaceholderPrefix(String) placeholder prefix} and the * {@linkplain #setValueSeparator(String) value separator}, or {@code null} * if no escaping should take place. + *

The default is determined by {@link AbstractPropertyResolver#getDefaultEscapeCharacter()}. * @since 6.2 */ public void setEscapeCharacter(@Nullable Character escapeCharacter) { 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 293ae6925c1..d78bb08406a 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 @@ -16,12 +16,15 @@ package org.springframework.context.support; +import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.Properties; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -37,7 +40,9 @@ import org.springframework.context.annotation.AnnotationConfigApplicationContext import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Scope; +import org.springframework.core.SpringProperties; import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.core.env.AbstractPropertyResolver; import org.springframework.core.env.EnumerablePropertySource; import org.springframework.core.env.MutablePropertySources; import org.springframework.core.env.PropertySource; @@ -47,12 +52,15 @@ 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 org.springframework.util.ReflectionUtils; 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.assertj.core.api.Assertions.assertThatIllegalStateException; import static org.springframework.beans.factory.support.BeanDefinitionBuilder.genericBeanDefinition; import static org.springframework.beans.factory.support.BeanDefinitionBuilder.rootBeanDefinition; +import static org.springframework.core.env.AbstractPropertyResolver.DEFAULT_PLACEHOLDER_ESCAPE_CHARACTER_PROPERTY_NAME; /** * Tests for {@link PropertySourcesPlaceholderConfigurer}. @@ -667,6 +675,108 @@ class PropertySourcesPlaceholderConfigurerTests { } + /** + * Tests that globally set the default escape character (or disable it) and + * rely on nested placeholder resolution. + */ + @Nested + class GlobalDefaultEscapeCharacterTests { + + private static final Field defaultEscapeCharacterField = + ReflectionUtils.findField(AbstractPropertyResolver.class, "defaultEscapeCharacter"); + + static { + ReflectionUtils.makeAccessible(defaultEscapeCharacterField); + } + + + @BeforeEach + void resetStateBeforeEachTest() { + resetState(); + } + + @AfterAll + static void resetState() { + ReflectionUtils.setField(defaultEscapeCharacterField, null, Character.MIN_VALUE); + setSpringProperty(null); + } + + + @Test // gh-34865 + void defaultEscapeCharacterSetToXyz() { + setSpringProperty("XYZ"); + + assertThatIllegalArgumentException() + .isThrownBy(PropertySourcesPlaceholderConfigurer::new) + .withMessage("Value [XYZ] for property [%s] must be a single character or an empty string", + DEFAULT_PLACEHOLDER_ESCAPE_CHARACTER_PROPERTY_NAME); + } + + @Test // gh-34865 + void defaultEscapeCharacterDisabled() { + setSpringProperty(""); + + MockEnvironment env = new MockEnvironment() + .withProperty("user.home", "admin") + .withProperty("my.property", "\\DOMAIN\\${user.home}"); + + DefaultListableBeanFactory bf = createBeanFactory(); + PropertySourcesPlaceholderConfigurer ppc = new PropertySourcesPlaceholderConfigurer(); + ppc.setEnvironment(env); + ppc.postProcessBeanFactory(bf); + + assertThat(bf.getBean(TestBean.class).getName()).isEqualTo("\\DOMAIN\\admin"); + } + + @Test // gh-34865 + void defaultEscapeCharacterSetToBackslash() { + setSpringProperty("\\"); + + MockEnvironment env = new MockEnvironment() + .withProperty("user.home", "admin") + .withProperty("my.property", "\\DOMAIN\\${user.home}"); + + DefaultListableBeanFactory bf = createBeanFactory(); + PropertySourcesPlaceholderConfigurer ppc = new PropertySourcesPlaceholderConfigurer(); + ppc.setEnvironment(env); + ppc.postProcessBeanFactory(bf); + + // \DOMAIN\${user.home} resolves to \DOMAIN${user.home} instead of \DOMAIN\admin + assertThat(bf.getBean(TestBean.class).getName()).isEqualTo("\\DOMAIN${user.home}"); + } + + @Test // gh-34865 + void defaultEscapeCharacterSetToTilde() { + setSpringProperty("~"); + + MockEnvironment env = new MockEnvironment() + .withProperty("user.home", "admin\\~${nested}") + .withProperty("my.property", "DOMAIN\\${user.home}\\~${enigma}"); + + DefaultListableBeanFactory bf = createBeanFactory(); + PropertySourcesPlaceholderConfigurer ppc = new PropertySourcesPlaceholderConfigurer(); + ppc.setEnvironment(env); + ppc.postProcessBeanFactory(bf); + + assertThat(bf.getBean(TestBean.class).getName()).isEqualTo("DOMAIN\\admin\\${nested}\\${enigma}"); + } + + private static void setSpringProperty(String value) { + SpringProperties.setProperty(DEFAULT_PLACEHOLDER_ESCAPE_CHARACTER_PROPERTY_NAME, value); + } + + private static DefaultListableBeanFactory createBeanFactory() { + BeanDefinition beanDefinition = genericBeanDefinition(TestBean.class) + .addPropertyValue("name", "${my.property}") + .getBeanDefinition(); + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + bf.registerBeanDefinition("testBean",beanDefinition); + return bf; + } + + } + + private static class OptionalTestBean { private Optional name; 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 72d655757e6..a13c40a74fb 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 @@ -23,6 +23,7 @@ import java.util.Set; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.springframework.core.SpringProperties; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.support.ConfigurableConversionService; import org.springframework.core.convert.support.DefaultConversionService; @@ -37,10 +38,52 @@ import org.springframework.util.SystemPropertyUtils; * * @author Chris Beams * @author Juergen Hoeller + * @author Sam Brannen * @since 3.1 */ public abstract class AbstractPropertyResolver implements ConfigurablePropertyResolver { + /** + * JVM system property used to change the default escape character + * for property placeholder support: {@value}. + *

To configure a custom escape character, supply a string containing a + * single character (other than {@link Character#MIN_VALUE}). For example, + * supplying the following JVM system property via the command line sets the + * default escape character to {@code '@'}. + *

-Dspring.placeholder.escapeCharacter.default=@
+ *

To disable escape character support, set the value to an empty string + * — for example, by supplying the following JVM system property via + * the command line. + *

-Dspring.placeholder.escapeCharacter.default=
+ *

If the property is not set, {@code '\'} will be used as the default + * escape character. + *

May alternatively be configured via a + * {@link org.springframework.core.SpringProperties spring.properties} file + * in the root of the classpath. + * @since 6.2.7 + * @see #getDefaultEscapeCharacter() + */ + public static final String DEFAULT_PLACEHOLDER_ESCAPE_CHARACTER_PROPERTY_NAME = + "spring.placeholder.escapeCharacter.default"; + + /** + * Since {@code null} is a valid value for {@link #defaultEscapeCharacter}, + * this constant provides a way to represent an undefined (or not yet set) + * value. Consequently, {@link #getDefaultEscapeCharacter()} prevents the use + * of {@link Character#MIN_VALUE} as the actual escape character. + * @since 6.2.7 + */ + static final Character UNDEFINED_ESCAPE_CHARACTER = Character.MIN_VALUE; + + + /** + * Cached value for the default escape character. + * @since 6.2.7 + */ + @Nullable + static volatile Character defaultEscapeCharacter = UNDEFINED_ESCAPE_CHARACTER; + + protected final Log logger = LogFactory.getLog(getClass()); @Nullable @@ -62,7 +105,7 @@ public abstract class AbstractPropertyResolver implements ConfigurablePropertyRe private String valueSeparator = SystemPropertyUtils.VALUE_SEPARATOR; @Nullable - private Character escapeCharacter = SystemPropertyUtils.ESCAPE_CHARACTER; + private Character escapeCharacter = getDefaultEscapeCharacter(); private final Set requiredProperties = new LinkedHashSet<>(); @@ -124,9 +167,8 @@ public abstract class AbstractPropertyResolver implements ConfigurablePropertyRe /** * {@inheritDoc} - *

The default is {@code '\'}. + *

The default is determined by {@link #getDefaultEscapeCharacter()}. * @since 6.2 - * @see SystemPropertyUtils#ESCAPE_CHARACTER */ @Override public void setEscapeCharacter(@Nullable Character escapeCharacter) { @@ -287,4 +329,60 @@ public abstract class AbstractPropertyResolver implements ConfigurablePropertyRe @Nullable protected abstract String getPropertyAsRawString(String key); + + /** + * Get the default {@linkplain #setEscapeCharacter(Character) escape character} + * to use when parsing strings for property placeholder resolution. + *

This method attempts to retrieve the default escape character configured + * via the {@value #DEFAULT_PLACEHOLDER_ESCAPE_CHARACTER_PROPERTY_NAME} JVM system + * property or Spring property. + *

Falls back to {@code '\'} if the property has not been set. + * @return the configured default escape character, {@code null} if escape character + * support has been disabled, or {@code '\'} if the property has not been set + * @throws IllegalArgumentException if the property is configured with an + * invalid value, such as {@link Character#MIN_VALUE} or a string containing + * more than one character + * @since 6.2.7 + * @see #DEFAULT_PLACEHOLDER_ESCAPE_CHARACTER_PROPERTY_NAME + * @see SystemPropertyUtils#ESCAPE_CHARACTER + * @see SpringProperties + */ + @Nullable + public static Character getDefaultEscapeCharacter() throws IllegalArgumentException { + Character escapeCharacter = defaultEscapeCharacter; + if (UNDEFINED_ESCAPE_CHARACTER.equals(escapeCharacter)) { + String value = SpringProperties.getProperty(DEFAULT_PLACEHOLDER_ESCAPE_CHARACTER_PROPERTY_NAME); + if (value != null) { + if (value.isEmpty()) { + // Disable escape character support by default. + escapeCharacter = null; + } + else if (value.length() == 1) { + try { + // Use custom default escape character. + escapeCharacter = value.charAt(0); + } + catch (Exception ex) { + throw new IllegalArgumentException("Failed to process value [%s] for property [%s]: %s" + .formatted(value, DEFAULT_PLACEHOLDER_ESCAPE_CHARACTER_PROPERTY_NAME, ex.getMessage()), ex); + } + Assert.isTrue(!escapeCharacter.equals(Character.MIN_VALUE), + () -> "Value for property [%s] must not be Character.MIN_VALUE" + .formatted(DEFAULT_PLACEHOLDER_ESCAPE_CHARACTER_PROPERTY_NAME)); + } + else { + throw new IllegalArgumentException( + "Value [%s] for property [%s] must be a single character or an empty string" + .formatted(value, DEFAULT_PLACEHOLDER_ESCAPE_CHARACTER_PROPERTY_NAME)); + } + } + else { + // Use standard default value for the escape character. + escapeCharacter = SystemPropertyUtils.ESCAPE_CHARACTER; + } + defaultEscapeCharacter = escapeCharacter; + } + return escapeCharacter; + } + } diff --git a/spring-core/src/test/java/org/springframework/core/env/AbstractPropertyResolverTests.java b/spring-core/src/test/java/org/springframework/core/env/AbstractPropertyResolverTests.java new file mode 100644 index 00000000000..ca536d83078 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/env/AbstractPropertyResolverTests.java @@ -0,0 +1,120 @@ +/* + * Copyright 2002-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.core.env; + +import java.util.stream.IntStream; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.core.SpringProperties; +import org.springframework.lang.Nullable; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.springframework.core.env.AbstractPropertyResolver.DEFAULT_PLACEHOLDER_ESCAPE_CHARACTER_PROPERTY_NAME; +import static org.springframework.core.env.AbstractPropertyResolver.UNDEFINED_ESCAPE_CHARACTER; + +/** + * Unit tests for {@link AbstractPropertyResolver}. + * + * @author Sam Brannen + * @since 6.2.7 + */ +class AbstractPropertyResolverTests { + + @BeforeEach + void resetStateBeforeEachTest() { + resetState(); + } + + @AfterAll + static void resetState() { + AbstractPropertyResolver.defaultEscapeCharacter = UNDEFINED_ESCAPE_CHARACTER; + setSpringProperty(null); + } + + + @Test + void getDefaultEscapeCharacterWithSpringPropertySetToCharacterMinValue() { + setSpringProperty("" + Character.MIN_VALUE); + + assertThatIllegalArgumentException() + .isThrownBy(AbstractPropertyResolver::getDefaultEscapeCharacter) + .withMessage("Value for property [%s] must not be Character.MIN_VALUE", + DEFAULT_PLACEHOLDER_ESCAPE_CHARACTER_PROPERTY_NAME); + + assertThat(AbstractPropertyResolver.defaultEscapeCharacter).isEqualTo(UNDEFINED_ESCAPE_CHARACTER); + } + + @Test + void getDefaultEscapeCharacterWithSpringPropertySetToXyz() { + setSpringProperty("XYZ"); + + assertThatIllegalArgumentException() + .isThrownBy(AbstractPropertyResolver::getDefaultEscapeCharacter) + .withMessage("Value [XYZ] for property [%s] must be a single character or an empty string", + DEFAULT_PLACEHOLDER_ESCAPE_CHARACTER_PROPERTY_NAME); + + assertThat(AbstractPropertyResolver.defaultEscapeCharacter).isEqualTo(UNDEFINED_ESCAPE_CHARACTER); + } + + @Test + void getDefaultEscapeCharacterWithSpringPropertySetToEmptyString() { + setSpringProperty(""); + assertEscapeCharacter(null); + } + + @Test + void getDefaultEscapeCharacterWithoutSpringPropertySet() { + assertEscapeCharacter('\\'); + } + + @Test + void getDefaultEscapeCharacterWithSpringPropertySetToBackslash() { + setSpringProperty("\\"); + assertEscapeCharacter('\\'); + } + + @Test + void getDefaultEscapeCharacterWithSpringPropertySetToTilde() { + setSpringProperty("~"); + assertEscapeCharacter('~'); + } + + @Test + void getDefaultEscapeCharacterFromMultipleThreads() { + setSpringProperty("~"); + + IntStream.range(1, 32).parallel().forEach(__ -> + assertThat(AbstractPropertyResolver.getDefaultEscapeCharacter()).isEqualTo('~')); + + assertThat(AbstractPropertyResolver.defaultEscapeCharacter).isEqualTo('~'); + } + + + private static void setSpringProperty(String value) { + SpringProperties.setProperty(DEFAULT_PLACEHOLDER_ESCAPE_CHARACTER_PROPERTY_NAME, value); + } + + private static void assertEscapeCharacter(@Nullable Character expected) { + assertThat(AbstractPropertyResolver.getDefaultEscapeCharacter()).isEqualTo(expected); + assertThat(AbstractPropertyResolver.defaultEscapeCharacter).isEqualTo(expected); + } + +}