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 3708ce7ce68..a0b89f3d916 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 @@ -24,12 +24,12 @@ import org.springframework.beans.factory.BeanInitializationException; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.config.PlaceholderConfigurerSupport; import org.springframework.context.EnvironmentAware; +import org.springframework.core.env.CompositePropertySource; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.ConfigurablePropertyResolver; import org.springframework.core.env.Environment; import org.springframework.core.env.MutablePropertySources; import org.springframework.core.env.PropertiesPropertySource; -import org.springframework.core.env.PropertyResolver; import org.springframework.core.env.PropertySource; import org.springframework.core.env.PropertySources; import org.springframework.core.env.PropertySourcesPropertyResolver; @@ -133,28 +133,24 @@ public class PropertySourcesPlaceholderConfigurer extends PlaceholderConfigurerS if (this.propertySources == null) { this.propertySources = new MutablePropertySources(); if (this.environment != null) { - PropertyResolver propertyResolver = this.environment; - // If the ignoreUnresolvablePlaceholders flag is set to true, we have to create a - // local PropertyResolver to enforce that setting, since the Environment is most - // likely not configured with ignoreUnresolvablePlaceholders set to true. - // See https://github.com/spring-projects/spring-framework/issues/27947 - if (this.ignoreUnresolvablePlaceholders && - (this.environment instanceof ConfigurableEnvironment configurableEnvironment)) { - PropertySourcesPropertyResolver resolver = - new PropertySourcesPropertyResolver(configurableEnvironment.getPropertySources()); - resolver.setIgnoreUnresolvableNestedPlaceholders(true); - propertyResolver = resolver; + PropertySource environmentPropertySource; + if (this.environment instanceof ConfigurableEnvironment configurableEnvironment) { + environmentPropertySource = new CompositePropertySource(ENVIRONMENT_PROPERTIES_PROPERTY_SOURCE_NAME, + configurableEnvironment.getPropertySources()); } - PropertyResolver propertyResolverToUse = propertyResolver; - this.propertySources.addLast( - new PropertySource<>(ENVIRONMENT_PROPERTIES_PROPERTY_SOURCE_NAME, this.environment) { - @Override - @Nullable - public String getProperty(String key) { - return propertyResolverToUse.getProperty(key); - } - } - ); + else { + // Fallback code path that should never apply in a regular scenario, since the + // Environment in the ApplicationContext should always be a ConfigurableEnvironment. + environmentPropertySource = + new PropertySource<>(ENVIRONMENT_PROPERTIES_PROPERTY_SOURCE_NAME, this.environment) { + @Override + @Nullable + public Object getProperty(String key) { + return super.source.getProperty(key); + } + }; + } + this.propertySources.addLast(environmentPropertySource); } try { PropertySource localPropertySource = 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 c8bb5d5ef70..9fee2b56e9b 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * 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. @@ -19,11 +19,13 @@ package org.springframework.context.support; import java.util.Optional; import java.util.Properties; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.BeanCreationException; import org.springframework.beans.factory.BeanDefinitionStoreException; import org.springframework.beans.factory.annotation.Value; +import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.beans.testfixture.beans.TestBean; import org.springframework.context.annotation.AnnotationConfigApplicationContext; @@ -432,6 +434,150 @@ class PropertySourcesPlaceholderConfigurerTests { } + /** + * Tests that use the escape character (or disable it) with nested placeholder + * resolution. + */ + @Nested + class EscapedNestedPlaceholdersTests { + + @Test // gh-34861 + void singleEscapeWithDefaultEscapeCharacter() { + 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-34861 + void singleEscapeWithCustomEscapeCharacter() { + 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); + // Set custom escape character. + ppc.setEscapeCharacter('~'); + ppc.postProcessBeanFactory(bf); + + assertThat(bf.getBean(TestBean.class).getName()).isEqualTo("DOMAIN\\admin\\${nested}\\${enigma}"); + } + + @Test // gh-34861 + void singleEscapeWithEscapeCharacterDisabled() { + MockEnvironment env = new MockEnvironment() + .withProperty("user.home", "admin\\") + .withProperty("my.property", "\\DOMAIN\\${user.home}"); + + DefaultListableBeanFactory bf = createBeanFactory(); + PropertySourcesPlaceholderConfigurer ppc = new PropertySourcesPlaceholderConfigurer(); + ppc.setEnvironment(env); + // Disable escape character. + ppc.setEscapeCharacter(null); + ppc.postProcessBeanFactory(bf); + + // \DOMAIN\${user.home} resolves to \DOMAIN\admin + assertThat(bf.getBean(TestBean.class).getName()).isEqualTo("\\DOMAIN\\admin\\"); + } + + @Test // gh-34861 + void tripleEscapeWithDefaultEscapeCharacter() { + MockEnvironment env = new MockEnvironment() + .withProperty("user.home", "admin\\\\\\") + .withProperty("my.property", "DOMAIN\\\\\\${user.home}#${user.home}"); + + DefaultListableBeanFactory bf = createBeanFactory(); + PropertySourcesPlaceholderConfigurer ppc = new PropertySourcesPlaceholderConfigurer(); + ppc.setEnvironment(env); + ppc.postProcessBeanFactory(bf); + + assertThat(bf.getBean(TestBean.class).getName()).isEqualTo("DOMAIN\\\\${user.home}#admin\\\\\\"); + } + + @Test // gh-34861 + void tripleEscapeWithCustomEscapeCharacter() { + MockEnvironment env = new MockEnvironment() + .withProperty("user.home", "admin\\~${enigma}") + .withProperty("my.property", "DOMAIN~~~${user.home}#${user.home}"); + + DefaultListableBeanFactory bf = createBeanFactory(); + PropertySourcesPlaceholderConfigurer ppc = new PropertySourcesPlaceholderConfigurer(); + ppc.setEnvironment(env); + // Set custom escape character. + ppc.setEscapeCharacter('~'); + ppc.postProcessBeanFactory(bf); + + assertThat(bf.getBean(TestBean.class).getName()).isEqualTo("DOMAIN~~${user.home}#admin\\${enigma}"); + } + + @Test // gh-34861 + void singleEscapeWithDefaultEscapeCharacterAndIgnoreUnresolvablePlaceholders() { + MockEnvironment env = new MockEnvironment() + .withProperty("user.home", "${enigma}") + .withProperty("my.property", "\\${DOMAIN}${user.home}"); + + DefaultListableBeanFactory bf = createBeanFactory(); + PropertySourcesPlaceholderConfigurer ppc = new PropertySourcesPlaceholderConfigurer(); + ppc.setEnvironment(env); + ppc.setIgnoreUnresolvablePlaceholders(true); + ppc.postProcessBeanFactory(bf); + + assertThat(bf.getBean(TestBean.class).getName()).isEqualTo("${DOMAIN}${enigma}"); + } + + @Test // gh-34861 + void singleEscapeWithCustomEscapeCharacterAndIgnoreUnresolvablePlaceholders() { + MockEnvironment env = new MockEnvironment() + .withProperty("user.home", "${enigma}") + .withProperty("my.property", "~${DOMAIN}\\${user.home}"); + + DefaultListableBeanFactory bf = createBeanFactory(); + PropertySourcesPlaceholderConfigurer ppc = new PropertySourcesPlaceholderConfigurer(); + ppc.setEnvironment(env); + // Set custom escape character. + ppc.setEscapeCharacter('~'); + ppc.setIgnoreUnresolvablePlaceholders(true); + ppc.postProcessBeanFactory(bf); + + assertThat(bf.getBean(TestBean.class).getName()).isEqualTo("${DOMAIN}\\${enigma}"); + } + + @Test // gh-34861 + void tripleEscapeWithDefaultEscapeCharacterAndIgnoreUnresolvablePlaceholders() { + MockEnvironment env = new MockEnvironment() + .withProperty("user.home", "${enigma}") + .withProperty("my.property", "X:\\\\\\${DOMAIN}${user.home}"); + + DefaultListableBeanFactory bf = createBeanFactory(); + PropertySourcesPlaceholderConfigurer ppc = new PropertySourcesPlaceholderConfigurer(); + ppc.setEnvironment(env); + ppc.setIgnoreUnresolvablePlaceholders(true); + ppc.postProcessBeanFactory(bf); + + assertThat(bf.getBean(TestBean.class).getName()).isEqualTo("X:\\\\${DOMAIN}${enigma}"); + } + + 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;