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" }