Add support for escaping prefix and separator in property placeholders
Closes gh-9628 Closes gh-26268
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
* <p>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.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 ?
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}\"");
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<String> 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.
|
||||
* <p>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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -0,0 +1,503 @@
|
||||
/*
|
||||
* 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.
|
||||
* 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.util;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver;
|
||||
|
||||
/**
|
||||
* Parser for Strings that have placeholder values in them. In its simplest form,
|
||||
* a placeholder takes the form of {@code ${name}}, where {@code name} is the key
|
||||
* that can be resolved using a {@link PlaceholderResolver PlaceholderResolver},
|
||||
* <code>${</code> the prefix, and <code>}</code> the suffix.
|
||||
*
|
||||
* <p>A placeholder can also have a default value if its key does not represent a
|
||||
* known property. The default value is separated from the key using a
|
||||
* {@code separator}. For instance {@code ${name:John}} resolves to {@code John} if
|
||||
* the placeholder resolver does not provide a value for the {@code name}
|
||||
* property.
|
||||
*
|
||||
* <p>Placeholders can also have a more complex structure, and the resolution of
|
||||
* a given key can involve the resolution of nested placeholders. Default values
|
||||
* can also have placeholders.
|
||||
*
|
||||
* <p>For situations where the syntax of a valid placeholder match a String that
|
||||
* must be rendered as is, the placeholder can be escaped using an {@code escape}
|
||||
* character. For instance {@code \${name}} resolves as {@code ${name}}.
|
||||
*
|
||||
* <p>The prefix, suffix, separator, and escape characters are configurable. Only
|
||||
* the prefix and suffix are mandatory and the support of default values or
|
||||
* escaping are conditional on providing a non-null value for them.
|
||||
*
|
||||
* <p>This parser makes sure to resolves placeholders as lazily as possible.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
* @since 6.2
|
||||
*/
|
||||
final class PlaceholderParser {
|
||||
|
||||
private static final Log logger = LogFactory.getLog(PlaceholderParser.class);
|
||||
|
||||
private static final Map<String, String> wellKnownSimplePrefixes = Map.of(
|
||||
"}", "{", "]", "[", ")", "(");
|
||||
|
||||
private final String prefix;
|
||||
|
||||
private final String suffix;
|
||||
|
||||
private final String simplePrefix;
|
||||
|
||||
@Nullable
|
||||
private final String separator;
|
||||
|
||||
private final boolean ignoreUnresolvablePlaceholders;
|
||||
|
||||
@Nullable
|
||||
private final Character escape;
|
||||
|
||||
/**
|
||||
* Create an instance using the specified input for the parser.
|
||||
*
|
||||
* @param prefix the prefix that denotes the start of a placeholder
|
||||
* @param suffix the suffix that denotes the end of a placeholder
|
||||
* @param ignoreUnresolvablePlaceholders whether unresolvable placeholders
|
||||
* should be ignored ({@code true}) or cause an exception ({@code false})
|
||||
* @param separator the separating character between the placeholder
|
||||
* variable and the associated default value, if any
|
||||
* @param escape the character to use at the beginning of a placeholder
|
||||
* to escape it and render it as is
|
||||
*/
|
||||
PlaceholderParser(String prefix, String suffix, boolean ignoreUnresolvablePlaceholders,
|
||||
@Nullable String separator, @Nullable Character escape) {
|
||||
this.prefix = prefix;
|
||||
this.suffix = suffix;
|
||||
String simplePrefixForSuffix = wellKnownSimplePrefixes.get(this.suffix);
|
||||
if (simplePrefixForSuffix != null && this.prefix.endsWith(simplePrefixForSuffix)) {
|
||||
this.simplePrefix = simplePrefixForSuffix;
|
||||
}
|
||||
else {
|
||||
this.simplePrefix = this.prefix;
|
||||
}
|
||||
this.separator = separator;
|
||||
this.ignoreUnresolvablePlaceholders = ignoreUnresolvablePlaceholders;
|
||||
this.escape = escape;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces all placeholders of format {@code ${name}} with the value returned
|
||||
* from the supplied {@link PlaceholderResolver}.
|
||||
* @param value the value containing the placeholders to be replaced
|
||||
* @param placeholderResolver the {@code PlaceholderResolver} to use for replacement
|
||||
* @return the supplied value with placeholders replaced inline
|
||||
*/
|
||||
public String replacePlaceholders(String value, PlaceholderResolver placeholderResolver) {
|
||||
Assert.notNull(value, "'value' must not be null");
|
||||
ParsedValue parsedValue = parse(value);
|
||||
PartResolutionContext resolutionContext = new PartResolutionContext(placeholderResolver,
|
||||
this.prefix, this.suffix, this.ignoreUnresolvablePlaceholders,
|
||||
candidate -> parse(candidate, false));
|
||||
return parsedValue.resolve(resolutionContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the specified value.
|
||||
* @param value the value containing the placeholders to be replaced
|
||||
* @return the different parts that have been identified
|
||||
*/
|
||||
ParsedValue parse(String value) {
|
||||
List<Part> parts = parse(value, false);
|
||||
return new ParsedValue(value, parts);
|
||||
}
|
||||
|
||||
private List<Part> parse(String value, boolean inPlaceholder) {
|
||||
LinkedList<Part> parts = new LinkedList<>();
|
||||
int startIndex = nextStartPrefix(value, 0);
|
||||
if (startIndex == -1) {
|
||||
Part part = inPlaceholder ? createSimplePlaceholderPart(value)
|
||||
: new TextPart(value);
|
||||
parts.add(part);
|
||||
return parts;
|
||||
}
|
||||
int position = 0;
|
||||
while (startIndex != -1) {
|
||||
int endIndex = nextValidEndPrefix(value, startIndex);
|
||||
if (endIndex == -1) { // Not a valid placeholder, consume the prefix and continue
|
||||
addText(value, position, startIndex + this.prefix.length(), parts);
|
||||
position = startIndex + this.prefix.length();
|
||||
startIndex = nextStartPrefix(value, position);
|
||||
}
|
||||
else if (isEscaped(value, startIndex)) { // Not a valid index, accumulate and skip the escape character
|
||||
addText(value, position, startIndex - 1, parts);
|
||||
addText(value, startIndex, startIndex + this.prefix.length(), parts);
|
||||
position = startIndex + this.prefix.length();
|
||||
startIndex = nextStartPrefix(value, position);
|
||||
}
|
||||
else { // Found valid placeholder, recursive parsing
|
||||
addText(value, position, startIndex, parts);
|
||||
String placeholder = value.substring(startIndex + this.prefix.length(), endIndex);
|
||||
List<Part> placeholderParts = parse(placeholder, true);
|
||||
parts.addAll(placeholderParts);
|
||||
startIndex = nextStartPrefix(value, endIndex + this.suffix.length());
|
||||
position = endIndex + this.suffix.length();
|
||||
}
|
||||
}
|
||||
// Add rest of text if necessary
|
||||
addText(value, position, value.length(), parts);
|
||||
return inPlaceholder ? List.of(createNestedPlaceholderPart(value, parts)) : parts;
|
||||
}
|
||||
|
||||
private SimplePlaceholderPart createSimplePlaceholderPart(String text) {
|
||||
String[] keyAndDefault = splitKeyAndDefault(text);
|
||||
return (keyAndDefault != null) ? new SimplePlaceholderPart(text, keyAndDefault[0], keyAndDefault[1])
|
||||
: new SimplePlaceholderPart(text, text, null);
|
||||
}
|
||||
|
||||
private NestedPlaceholderPart createNestedPlaceholderPart(String text, List<Part> parts) {
|
||||
if (this.separator == null) {
|
||||
return new NestedPlaceholderPart(text, parts, null);
|
||||
}
|
||||
List<Part> keyParts = new ArrayList<>();
|
||||
List<Part> defaultParts = new ArrayList<>();
|
||||
for (int i = 0; i < parts.size(); i++) {
|
||||
Part part = parts.get(i);
|
||||
if (!(part instanceof TextPart)) {
|
||||
keyParts.add(part);
|
||||
}
|
||||
else {
|
||||
String candidate = part.text();
|
||||
String[] keyAndDefault = splitKeyAndDefault(candidate);
|
||||
if (keyAndDefault != null) {
|
||||
keyParts.add(new TextPart(keyAndDefault[0]));
|
||||
if (keyAndDefault[1] != null) {
|
||||
defaultParts.add(new TextPart(keyAndDefault[1]));
|
||||
}
|
||||
defaultParts.addAll(parts.subList(i + 1, parts.size()));
|
||||
return new NestedPlaceholderPart(text, keyParts, defaultParts);
|
||||
}
|
||||
else {
|
||||
keyParts.add(part);
|
||||
}
|
||||
}
|
||||
}
|
||||
// No separator found
|
||||
return new NestedPlaceholderPart(text, parts, null);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private String[] splitKeyAndDefault(String value) {
|
||||
if (this.separator == null || !value.contains(this.separator)) {
|
||||
return null;
|
||||
}
|
||||
int position = 0;
|
||||
int index = value.indexOf(this.separator, position);
|
||||
StringBuilder buffer = new StringBuilder();
|
||||
while (index != -1) {
|
||||
if (isEscaped(value, index)) {
|
||||
// Accumulate, without the escape character.
|
||||
buffer.append(value, position, index - 1);
|
||||
buffer.append(value, index, index + this.separator.length());
|
||||
position = index + this.separator.length();
|
||||
index = value.indexOf(this.separator, position);
|
||||
}
|
||||
else {
|
||||
buffer.append(value, position, index);
|
||||
String key = buffer.toString();
|
||||
String fallback = value.substring(index + this.separator.length());
|
||||
return new String[] { key, fallback };
|
||||
}
|
||||
}
|
||||
buffer.append(value, position, value.length());
|
||||
return new String[] { buffer.toString(), null };
|
||||
}
|
||||
|
||||
private static void addText(String value, int start, int end, LinkedList<Part> parts) {
|
||||
if (start > end) {
|
||||
return;
|
||||
}
|
||||
String text = value.substring(start, end);
|
||||
if (!text.isEmpty()) {
|
||||
if (!parts.isEmpty()) {
|
||||
Part current = parts.removeLast();
|
||||
if (current instanceof TextPart textPart) {
|
||||
parts.add(new TextPart(textPart.text + text));
|
||||
}
|
||||
else {
|
||||
parts.add(current);
|
||||
parts.add(new TextPart(text));
|
||||
}
|
||||
}
|
||||
else {
|
||||
parts.add(new TextPart(text));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private int nextStartPrefix(String value, int index) {
|
||||
return value.indexOf(this.prefix, index);
|
||||
}
|
||||
|
||||
private int nextValidEndPrefix(String value, int startIndex) {
|
||||
int index = startIndex + this.prefix.length();
|
||||
int withinNestedPlaceholder = 0;
|
||||
while (index < value.length()) {
|
||||
if (StringUtils.substringMatch(value, index, this.suffix)) {
|
||||
if (withinNestedPlaceholder > 0) {
|
||||
withinNestedPlaceholder--;
|
||||
index = index + this.suffix.length();
|
||||
}
|
||||
else {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
else if (StringUtils.substringMatch(value, index, this.simplePrefix)) {
|
||||
withinNestedPlaceholder++;
|
||||
index = index + this.simplePrefix.length();
|
||||
}
|
||||
else {
|
||||
index++;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private boolean isEscaped(String value, int index) {
|
||||
return (this.escape != null && index > 0 && value.charAt(index - 1) == this.escape);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide the necessary to handle and resolve underlying placeholders.
|
||||
*/
|
||||
static class PartResolutionContext implements PlaceholderResolver {
|
||||
|
||||
private final String prefix;
|
||||
|
||||
private final String suffix;
|
||||
|
||||
private final boolean ignoreUnresolvablePlaceholders;
|
||||
|
||||
private final Function<String, List<Part>> parser;
|
||||
|
||||
private final PlaceholderResolver resolver;
|
||||
|
||||
@Nullable
|
||||
private Set<String> visitedPlaceholders;
|
||||
|
||||
PartResolutionContext(PlaceholderResolver resolver, String prefix, String suffix,
|
||||
boolean ignoreUnresolvablePlaceholders, Function<String, List<Part>> parser) {
|
||||
this.prefix = prefix;
|
||||
this.suffix = suffix;
|
||||
this.ignoreUnresolvablePlaceholders = ignoreUnresolvablePlaceholders;
|
||||
this.parser = parser;
|
||||
this.resolver = resolver;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String resolvePlaceholder(String placeholderName) {
|
||||
String value = this.resolver.resolvePlaceholder(placeholderName);
|
||||
if (value != null && logger.isTraceEnabled()) {
|
||||
logger.trace("Resolved placeholder '" + placeholderName + "'");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
public String handleUnresolvablePlaceholder(String key, String text) {
|
||||
if (this.ignoreUnresolvablePlaceholders) {
|
||||
return toPlaceholderText(key);
|
||||
}
|
||||
String originalValue = (!key.equals(text) ? toPlaceholderText(text) : null);
|
||||
throw new PlaceholderResolutionException(
|
||||
"Could not resolve placeholder '%s'".formatted(key), key, originalValue);
|
||||
}
|
||||
|
||||
private String toPlaceholderText(String text) {
|
||||
return this.prefix + text + this.suffix;
|
||||
}
|
||||
|
||||
public List<Part> parse(String text) {
|
||||
return this.parser.apply(text);
|
||||
}
|
||||
|
||||
public void flagPlaceholderAsVisited(String placeholder) {
|
||||
if (this.visitedPlaceholders == null) {
|
||||
this.visitedPlaceholders = new HashSet<>(4);
|
||||
}
|
||||
if (!this.visitedPlaceholders.add(placeholder)) {
|
||||
throw new PlaceholderResolutionException(
|
||||
"Circular placeholder reference '%s'".formatted(placeholder), placeholder, null);
|
||||
}
|
||||
}
|
||||
|
||||
public void removePlaceholder(String placeholder) {
|
||||
this.visitedPlaceholders.remove(placeholder);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A part is a section of a String containing placeholders to replace.
|
||||
*/
|
||||
interface Part {
|
||||
|
||||
/**
|
||||
* Resolve this part using the specified {@link PartResolutionContext}.
|
||||
* @param resolutionContext the context to use
|
||||
* @return the resolved part
|
||||
*/
|
||||
String resolve(PartResolutionContext resolutionContext);
|
||||
|
||||
/**
|
||||
* Provide a textual representation of this part.
|
||||
* @return the raw text that this part defines
|
||||
*/
|
||||
String text();
|
||||
|
||||
/**
|
||||
* Return a String that appends the resolution of the specified parts.
|
||||
* @param parts the parts to resolve
|
||||
* @param resolutionContext the context to use for the resolution
|
||||
* @return a concatenation of the supplied parts with placeholders replaced inline
|
||||
*/
|
||||
static String resolveAll(Iterable<Part> parts, PartResolutionContext resolutionContext) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (Part part : parts) {
|
||||
sb.append(part.resolve(resolutionContext));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* A representation of the parsing of an input string.
|
||||
* @param text the raw input string
|
||||
* @param parts the parts that appear in the string, in order
|
||||
*/
|
||||
record ParsedValue(String text, List<Part> parts) {
|
||||
|
||||
public String resolve(PartResolutionContext resolutionContext) {
|
||||
try {
|
||||
return Part.resolveAll(this.parts, resolutionContext);
|
||||
}
|
||||
catch (PlaceholderResolutionException ex) {
|
||||
throw ex.withValue(this.text);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link Part} implementation that does not contain a valid placeholder.
|
||||
* @param text the raw (and resolved) text
|
||||
*/
|
||||
record TextPart(String text) implements Part {
|
||||
|
||||
@Override
|
||||
public String resolve(PartResolutionContext resolutionContext) {
|
||||
return this.text;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link Part} implementation that represents a single placeholder with
|
||||
* a hard-coded fallback.
|
||||
* @param text the raw text
|
||||
* @param key the key of the placeholder
|
||||
* @param fallback the fallback to use, if any
|
||||
*/
|
||||
record SimplePlaceholderPart(String text, String key, @Nullable String fallback) implements Part {
|
||||
|
||||
@Override
|
||||
public String resolve(PartResolutionContext resolutionContext) {
|
||||
String resolvedValue = resolveToText(resolutionContext, this.key);
|
||||
if (resolvedValue != null) {
|
||||
return resolvedValue;
|
||||
}
|
||||
else if (this.fallback != null) {
|
||||
return this.fallback;
|
||||
}
|
||||
return resolutionContext.handleUnresolvablePlaceholder(this.key, this.text);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private String resolveToText(PartResolutionContext resolutionContext, String text) {
|
||||
String resolvedValue = resolutionContext.resolvePlaceholder(text);
|
||||
if (resolvedValue != null) {
|
||||
resolutionContext.flagPlaceholderAsVisited(text);
|
||||
// Let's check if we need to recursively resolve that value
|
||||
List<Part> nestedParts = resolutionContext.parse(resolvedValue);
|
||||
String value = toText(nestedParts);
|
||||
if (!isTextOnly(nestedParts)) {
|
||||
value = new ParsedValue(resolvedValue, nestedParts).resolve(resolutionContext);
|
||||
}
|
||||
resolutionContext.removePlaceholder(text);
|
||||
return value;
|
||||
}
|
||||
// Not found
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean isTextOnly(List<Part> parts) {
|
||||
return parts.stream().allMatch(part -> part instanceof TextPart);
|
||||
}
|
||||
|
||||
private String toText(List<Part> parts) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
parts.forEach(part -> sb.append(part.text()));
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link Part} implementation that represents a single placeholder
|
||||
* containing nested placeholders.
|
||||
* @param text the raw text of the root placeholder
|
||||
* @param keyParts the parts of the key
|
||||
* @param defaultParts the parts of the fallback, if any
|
||||
*/
|
||||
record NestedPlaceholderPart(String text, List<Part> keyParts, @Nullable List<Part> defaultParts) implements Part {
|
||||
|
||||
@Override
|
||||
public String resolve(PartResolutionContext resolutionContext) {
|
||||
String resolvedKey = Part.resolveAll(this.keyParts, resolutionContext);
|
||||
String value = resolutionContext.resolvePlaceholder(resolvedKey);
|
||||
if (value != null) {
|
||||
return value;
|
||||
}
|
||||
else if (this.defaultParts != null) {
|
||||
return Part.resolveAll(this.defaultParts, resolutionContext);
|
||||
}
|
||||
return resolutionContext.handleUnresolvablePlaceholder(resolvedKey, this.text);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* 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.
|
||||
* 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.util;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* Thrown when the resolution of placeholder failed. This exception provides
|
||||
* the placeholder as well as the hierarchy of values that led to the issue.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
* @since 6.2
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
public class PlaceholderResolutionException extends RuntimeException {
|
||||
|
||||
private final String reason;
|
||||
|
||||
private final String placeholder;
|
||||
|
||||
private final List<String> values;
|
||||
|
||||
/**
|
||||
* Create an exception using the specified reason for its message.
|
||||
* @param reason the reason for the exception, should contain the placeholder
|
||||
* @param placeholder the placeholder
|
||||
* @param value the original expression that led to the issue if available
|
||||
*/
|
||||
PlaceholderResolutionException(String reason, String placeholder, @Nullable String value) {
|
||||
this(reason, placeholder, (value != null ? List.of(value) : Collections.emptyList()));
|
||||
}
|
||||
|
||||
private PlaceholderResolutionException(String reason, String placeholder, List<String> values) {
|
||||
super(buildMessage(reason, values));
|
||||
this.reason = reason;
|
||||
this.placeholder = placeholder;
|
||||
this.values = values;
|
||||
}
|
||||
|
||||
private static String buildMessage(String reason, List<String> values) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(reason);
|
||||
if (!CollectionUtils.isEmpty(values)) {
|
||||
String valuesChain = values.stream().map(value -> "\"" + value + "\"")
|
||||
.collect(Collectors.joining(" <-- "));
|
||||
sb.append(" in value %s".formatted(valuesChain));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a {@link PlaceholderResolutionException} that provides
|
||||
* an additional parent value.
|
||||
* @param value the parent value to add
|
||||
* @return a new exception with the parent value added
|
||||
*/
|
||||
PlaceholderResolutionException withValue(String value) {
|
||||
List<String> allValues = new ArrayList<>(this.values);
|
||||
allValues.add(value);
|
||||
return new PlaceholderResolutionException(this.reason, this.placeholder, allValues);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the placeholder that could not be resolved.
|
||||
* @return the unresolvable placeholder
|
||||
*/
|
||||
public String getPlaceholder() {
|
||||
return this.placeholder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a contextualized list of the resolution attempts that led to this
|
||||
* exception, where the first element is the value that generated this
|
||||
* exception.
|
||||
* @return the stack of values that led to this exception
|
||||
*/
|
||||
public List<String> getValues() {
|
||||
return this.values;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<String, String> 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<String> 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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
/**
|
||||
|
||||
@@ -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}\"");
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -0,0 +1,355 @@
|
||||
/*
|
||||
* 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.
|
||||
* 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.util;
|
||||
|
||||
import java.util.Properties;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
|
||||
import org.springframework.util.PlaceholderParser.ParsedValue;
|
||||
import org.springframework.util.PlaceholderParser.TextPart;
|
||||
import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
|
||||
/**
|
||||
* Tests for {@link PlaceholderParser}.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
*/
|
||||
class PlaceholderParserTests {
|
||||
|
||||
@Nested // Tests with only the basic placeholder feature enabled
|
||||
class OnlyPlaceholderTests {
|
||||
|
||||
private final PlaceholderParser parser = new PlaceholderParser("${", "}", true, null, null);
|
||||
|
||||
@ParameterizedTest(name = "{0} -> {1}")
|
||||
@MethodSource("placeholders")
|
||||
void placeholderIsReplaced(String text, String expected) {
|
||||
Properties properties = new Properties();
|
||||
properties.setProperty("firstName", "John");
|
||||
properties.setProperty("nested0", "first");
|
||||
properties.setProperty("nested1", "Name");
|
||||
assertThat(this.parser.replacePlaceholders(text, properties::getProperty)).isEqualTo(expected);
|
||||
}
|
||||
|
||||
static Stream<Arguments> placeholders() {
|
||||
return Stream.of(
|
||||
Arguments.of("${firstName}", "John"),
|
||||
Arguments.of("$${firstName}", "$John"),
|
||||
Arguments.of("}${firstName}", "}John"),
|
||||
Arguments.of("${firstName}$", "John$"),
|
||||
Arguments.of("${firstName}}", "John}"),
|
||||
Arguments.of("${firstName} ${firstName}", "John John"),
|
||||
Arguments.of("First name: ${firstName}", "First name: John"),
|
||||
Arguments.of("${firstName} is the first name", "John is the first name"),
|
||||
Arguments.of("${first${nested1}}", "John"),
|
||||
Arguments.of("${${nested0}${nested1}}", "John")
|
||||
);
|
||||
}
|
||||
|
||||
@ParameterizedTest(name = "{0} -> {1}")
|
||||
@MethodSource("nestedPlaceholders")
|
||||
void nestedPlaceholdersAreReplaced(String text, String expected) {
|
||||
Properties properties = new Properties();
|
||||
properties.setProperty("p1", "v1");
|
||||
properties.setProperty("p2", "v2");
|
||||
properties.setProperty("p3", "${p1}:${p2}"); // nested placeholders
|
||||
properties.setProperty("p4", "${p3}"); // deeply nested placeholders
|
||||
properties.setProperty("p5", "${p1}:${p2}:${bogus}"); // unresolvable placeholder
|
||||
assertThat(this.parser.replacePlaceholders(text, properties::getProperty)).isEqualTo(expected);
|
||||
}
|
||||
|
||||
static Stream<Arguments> nestedPlaceholders() {
|
||||
return Stream.of(
|
||||
Arguments.of("${p1}:${p2}", "v1:v2"),
|
||||
Arguments.of("${p3}", "v1:v2"),
|
||||
Arguments.of("${p4}", "v1:v2"),
|
||||
Arguments.of("${p5}", "v1:v2:${bogus}"),
|
||||
Arguments.of("${p0${p0}}", "${p0${p0}}")
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseWithSinglePlaceholder() {
|
||||
PlaceholderResolver resolver = mockPlaceholderResolver("firstName", "John");
|
||||
assertThat(this.parser.replacePlaceholders("${firstName}", resolver))
|
||||
.isEqualTo("John");
|
||||
verify(resolver).resolvePlaceholder("firstName");
|
||||
verifyNoMoreInteractions(resolver);
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseWithPlaceholderAndPrefixText() {
|
||||
PlaceholderResolver resolver = mockPlaceholderResolver("firstName", "John");
|
||||
assertThat(this.parser.replacePlaceholders("This is ${firstName}", resolver))
|
||||
.isEqualTo("This is John");
|
||||
verify(resolver).resolvePlaceholder("firstName");
|
||||
verifyNoMoreInteractions(resolver);
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseWithMultiplePlaceholdersAndText() {
|
||||
PlaceholderResolver resolver = mockPlaceholderResolver("firstName", "John", "lastName", "Smith");
|
||||
assertThat(this.parser.replacePlaceholders("User: ${firstName} - ${lastName}.", resolver))
|
||||
.isEqualTo("User: John - Smith.");
|
||||
verify(resolver).resolvePlaceholder("firstName");
|
||||
verify(resolver).resolvePlaceholder("lastName");
|
||||
verifyNoMoreInteractions(resolver);
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseWithNestedPlaceholderInKey() {
|
||||
PlaceholderResolver resolver = mockPlaceholderResolver(
|
||||
"nested", "Name", "firstName", "John");
|
||||
assertThat(this.parser.replacePlaceholders("${first${nested}}", resolver))
|
||||
.isEqualTo("John");
|
||||
verifyPlaceholderResolutions(resolver, "nested", "firstName");
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseWithMultipleNestedPlaceholdersInKey() {
|
||||
PlaceholderResolver resolver = mockPlaceholderResolver(
|
||||
"nested0", "first", "nested1", "Name", "firstName", "John");
|
||||
assertThat(this.parser.replacePlaceholders("${${nested0}${nested1}}", resolver))
|
||||
.isEqualTo("John");
|
||||
verifyPlaceholderResolutions(resolver, "nested0", "nested1", "firstName");
|
||||
}
|
||||
|
||||
@Test
|
||||
void placeholdersWithSeparatorAreHandledAsIs() {
|
||||
PlaceholderResolver resolver = mockPlaceholderResolver("my:test", "value");
|
||||
assertThat(this.parser.replacePlaceholders("${my:test}", resolver)).isEqualTo("value");
|
||||
verifyPlaceholderResolutions(resolver, "my:test");
|
||||
}
|
||||
|
||||
@Test
|
||||
void placeholdersWithoutEscapeCharAreNotEscaped() {
|
||||
PlaceholderResolver resolver = mockPlaceholderResolver("test", "value");
|
||||
assertThat(this.parser.replacePlaceholders("\\${test}", resolver)).isEqualTo("\\value");
|
||||
verifyPlaceholderResolutions(resolver, "test");
|
||||
}
|
||||
|
||||
@Test
|
||||
void textWithInvalidPlaceholderIsMerged() {
|
||||
String text = "test${of${with${and${";
|
||||
ParsedValue parsedValue = this.parser.parse(text);
|
||||
assertThat(parsedValue.parts()).singleElement().isInstanceOfSatisfying(
|
||||
TextPart.class, textPart -> assertThat(textPart.text()).isEqualTo(text));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested // Tests with the use of a separator
|
||||
class DefaultValueTests {
|
||||
|
||||
private final PlaceholderParser parser = new PlaceholderParser("${", "}", true, ":", null);
|
||||
|
||||
@ParameterizedTest(name = "{0} -> {1}")
|
||||
@MethodSource("placeholders")
|
||||
void placeholderIsReplaced(String text, String expected) {
|
||||
Properties properties = new Properties();
|
||||
properties.setProperty("firstName", "John");
|
||||
properties.setProperty("nested0", "first");
|
||||
properties.setProperty("nested1", "Name");
|
||||
assertThat(this.parser.replacePlaceholders(text, properties::getProperty)).isEqualTo(expected);
|
||||
}
|
||||
|
||||
static Stream<Arguments> placeholders() {
|
||||
return Stream.of(
|
||||
Arguments.of("${invalid:John}", "John"),
|
||||
Arguments.of("${first${invalid:Name}}", "John"),
|
||||
Arguments.of("${invalid:${firstName}}", "John"),
|
||||
Arguments.of("${invalid:${${nested0}${nested1}}}", "John"),
|
||||
Arguments.of("${invalid:$${firstName}}", "$John"),
|
||||
Arguments.of("${invalid: }${firstName}", " John"),
|
||||
Arguments.of("${invalid:}", ""),
|
||||
Arguments.of("${:}", "")
|
||||
);
|
||||
}
|
||||
|
||||
@ParameterizedTest(name = "{0} -> {1}")
|
||||
@MethodSource("nestedPlaceholders")
|
||||
void nestedPlaceholdersAreReplaced(String text, String expected) {
|
||||
Properties properties = new Properties();
|
||||
properties.setProperty("p1", "v1");
|
||||
properties.setProperty("p2", "v2");
|
||||
properties.setProperty("p3", "${p1}:${p2}"); // nested placeholders
|
||||
properties.setProperty("p4", "${p3}"); // deeply nested placeholders
|
||||
properties.setProperty("p5", "${p1}:${p2}:${bogus}"); // unresolvable placeholder
|
||||
properties.setProperty("p6", "${p1}:${p2}:${bogus:def}"); // unresolvable w/ default
|
||||
assertThat(this.parser.replacePlaceholders(text, properties::getProperty)).isEqualTo(expected);
|
||||
}
|
||||
|
||||
static Stream<Arguments> nestedPlaceholders() {
|
||||
return Stream.of(
|
||||
Arguments.of("${p6}", "v1:v2:def"),
|
||||
Arguments.of("${invalid:${p1}:${p2}}", "v1:v2"),
|
||||
Arguments.of("${invalid:${p3}}", "v1:v2"),
|
||||
Arguments.of("${invalid:${p4}}", "v1:v2"),
|
||||
Arguments.of("${invalid:${p5}}", "v1:v2:${bogus}"),
|
||||
Arguments.of("${invalid:${p6}}", "v1:v2:def")
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseWithHardcodedFallback() {
|
||||
PlaceholderResolver resolver = mockPlaceholderResolver();
|
||||
assertThat(this.parser.replacePlaceholders("${firstName:Steve}", resolver))
|
||||
.isEqualTo("Steve");
|
||||
verifyPlaceholderResolutions(resolver, "firstName");
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseWithNestedPlaceholderInKeyUsingFallback() {
|
||||
PlaceholderResolver resolver = mockPlaceholderResolver("firstName", "John");
|
||||
assertThat(this.parser.replacePlaceholders("${first${invalid:Name}}", resolver))
|
||||
.isEqualTo("John");
|
||||
verifyPlaceholderResolutions(resolver, "invalid", "firstName");
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseWithFallbackUsingPlaceholder() {
|
||||
PlaceholderResolver resolver = mockPlaceholderResolver("firstName", "John");
|
||||
assertThat(this.parser.replacePlaceholders("${invalid:${firstName}}", resolver))
|
||||
.isEqualTo("John");
|
||||
verifyPlaceholderResolutions(resolver, "invalid", "firstName");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested // Tests with the use of the escape character
|
||||
class EscapedTests {
|
||||
|
||||
private final PlaceholderParser parser = new PlaceholderParser("${", "}", true, ":", '\\');
|
||||
|
||||
@ParameterizedTest(name = "{0} -> {1}")
|
||||
@MethodSource("escapedPlaceholders")
|
||||
void escapedPlaceholderIsNotReplaced(String text, String expected) {
|
||||
PlaceholderResolver resolver = mockPlaceholderResolver(
|
||||
"firstName", "John", "nested0", "first", "nested1", "Name",
|
||||
"${test}", "John",
|
||||
"p1", "v1", "p2", "\\${p1:default}", "p3", "${p2}",
|
||||
"p4", "adc${p0:\\${p1}}",
|
||||
"p5", "adc${\\${p0}:${p1}}",
|
||||
"p6", "adc${p0:def\\${p1}}",
|
||||
"p7", "adc\\${");
|
||||
assertThat(this.parser.replacePlaceholders(text, resolver)).isEqualTo(expected);
|
||||
}
|
||||
|
||||
static Stream<Arguments> escapedPlaceholders() {
|
||||
return Stream.of(
|
||||
Arguments.of("\\${firstName}", "${firstName}"),
|
||||
Arguments.of("First name: \\${firstName}", "First name: ${firstName}"),
|
||||
Arguments.of("$\\${firstName}", "$${firstName}"),
|
||||
Arguments.of("\\}${firstName}", "\\}John"),
|
||||
Arguments.of("${\\${test}}", "John"),
|
||||
Arguments.of("${p2}", "${p1:default}"),
|
||||
Arguments.of("${p3}", "${p1:default}"),
|
||||
Arguments.of("${p4}", "adc${p1}"),
|
||||
Arguments.of("${p5}", "adcv1"),
|
||||
Arguments.of("${p6}", "adcdef${p1}"),
|
||||
Arguments.of("${p7}", "adc\\${"));
|
||||
|
||||
}
|
||||
|
||||
@ParameterizedTest(name = "{0} -> {1}")
|
||||
@MethodSource("escapedSeparators")
|
||||
void escapedSeparatorIsNotReplaced(String text, String expected) {
|
||||
Properties properties = new Properties();
|
||||
properties.setProperty("first:Name", "John");
|
||||
properties.setProperty("nested0", "first");
|
||||
properties.setProperty("nested1", "Name");
|
||||
assertThat(this.parser.replacePlaceholders(text, properties::getProperty)).isEqualTo(expected);
|
||||
}
|
||||
|
||||
static Stream<Arguments> escapedSeparators() {
|
||||
return Stream.of(
|
||||
Arguments.of("${first\\:Name}", "John"),
|
||||
Arguments.of("${last\\:Name}", "${last:Name}")
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class ExceptionTests {
|
||||
|
||||
private final PlaceholderParser parser = new PlaceholderParser("${", "}", false, ":", null);
|
||||
|
||||
@Test
|
||||
void textWithCircularReference() {
|
||||
PlaceholderResolver resolver = mockPlaceholderResolver("pL", "${pR}", "pR", "${pL}");
|
||||
assertThatThrownBy(() -> this.parser.replacePlaceholders("${pL}", resolver))
|
||||
.isInstanceOf(PlaceholderResolutionException.class)
|
||||
.hasMessage("Circular placeholder reference 'pL' in value \"${pL}\" <-- \"${pR}\" <-- \"${pL}\"");
|
||||
}
|
||||
|
||||
@Test
|
||||
void unresolvablePlaceholderIsReported() {
|
||||
PlaceholderResolver resolver = mockPlaceholderResolver();
|
||||
assertThatExceptionOfType(PlaceholderResolutionException.class)
|
||||
.isThrownBy(() -> this.parser.replacePlaceholders("${bogus}", resolver))
|
||||
.withMessage("Could not resolve placeholder 'bogus' in value \"${bogus}\"")
|
||||
.withNoCause();
|
||||
}
|
||||
|
||||
@Test
|
||||
void unresolvablePlaceholderInNestedPlaceholderIsReportedWithChain() {
|
||||
PlaceholderResolver resolver = mockPlaceholderResolver("p1", "v1", "p2", "v2",
|
||||
"p3", "${p1}:${p2}:${bogus}");
|
||||
assertThatExceptionOfType(PlaceholderResolutionException.class)
|
||||
.isThrownBy(() -> this.parser.replacePlaceholders("${p3}", resolver))
|
||||
.withMessage("Could not resolve placeholder 'bogus' in value \"${p1}:${p2}:${bogus}\" <-- \"${p3}\"")
|
||||
.withNoCause();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
PlaceholderResolver mockPlaceholderResolver(String... pairs) {
|
||||
if (pairs.length % 2 == 1) {
|
||||
throw new IllegalArgumentException("size must be even, it is a set of key=value pairs");
|
||||
}
|
||||
PlaceholderResolver resolver = mock(PlaceholderResolver.class);
|
||||
for (int i = 0; i < pairs.length; i += 2) {
|
||||
String key = pairs[i];
|
||||
String value = pairs[i + 1];
|
||||
given(resolver.resolvePlaceholder(key)).willReturn(value);
|
||||
}
|
||||
return resolver;
|
||||
}
|
||||
|
||||
void verifyPlaceholderResolutions(PlaceholderResolver mock, String... placeholders) {
|
||||
for (String placeholder : placeholders) {
|
||||
verify(mock).resolvePlaceholder(placeholder);
|
||||
}
|
||||
verifyNoMoreInteractions(mock);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<Arguments> defaultValues() {
|
||||
|
||||
@@ -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}"));
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<StandaloneM
|
||||
private final PlaceholderResolver resolver;
|
||||
|
||||
public StaticStringValueResolver(Map<String, String> values) {
|
||||
this.helper = new PropertyPlaceholderHelper("${", "}", ":", false);
|
||||
this.helper = new PropertyPlaceholderHelper("${", "}", ":", false, null);
|
||||
this.resolver = values::get;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user