[MNG-8344] Support multiple operators in variable expansion (#1832)

This commit is contained in:
Guillaume Nodet 2024-10-24 13:54:34 +02:00 committed by GitHub
parent dc6d48ce00
commit 54df597018
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 139 additions and 60 deletions

View File

@ -268,66 +268,8 @@ public class DefaultInterpolator implements Interpolator {
String variable = val.substring(startDelim + DELIM_START.length(), stopDelim); String variable = val.substring(startDelim + DELIM_START.length(), stopDelim);
String org = variable; String org = variable;
// Strip expansion modifiers String substValue = processSubstitution(
int idx1 = variable.lastIndexOf(":-"); variable, org, cycleMap, configProps, callback, postprocessor, defaultsToEmptyString);
int idx2 = variable.lastIndexOf(":+");
int idx = idx1 >= 0 ? idx2 >= 0 ? Math.min(idx1, idx2) : idx1 : idx2;
String op = null;
if (idx >= 0) {
op = variable.substring(idx);
variable = variable.substring(0, idx);
}
// Verify that this is not a recursive variable reference.
if (!cycleMap.add(variable)) {
throw new InterpolatorException("recursive variable reference: " + variable);
}
String substValue = null;
// Get the value of the deepest nested variable placeholder.
// Try to configuration properties first.
if (configProps != null) {
substValue = configProps.get(variable);
}
if (substValue == null) {
if (!variable.isEmpty()) {
if (callback != null) {
String s1 = callback.apply(variable);
String s2 = doSubstVars(
s1, variable, cycleMap, configProps, callback, postprocessor, defaultsToEmptyString);
substValue = postprocessor != null ? postprocessor.apply(variable, s2) : s2;
}
}
}
if (op != null) {
if (op.startsWith(":-")) {
if (substValue == null || substValue.isEmpty()) {
substValue = op.substring(":-".length());
}
} else if (op.startsWith(":+")) {
if (substValue != null && !substValue.isEmpty()) {
substValue = op.substring(":+".length());
}
} else {
throw new InterpolatorException("Bad substitution: ${" + org + "}");
}
}
if (substValue == null) {
if (defaultsToEmptyString) {
substValue = "";
} else {
// alters the original token to avoid infinite recursion
// altered tokens are reverted in unescape()
substValue = MARKER + "{" + variable + "}";
}
}
// Remove the found variable from the cycle map, since
// it may appear more than once in the value and we don't
// want such situations to appear as a recursive reference.
cycleMap.remove(variable);
// Append the leading characters, the substituted value of // Append the leading characters, the substituted value of
// the variable, and the trailing characters to get the new // the variable, and the trailing characters to get the new
@ -344,6 +286,111 @@ public class DefaultInterpolator implements Interpolator {
return val; return val;
} }
private static String processSubstitution(
String variable,
String org,
Set<String> cycleMap,
Map<String, String> configProps,
Function<String, String> callback,
BiFunction<String, String, String> postprocessor,
boolean defaultsToEmptyString) {
// Process chained operators from left to right
int startIdx = 0;
String currentVar = variable;
String substValue = null;
while (startIdx < variable.length()) {
int idx1 = variable.indexOf(":-", startIdx);
int idx2 = variable.indexOf(":+", startIdx);
int idx = idx1 >= 0 ? idx2 >= 0 ? Math.min(idx1, idx2) : idx1 : idx2;
if (idx < 0) {
// No more operators, process the final variable
if (substValue == null) {
currentVar = variable.substring(startIdx);
substValue = resolveVariable(
currentVar, cycleMap, configProps, callback, postprocessor, defaultsToEmptyString);
}
break;
}
// Get the current variable part before the operator
String varPart = variable.substring(startIdx, idx);
if (substValue == null) {
substValue =
resolveVariable(varPart, cycleMap, configProps, callback, postprocessor, defaultsToEmptyString);
}
// Find the end of the current operator's value
int nextIdx1 = variable.indexOf(":-", idx + 2);
int nextIdx2 = variable.indexOf(":+", idx + 2);
int nextIdx = nextIdx1 >= 0 ? nextIdx2 >= 0 ? Math.min(nextIdx1, nextIdx2) : nextIdx1 : nextIdx2;
String op = variable.substring(idx, idx + 2);
String opValue = variable.substring(idx + 2, nextIdx >= 0 ? nextIdx : variable.length());
// Process the operator value through substitution if it contains variables
String processedOpValue =
doSubstVars(opValue, org, cycleMap, configProps, callback, postprocessor, defaultsToEmptyString);
// Apply the operator
if (":+".equals(op)) {
if (substValue != null && !substValue.isEmpty()) {
substValue = processedOpValue;
}
} else if (":-".equals(op)) {
if (substValue == null || substValue.isEmpty()) {
substValue = processedOpValue;
}
} else {
throw new InterpolatorException("Bad substitution operator in: ${" + org + "}");
}
startIdx = nextIdx >= 0 ? nextIdx : variable.length();
}
if (substValue == null) {
if (defaultsToEmptyString) {
substValue = "";
} else {
substValue = MARKER + "{" + variable + "}";
}
}
return substValue;
}
private static String resolveVariable(
String variable,
Set<String> cycleMap,
Map<String, String> configProps,
Function<String, String> callback,
BiFunction<String, String, String> postprocessor,
boolean defaultsToEmptyString) {
// Verify that this is not a recursive variable reference
if (!cycleMap.add(variable)) {
throw new InterpolatorException("recursive variable reference: " + variable);
}
String substValue = null;
// Try configuration properties first
if (configProps != null) {
substValue = configProps.get(variable);
}
if (substValue == null && !variable.isEmpty() && callback != null) {
String s1 = callback.apply(variable);
String s2 =
doSubstVars(s1, variable, cycleMap, configProps, callback, postprocessor, defaultsToEmptyString);
substValue = postprocessor != null ? postprocessor.apply(variable, s2) : s2;
}
// Remove the variable from cycle map
cycleMap.remove(variable);
return substValue;
}
/** /**
* Escapes special characters in the given string to prevent unwanted interpolation. * Escapes special characters in the given string to prevent unwanted interpolation.
* *

View File

@ -170,6 +170,38 @@ class DefaultInterpolatorTest {
assertEquals("", props.get("c_cp")); assertEquals("", props.get("c_cp"));
} }
@Test
void testXdg() {
Map<String, String> props;
props = new LinkedHashMap<>();
props.put("user.home", "/Users/gnodet");
props.put(
"maven.user.config",
"${env.MAVEN_XDG:+${env.XDG_CONFIG_HOME:-${user.home}/.config/maven}:-${user.home}/.m2}");
performSubstitution(props);
assertEquals("/Users/gnodet/.m2", props.get("maven.user.config"));
props = new LinkedHashMap<>();
props.put("user.home", "/Users/gnodet");
props.put(
"maven.user.config",
"${env.MAVEN_XDG:+${env.XDG_CONFIG_HOME:-${user.home}/.config/maven}:-${user.home}/.m2}");
props.put("env.MAVEN_XDG", "true");
performSubstitution(props);
assertEquals("/Users/gnodet/.config/maven", props.get("maven.user.config"));
props = new LinkedHashMap<>();
props.put("user.home", "/Users/gnodet");
props.put(
"maven.user.config",
"${env.MAVEN_XDG:+${env.XDG_CONFIG_HOME:-${user.home}/.config/maven}:-${user.home}/.m2}");
props.put("env.MAVEN_XDG", "true");
props.put("env.XDG_CONFIG_HOME", "/Users/gnodet/.xdg/maven");
performSubstitution(props);
assertEquals("/Users/gnodet/.xdg/maven", props.get("maven.user.config"));
}
private void performSubstitution(Map<String, String> props) { private void performSubstitution(Map<String, String> props) {
performSubstitution(props, null); performSubstitution(props, null);
} }