diff --git a/src/main/java/org/apache/commons/text/StringSubstitutor.java b/src/main/java/org/apache/commons/text/StringSubstitutor.java
index 9fce66de24..2d90908046 100644
--- a/src/main/java/org/apache/commons/text/StringSubstitutor.java
+++ b/src/main/java/org/apache/commons/text/StringSubstitutor.java
@@ -1390,19 +1390,18 @@ protected boolean substitute(final TextStringBuilder builder, final int offset,
}
/**
- * Recursive handler for multiple levels of interpolation. This is the main interpolation method, which resolves the
- * values of all variable references contained in the passed in text.
+ * Recursive handler for multiple levels of interpolation. This is the main interpolation method, which resolves the values of all variable references
+ * contained in the passed in text.
*
- * @param builder the string builder to substitute into, not null.
- * @param offset the start offset within the builder, must be valid.
- * @param length the length within the builder to be processed, must be valid.
+ * @param builder the string builder to substitute into, not null.
+ * @param offset the start offset within the builder, must be valid.
+ * @param length the length within the builder to be processed, must be valid.
* @param priorVariables the stack keeping track of the replaced variables, may be null.
* @return The result.
* @throws IllegalArgumentException if variable is not found and isEnableUndefinedVariableException() == true.
* @since 1.9
*/
- private Result substitute(final TextStringBuilder builder, final int offset, final int length,
- List priorVariables) {
+ private Result substitute(final TextStringBuilder builder, final int offset, final int length, List priorVariables) {
Objects.requireNonNull(builder, "builder");
final StringMatcher prefixMatcher = getVariablePrefixMatcher();
final StringMatcher suffixMatcher = getVariableSuffixMatcher();
@@ -1412,7 +1411,6 @@ private Result substitute(final TextStringBuilder builder, final int offset, fin
final boolean substitutionInValuesDisabled = isDisableSubstitutionInValues();
final boolean undefinedVariableException = isEnableUndefinedVariableException();
final boolean preserveEscapes = isPreserveEscapes();
-
boolean altered = false;
int lengthChange = 0;
int bufEnd = offset + length;
@@ -1447,7 +1445,6 @@ private Result substitute(final TextStringBuilder builder, final int offset, fin
pos += endMatchLen;
continue;
}
-
endMatchLen = suffixMatcher.isMatch(builder, pos, offset, bufEnd);
if (endMatchLen == 0) {
pos++;
@@ -1455,19 +1452,21 @@ private Result substitute(final TextStringBuilder builder, final int offset, fin
// found variable end marker
if (nestedVarCount == 0) {
if (escPos >= 0) {
+ final boolean escapedVariableStartsWithNestedPrefix = prefixMatcher.isMatch(builder, startPos + startMatchLen, offset,
+ bufEnd) != 0;
+ final boolean hasOuterSuffix = hasLaterSuffix(builder, pos + endMatchLen, offset, bufEnd, suffixMatcher);
+ pos = escapedVariableStartsWithNestedPrefix && !hasOuterSuffix ? escPos : startPos + 1;
// delete escape
builder.deleteCharAt(escPos);
escPos = -1;
lengthChange--;
altered = true;
bufEnd--;
- pos = startPos + 1;
startPos--;
continue outer;
}
// get var name
- String varNameExpr = builder.midString(startPos + startMatchLen,
- pos - startPos - startMatchLen);
+ String varNameExpr = builder.midString(startPos + startMatchLen, pos - startPos - startMatchLen);
if (substitutionInVariablesEnabled) {
final TextStringBuilder bufName = new TextStringBuilder(varNameExpr);
substitute(bufName, 0, bufName.length());
@@ -1475,41 +1474,33 @@ private Result substitute(final TextStringBuilder builder, final int offset, fin
}
pos += endMatchLen;
final int endPos = pos;
-
String varName = varNameExpr;
String varDefaultValue = null;
-
if (valueDelimMatcher != null) {
final char[] varNameExprChars = varNameExpr.toCharArray();
int valueDelimiterMatchLen = 0;
for (int i = 0; i < varNameExprChars.length; i++) {
// if there's any nested variable when nested variable substitution disabled,
// then stop resolving name and default value.
- if (!substitutionInVariablesEnabled && prefixMatcher.isMatch(varNameExprChars, i, i,
- varNameExprChars.length) != 0) {
+ if (!substitutionInVariablesEnabled && prefixMatcher.isMatch(varNameExprChars, i, i, varNameExprChars.length) != 0) {
break;
}
- if (valueDelimMatcher.isMatch(varNameExprChars, i, 0,
- varNameExprChars.length) != 0) {
- valueDelimiterMatchLen = valueDelimMatcher.isMatch(varNameExprChars, i, 0,
- varNameExprChars.length);
+ if (valueDelimMatcher.isMatch(varNameExprChars, i, 0, varNameExprChars.length) != 0) {
+ valueDelimiterMatchLen = valueDelimMatcher.isMatch(varNameExprChars, i, 0, varNameExprChars.length);
varName = varNameExpr.substring(0, i);
varDefaultValue = varNameExpr.substring(i + valueDelimiterMatchLen);
break;
}
}
}
-
// on the first call initialize priorVariables
if (priorVariables == null) {
priorVariables = new ArrayList<>();
priorVariables.add(builder.midString(offset, length));
}
-
// handle cyclic substitution
checkCyclicSubstitution(varName, priorVariables);
priorVariables.add(varName);
-
// resolve the variable
String varValue = resolveVariable(varName, builder, startPos, endPos);
if (varValue == null) {
@@ -1528,11 +1519,9 @@ private Result substitute(final TextStringBuilder builder, final int offset, fin
bufEnd += change;
lengthChange += change;
} else if (undefinedVariableException) {
- throw new IllegalArgumentException(
- String.format("Cannot resolve variable '%s' (enableSubstitutionInVariables=%s).",
- varName, substitutionInVariablesEnabled));
+ throw new IllegalArgumentException(String.format("Cannot resolve variable '%s' (enableSubstitutionInVariables=%s).", varName,
+ substitutionInVariablesEnabled));
}
-
// remove variable from the cyclic stack
priorVariables.remove(priorVariables.size() - 1);
break;
@@ -1546,6 +1535,26 @@ private Result substitute(final TextStringBuilder builder, final int offset, fin
return new Result(altered, lengthChange);
}
+ /**
+ * Checks whether the specified buffer contains a variable suffix after the given position.
+ *
+ * @param builder the string builder to check, not null.
+ * @param pos the position to start checking from.
+ * @param offset the start offset within the builder, must be valid.
+ * @param bufEnd the end offset within the builder, must be valid.
+ * @param suffixMatcher the suffix matcher to use, not null.
+ * @return true if a suffix is found after the given position.
+ */
+ private boolean hasLaterSuffix(final TextStringBuilder builder, int pos, final int offset, final int bufEnd, final StringMatcher suffixMatcher) {
+ while (pos < bufEnd) {
+ if (suffixMatcher.isMatch(builder, pos, offset, bufEnd) != 0) {
+ return true;
+ }
+ pos++;
+ }
+ return false;
+ }
+
/**
* Returns a string representation of the object.
*
diff --git a/src/test/java/org/apache/commons/text/StringSubstitutorTest.java b/src/test/java/org/apache/commons/text/StringSubstitutorTest.java
index c339189855..a6fdf45297 100644
--- a/src/test/java/org/apache/commons/text/StringSubstitutorTest.java
+++ b/src/test/java/org/apache/commons/text/StringSubstitutorTest.java
@@ -38,7 +38,6 @@
import org.apache.commons.text.matcher.StringMatcherFactory;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
@@ -284,7 +283,6 @@ void testReplace_JiraText178_WeirdPatterns2() throws IOException {
* Tests interpolation with weird boundary patterns.
*/
@Test
- @Disabled
void testReplace_JiraText178_WeirdPatterns3() throws IOException {
doReplace("${${a}", "$${${a}", false); // not "$${1" or "${1"
}