in src/main/java/org/apache/commons/text/StringSubstitutor.java [1411:1554]
private Result substitute(final TextStringBuilder builder, final int offset, final int length,
List<String> priorVariables) {
Objects.requireNonNull(builder, "builder");
final StringMatcher prefixMatcher = getVariablePrefixMatcher();
final StringMatcher suffixMatcher = getVariableSuffixMatcher();
final char escapeCh = getEscapeChar();
final StringMatcher valueDelimMatcher = getValueDelimiterMatcher();
final boolean substitutionInVariablesEnabled = isEnableSubstitutionInVariables();
final boolean substitutionInValuesDisabled = isDisableSubstitutionInValues();
final boolean undefinedVariableException = isEnableUndefinedVariableException();
final boolean preserveEscapes = isPreserveEscapes();
boolean altered = false;
int lengthChange = 0;
int bufEnd = offset + length;
int pos = offset;
int escPos = -1;
outer: while (pos < bufEnd) {
final int startMatchLen = prefixMatcher.isMatch(builder, pos, offset, bufEnd);
if (startMatchLen == 0) {
pos++;
} else {
// found variable start marker
if (pos > offset && builder.charAt(pos - 1) == escapeCh) {
// escape detected
if (preserveEscapes) {
// keep escape
pos++;
continue;
}
// mark esc ch for deletion if we find a complete variable
escPos = pos - 1;
}
// find suffix
int startPos = pos;
pos += startMatchLen;
int endMatchLen = 0;
int nestedVarCount = 0;
while (pos < bufEnd) {
if (substitutionInVariablesEnabled && prefixMatcher.isMatch(builder, pos, offset, bufEnd) != 0) {
// found a nested variable start
endMatchLen = prefixMatcher.isMatch(builder, pos, offset, bufEnd);
nestedVarCount++;
pos += endMatchLen;
continue;
}
endMatchLen = suffixMatcher.isMatch(builder, pos, offset, bufEnd);
if (endMatchLen == 0) {
pos++;
} else {
// found variable end marker
if (nestedVarCount == 0) {
if (escPos >= 0) {
// 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);
if (substitutionInVariablesEnabled) {
final TextStringBuilder bufName = new TextStringBuilder(varNameExpr);
substitute(bufName, 0, bufName.length());
varNameExpr = bufName.toString();
}
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) {
break;
}
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) {
varValue = varDefaultValue;
}
if (varValue != null) {
final int varLen = varValue.length();
builder.replace(startPos, endPos, varValue);
altered = true;
int change = 0;
if (!substitutionInValuesDisabled) { // recursive replace
change = substitute(builder, startPos, varLen, priorVariables).lengthChange;
}
change = change + varLen - (endPos - startPos);
pos += change;
bufEnd += change;
lengthChange += change;
} else if (undefinedVariableException) {
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;
}
nestedVarCount--;
pos += endMatchLen;
}
}
}
}
return new Result(altered, lengthChange);
}