Skip to content

Commit e402269

Browse files
MBoegerstimtebeek
andauthored
Support escaping property placeholders with backslash (#6817)
* Support escaping property placeholders in recipe YAML When recipe YAML contains Maven property references like ${java.version} in parameter values, the YamlResourceLoader resolves them as property placeholders at load time. This prevents users from passing literal ${...} references through to recipe parameters. Add escape support: doubling the first character of the placeholder prefix (e.g., writing $${java.version} for the ${ prefix) produces a literal ${java.version} in the resolved output. Fixes moderneinc/customer-requests#1874 * Support escaping property placeholders in recipe YAML When recipe YAML contains Maven property references like ${java.version} in parameter values, the YamlResourceLoader resolves them as property placeholders at load time. This prevents users from passing literal ${...} references through to recipe parameters. Add escape support: a backslash before the placeholder prefix (e.g., writing \${java.version} for the ${ prefix) produces a literal ${java.version} in the resolved output. Fixes moderneinc/customer-requests#1874 * Update configuration description and add regression tests Document the \${...} escape syntax in the ChangePluginConfiguration @option description so users know how to pass literal Maven property references. Add regression tests to ensure existing behavior is preserved: - Unresolved placeholders are left as-is - Normal resolution still works - Backslashes not before a placeholder prefix are preserved * Add javadoc to replacePlaceholders methods * Polish * Regenerate recipes.csv for updated docs; remove duplicates --------- Co-authored-by: Tim te Beek <tim@moderne.io>
1 parent 0abe800 commit e402269

5 files changed

Lines changed: 187 additions & 109 deletions

File tree

rewrite-core/src/main/java/org/openrewrite/internal/PropertyPlaceholderHelper.java

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,12 +84,31 @@ public List<String> getPlaceholders(@Nullable String value) {
8484
return placeholders;
8585
}
8686

87+
/**
88+
* Replace placeholders in the given value, resolving each against the supplied properties.
89+
* A backslash before the placeholder prefix (e.g. {@code \${...}}) escapes it as a literal.
90+
*/
8791
public String replacePlaceholders(String value, final Properties properties) {
8892
return replacePlaceholders(value, properties::getProperty);
8993
}
9094

95+
/**
96+
* Replace placeholders in the given value, resolving each via {@code placeholderResolver}.
97+
* A backslash before the placeholder prefix (e.g. {@code \${...}}) escapes it as a literal.
98+
*/
9199
public String replacePlaceholders(String value, Function<String, @Nullable String> placeholderResolver) {
92-
return parseStringValue(value, placeholderResolver, null);
100+
// Support escaping: a backslash before the placeholder prefix produces a literal
101+
// prefix. E.g., for prefix "${", writing "\${" produces literal "${".
102+
String escapePrefix = "\\" + placeholderPrefix;
103+
boolean hasEscaped = value.contains(escapePrefix);
104+
if (hasEscaped) {
105+
value = value.replace(escapePrefix, "\u0000\u0001\u0002");
106+
}
107+
String result = parseStringValue(value, placeholderResolver, null);
108+
if (hasEscaped) {
109+
result = result.replace("\u0000\u0001\u0002", placeholderPrefix);
110+
}
111+
return result;
93112
}
94113

95114
protected String parseStringValue(String value, Function<String, @Nullable String> placeholderResolver,

rewrite-core/src/test/java/org/openrewrite/internal/PropertyPlaceholderHelperTest.java

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,52 @@ void withValueSeparatorAndValueReplacement() {
8888
assertThat(s).isEqualTo("hi jon");
8989
}
9090

91+
@Test
92+
void escapedPlaceholder() {
93+
var helper = new PropertyPlaceholderHelper("${", "}", null);
94+
var s = helper.replacePlaceholders("\\${java.version}", k -> "should-not-resolve");
95+
assertThat(s).isEqualTo("${java.version}");
96+
}
97+
98+
@Test
99+
void escapedPlaceholderWithOtherPrefix() {
100+
var helper = new PropertyPlaceholderHelper("%%{", "}", null);
101+
var s = helper.replacePlaceholders("\\%%{k1}", k -> "should-not-resolve");
102+
assertThat(s).isEqualTo("%%{k1}");
103+
}
104+
105+
@Test
106+
void mixedEscapedAndResolvedPlaceholders() {
107+
var helper = new PropertyPlaceholderHelper("${", "}", null);
108+
var s = helper.replacePlaceholders("${greeting} \\${java.version}", k -> "greeting".equals(k) ? "hello" : null);
109+
assertThat(s).isEqualTo("hello ${java.version}");
110+
}
111+
112+
@Test
113+
void unresolvedPlaceholderLeftAsIs() {
114+
var helper = new PropertyPlaceholderHelper("${", "}", null);
115+
var s = helper.replacePlaceholders("${unresolved}", k -> null);
116+
assertThat(s).isEqualTo("${unresolved}");
117+
}
118+
119+
@Test
120+
void normalResolutionStillWorks() {
121+
var helper = new PropertyPlaceholderHelper("${", "}", null);
122+
var s = helper.replacePlaceholders("${greeting} ${name}", k -> switch (k) {
123+
case "greeting" -> "hello";
124+
case "name" -> "world";
125+
default -> null;
126+
});
127+
assertThat(s).isEqualTo("hello world");
128+
}
129+
130+
@Test
131+
void backslashNotBeforePrefixIsPreserved() {
132+
var helper = new PropertyPlaceholderHelper("${", "}", null);
133+
var s = helper.replacePlaceholders("path\\to\\file", k -> null);
134+
assertThat(s).isEqualTo("path\\to\\file");
135+
}
136+
91137
@Test
92138
void withValueSeparatorAndNullReplacement() {
93139
var helper = new PropertyPlaceholderHelper("%%{", "}", ",");

rewrite-maven/src/main/java/org/openrewrite/maven/ChangePluginConfiguration.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,10 @@ public class ChangePluginConfiguration extends Recipe {
5050
@Option(displayName = "Configuration",
5151
description = "Plugin configuration provided as raw XML overriding any existing configuration. " +
5252
"Configuration inside `<executions>` blocks will not be altered. " +
53-
"Supplying `null` will remove any existing configuration.",
53+
"Supplying `null` will remove any existing configuration. " +
54+
"To include a literal `${...}` property reference in the configuration " +
55+
"(e.g. a Maven property like `${java.version}`), escape it as `\\${...}` " +
56+
"in your recipe YAML to prevent it from being resolved as a recipe placeholder.",
5457
example = "<foo>bar</foo>",
5558
required = false)
5659
@Nullable

0 commit comments

Comments
 (0)