    001/*
    002 * Licensed to the Apache Software Foundation (ASF) under one or more
    003 * contributor license agreements.  See the NOTICE file distributed with
    004 * this work for additional information regarding copyright ownership.
    005 * The ASF licenses this file to You under the Apache License, Version 2.0
    006 * (the "License"); you may not use this file except in compliance with
    007 * the License.  You may obtain a copy of the License at
    008 *
    009 *      http://www.apache.org/licenses/LICENSE-2.0
    010 *
    011 * Unless required by applicable law or agreed to in writing, software
    012 * distributed under the License is distributed on an "AS IS" BASIS,
    013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    014 * See the License for the specific language governing permissions and
    015 * limitations under the License.
    016 */
    017package org.apache.commons.text;
    018
    019import java.util.ArrayList;
    020import java.util.Enumeration;
    021import java.util.HashMap;
    022import java.util.List;
    023import java.util.Map;
    024import java.util.Properties;
    025
    026import org.apache.commons.lang3.Validate;
    027import org.apache.commons.text.lookup.StringLookup;
    028import org.apache.commons.text.lookup.StringLookupFactory;
    029import org.apache.commons.text.matcher.StringMatcher;
    030import org.apache.commons.text.matcher.StringMatcherFactory;
    031
    032/**
    033 * Substitutes variables within a string by values.
    034 * <p>
    035 * This class takes a piece of text and substitutes all the variables within it. The default definition of a variable is
    036 * <code>${variableName}</code>. The prefix and suffix can be changed via constructors and set methods.
    037 * <p>
    038 * Variable values are typically resolved from a map, but could also be resolved from system properties, or by supplying
    039 * a custom variable resolver.
    040 * <p>
    041 * The simplest example is to use this class to replace Java System properties. For example:
    042 *
    043 * <pre>
    044 * StringSubstitutor
    045 *         .replaceSystemProperties("You are running with java.version = ${java.version} and os.name = ${os.name}.");
    046 * </pre>
    047 * <p>
    048 * Typical usage of this class follows the following pattern: First an instance is created and initialized with the map
    049 * that contains the values for the available variables. If a prefix and/or suffix for variables should be used other
    050 * than the default ones, the appropriate settings can be performed. After that the <code>replace()</code> method can be
    051 * called passing in the source text for interpolation. In the returned text all variable references (as long as their
    052 * values are known) will be resolved. The following example demonstrates this:
    053 *
    054 * <pre>
    055 * Map valuesMap = HashMap();
    056 * valuesMap.put(&quot;animal&quot;, &quot;quick brown fox&quot;);
    057 * valuesMap.put(&quot;target&quot;, &quot;lazy dog&quot;);
    058 * String templateString = &quot;The ${animal} jumped over the ${target}.&quot;;
    059 * StringSubstitutor sub = new StringSubstitutor(valuesMap);
    060 * String resolvedString = sub.replace(templateString);
    061 * </pre>
    062 *
    063 * yielding:
    064 *
    065 * <pre>
    066 *      The quick brown fox jumped over the lazy dog.
    067 * </pre>
    068 * <p>
    069 * Also, this class allows to set a default value for unresolved variables. The default value for a variable can be
    070 * appended to the variable name after the variable default value delimiter. The default value of the variable default
    071 * value delimiter is ':-', as in bash and other *nix shells, as those are arguably where the default ${} delimiter set
    072 * originated. The variable default value delimiter can be manually set by calling
    073 * {@link #setValueDelimiterMatcher(StringMatcher)}, {@link #setValueDelimiter(char)} or
    074 * {@link #setValueDelimiter(String)}. The following shows an example with variable default value settings:
    075 *
    076 * <pre>
    077 * Map valuesMap = HashMap();
    078 * valuesMap.put(&quot;animal&quot;, &quot;quick brown fox&quot;);
    079 * valuesMap.put(&quot;target&quot;, &quot;lazy dog&quot;);
    080 * String templateString = &quot;The ${animal} jumped over the ${target}. ${undefined.number:-1234567890}.&quot;;
    081 * StringSubstitutor sub = new StringSubstitutor(valuesMap);
    082 * String resolvedString = sub.replace(templateString);
    083 * </pre>
    084 *
    085 * yielding:
    086 *
    087 * <pre>
    088 *      The quick brown fox jumped over the lazy dog. 1234567890.
    089 * </pre>
    090 * <p>
    091 * In addition to this usage pattern there are some static convenience methods that cover the most common use cases.
    092 * These methods can be used without the need of manually creating an instance. However if multiple replace operations
    093 * are to be performed, creating and reusing an instance of this class will be more efficient.
    094 * <p>
    095 * Variable replacement works in a recursive way. Thus, if a variable value contains a variable then that variable will
    096 * also be replaced. Cyclic replacements are detected and will cause an exception to be thrown.
    097 * <p>
    098 * Sometimes the interpolation's result must contain a variable prefix. As an example take the following source text:
    099 *
    100 * <pre>
    101 *   The variable ${${name}} must be used.
    102 * </pre>
    103 *
    104 * Here only the variable's name referred to in the text should be replaced resulting in the text (assuming that the
    105 * value of the <code>name</code> variable is <code>x</code>):
    106 *
    107 * <pre>
    108 *   The variable ${x} must be used.
    109 * </pre>
    110 *
    111 * To achieve this effect there are two possibilities: Either set a different prefix and suffix for variables which do
    112 * not conflict with the result text you want to produce. The other possibility is to use the escape character, by
    113 * default '$'. If this character is placed before a variable reference, this reference is ignored and won't be
    114 * replaced. For example:
    115 *
    116 * <pre>
    117 *   The variable $${${name}} must be used.
    118 * </pre>
    119 * <p>
    120 * In some complex scenarios you might even want to perform substitution in the names of variables, for instance
    121 *
    122 * <pre>
    123 * ${jre-${java.specification.version}}
    124 * </pre>
    125 *
    126 * <code>StringSubstitutor</code> supports this recursive substitution in variable names, but it has to be enabled
    127 * explicitly by setting the {@link #setEnableSubstitutionInVariables(boolean) enableSubstitutionInVariables} property
    128 * to <b>true</b>.
    129 * <p>
    130 * This class is <b>not</b> thread safe.
    131 * </p>
    132 *
    133 * @since 1.3
    134 */
    135public class StringSubstitutor {
    136
    137    /**
    138     * The default variable default separator.
    139     *
    140     * @since 1.5.
    141     */
    142    public static final String DEFAULT_VAR_DEFAULT = ":-";
    143
    144    /**
    145     * The default variable end separator.
    146     *
    147     * @since 1.5.
    148     */
    149    public static final String DEFAULT_VAR_END = "}";
    150
    151    /**
    152     * The default variable start separator.
    153     *
    154     * @since 1.5.
    155     */
    156    public static final String DEFAULT_VAR_START = "${";
    157
    158    /**
    159     * Constant for the default escape character.
    160     */
    161    public static final char DEFAULT_ESCAPE = '$';
    162
    163    /**
    164     * Constant for the default variable prefix.
    165     */
    166    public static final StringMatcher DEFAULT_PREFIX = StringMatcherFactory.INSTANCE.stringMatcher(DEFAULT_VAR_START);
    167
    168    /**
    169     * Constant for the default variable suffix.
    170     */
    171    public static final StringMatcher DEFAULT_SUFFIX = StringMatcherFactory.INSTANCE.stringMatcher(DEFAULT_VAR_END);
    172
    173    /**
    174     * Constant for the default value delimiter of a variable.
    175     */
    176    public static final StringMatcher DEFAULT_VALUE_DELIMITER =
    177            StringMatcherFactory.INSTANCE.stringMatcher(DEFAULT_VAR_DEFAULT);
    178
    179    // -----------------------------------------------------------------------
    180    /**
    181     * Replaces all the occurrences of variables in the given source object with their matching values from the map.
    182     *
    183     * @param <V>
    184     *            the type of the values in the map
    185     * @param source
    186     *            the source text containing the variables to substitute, null returns null
    187     * @param valueMap
    188     *            the map with the values, may be null
    189     * @return the result of the replace operation
    190     */
    191    public static <V> String replace(final Object source, final Map<String, V> valueMap) {
    192        return new StringSubstitutor(valueMap).replace(source);
    193    }
    194
    195    /**
    196     * Replaces all the occurrences of variables in the given source object with their matching values from the map.
    197     * This method allows to specify a custom variable prefix and suffix
    198     *
    199     * @param <V>
    200     *            the type of the values in the map
    201     * @param source
    202     *            the source text containing the variables to substitute, null returns null
    203     * @param valueMap
    204     *            the map with the values, may be null
    205     * @param prefix
    206     *            the prefix of variables, not null
    207     * @param suffix
    208     *            the suffix of variables, not null
    209     * @return the result of the replace operation
    210     * @throws IllegalArgumentException
    211     *             if the prefix or suffix is null
    212     */
    213    public static <V> String replace(final Object source, final Map<String, V> valueMap, final String prefix,
    214            final String suffix) {
    215        return new StringSubstitutor(valueMap, prefix, suffix).replace(source);
    216    }
    217
    218    /**
    219     * Replaces all the occurrences of variables in the given source object with their matching values from the
    220     * properties.
    221     *
    222     * @param source
    223     *            the source text containing the variables to substitute, null returns null
    224     * @param valueProperties
    225     *            the properties with values, may be null
    226     * @return the result of the replace operation
    227     */
    228    public static String replace(final Object source, final Properties valueProperties) {
    229        if (valueProperties == null) {
    230            return source.toString();
    231        }
    232        final Map<String, String> valueMap = new HashMap<>();
    233        final Enumeration<?> propNames = valueProperties.propertyNames();
    234        while (propNames.hasMoreElements()) {
    235            final String propName = (String) propNames.nextElement();
    236            final String propValue = valueProperties.getProperty(propName);
    237            valueMap.put(propName, propValue);
    238        }
    239        return StringSubstitutor.replace(source, valueMap);
    240    }
    241
    242    /**
    243     * Replaces all the occurrences of variables in the given source object with their matching values from the system
    244     * properties.
    245     *
    246     * @param source
    247     *            the source text containing the variables to substitute, null returns null
    248     * @return the result of the replace operation
    249     */
    250    public static String replaceSystemProperties(final Object source) {
    251        return new StringSubstitutor(StringLookupFactory.INSTANCE.systemPropertyStringLookup()).replace(source);
    252    }
    253
    254    /**
    255     * Stores the escape character.
    256     */
    257    private char escapeChar;
    258
    259    /**
    260     * Stores the variable prefix.
    261     */
    262    private StringMatcher prefixMatcher;
    263
    264    /**
    265     * Stores the variable suffix.
    266     */
    267    private StringMatcher suffixMatcher;
    268
    269    /**
    270     * Stores the default variable value delimiter.
    271     */
    272    private StringMatcher valueDelimiterMatcher;
    273
    274    /**
    275     * Variable resolution is delegated to an implementor of {@link StringLookup}.
    276     */
    277    private StringLookup variableResolver;
    278
    279    /**
    280     * The flag whether substitution in variable names is enabled.
    281     */
    282    private boolean enableSubstitutionInVariables;
    283
    284    /**
    285     * Whether escapes should be preserved. Default is false;
    286     */
    287    private boolean preserveEscapes = false;
    288
    289    /**
    290     * The flag whether substitution in variable values is disabled.
    291     */
    292    private boolean disableSubstitutionInValues;
    293
    294    // -----------------------------------------------------------------------
    295    /**
    296     * Creates a new instance with defaults for variable prefix and suffix and the escaping character.
    297     */
    298    public StringSubstitutor() {
    299        this((StringLookup) null, DEFAULT_PREFIX, DEFAULT_SUFFIX, DEFAULT_ESCAPE);
    300    }
    301
    302    /**
    303     * Creates a new instance and initializes it. Uses defaults for variable prefix and suffix and the escaping
    304     * character.
    305     *
    306     * @param <V>
    307     *            the type of the values in the map
    308     * @param valueMap
    309     *            the map with the variables' values, may be null
    310     */
    311    public <V> StringSubstitutor(final Map<String, V> valueMap) {
    312        this(StringLookupFactory.INSTANCE.mapStringLookup(valueMap), DEFAULT_PREFIX, DEFAULT_SUFFIX, DEFAULT_ESCAPE);
    313    }
    314
    315    /**
    316     * Creates a new instance and initializes it. Uses a default escaping character.
    317     *
    318     * @param <V>
    319     *            the type of the values in the map
    320     * @param valueMap
    321     *            the map with the variables' values, may be null
    322     * @param prefix
    323     *            the prefix for variables, not null
    324     * @param suffix
    325     *            the suffix for variables, not null
    326     * @throws IllegalArgumentException
    327     *             if the prefix or suffix is null
    328     */
    329    public <V> StringSubstitutor(final Map<String, V> valueMap, final String prefix, final String suffix) {
    330        this(StringLookupFactory.INSTANCE.mapStringLookup(valueMap), prefix, suffix, DEFAULT_ESCAPE);
    331    }
    332
    333    /**
    334     * Creates a new instance and initializes it.
    335     *
    336     * @param <V>
    337     *            the type of the values in the map
    338     * @param valueMap
    339     *            the map with the variables' values, may be null
    340     * @param prefix
    341     *            the prefix for variables, not null
    342     * @param suffix
    343     *            the suffix for variables, not null
    344     * @param escape
    345     *            the escape character
    346     * @throws IllegalArgumentException
    347     *             if the prefix or suffix is null
    348     */
    349    public <V> StringSubstitutor(final Map<String, V> valueMap, final String prefix, final String suffix,
    350            final char escape) {
    351        this(StringLookupFactory.INSTANCE.mapStringLookup(valueMap), prefix, suffix, escape);
    352    }
    353
    354    /**
    355     * Creates a new instance and initializes it.
    356     *
    357     * @param <V>
    358     *            the type of the values in the map
    359     * @param valueMap
    360     *            the map with the variables' values, may be null
    361     * @param prefix
    362     *            the prefix for variables, not null
    363     * @param suffix
    364     *            the suffix for variables, not null
    365     * @param escape
    366     *            the escape character
    367     * @param valueDelimiter
    368     *            the variable default value delimiter, may be null
    369     * @throws IllegalArgumentException
    370     *             if the prefix or suffix is null
    371     */
    372    public <V> StringSubstitutor(final Map<String, V> valueMap, final String prefix, final String suffix,
    373            final char escape, final String valueDelimiter) {
    374        this(StringLookupFactory.INSTANCE.mapStringLookup(valueMap), prefix, suffix, escape, valueDelimiter);
    375    }
    376
    377    /**
    378     * Creates a new instance and initializes it.
    379     *
    380     * @param variableResolver
    381     *            the variable resolver, may be null
    382     */
    383    public StringSubstitutor(final StringLookup variableResolver) {
    384        this(variableResolver, DEFAULT_PREFIX, DEFAULT_SUFFIX, DEFAULT_ESCAPE);
    385    }
    386
    387    /**
    388     * Creates a new instance and initializes it.
    389     *
    390     * @param variableResolver
    391     *            the variable resolver, may be null
    392     * @param prefix
    393     *            the prefix for variables, not null
    394     * @param suffix
    395     *            the suffix for variables, not null
    396     * @param escape
    397     *            the escape character
    398     * @throws IllegalArgumentException
    399     *             if the prefix or suffix is null
    400     */
    401    public StringSubstitutor(final StringLookup variableResolver, final String prefix, final String suffix,
    402            final char escape) {
    403        this.setVariableResolver(variableResolver);
    404        this.setVariablePrefix(prefix);
    405        this.setVariableSuffix(suffix);
    406        this.setEscapeChar(escape);
    407        this.setValueDelimiterMatcher(DEFAULT_VALUE_DELIMITER);
    408    }
    409
    410    /**
    411     * Creates a new instance and initializes it.
    412     *
    413     * @param variableResolver
    414     *            the variable resolver, may be null
    415     * @param prefix
    416     *            the prefix for variables, not null
    417     * @param suffix
    418     *            the suffix for variables, not null
    419     * @param escape
    420     *            the escape character
    421     * @param valueDelimiter
    422     *            the variable default value delimiter string, may be null
    423     * @throws IllegalArgumentException
    424     *             if the prefix or suffix is null
    425     */
    426    public StringSubstitutor(final StringLookup variableResolver, final String prefix, final String suffix,
    427            final char escape, final String valueDelimiter) {
    428        this.setVariableResolver(variableResolver);
    429        this.setVariablePrefix(prefix);
    430        this.setVariableSuffix(suffix);
    431        this.setEscapeChar(escape);
    432        this.setValueDelimiter(valueDelimiter);
    433    }
    434
    435    /**
    436     * Creates a new instance and initializes it.
    437     *
    438     * @param variableResolver
    439     *            the variable resolver, may be null
    440     * @param prefixMatcher
    441     *            the prefix for variables, not null
    442     * @param suffixMatcher
    443     *            the suffix for variables, not null
    444     * @param escape
    445     *            the escape character
    446     * @throws IllegalArgumentException
    447     *             if the prefix or suffix is null
    448     */
    449    public StringSubstitutor(final StringLookup variableResolver, final StringMatcher prefixMatcher,
    450            final StringMatcher suffixMatcher, final char escape) {
    451        this(variableResolver, prefixMatcher, suffixMatcher, escape, DEFAULT_VALUE_DELIMITER);
    452    }
    453
    454    /**
    455     * Creates a new instance and initializes it.
    456     *
    457     * @param variableResolver
    458     *            the variable resolver, may be null
    459     * @param prefixMatcher
    460     *            the prefix for variables, not null
    461     * @param suffixMatcher
    462     *            the suffix for variables, not null
    463     * @param escape
    464     *            the escape character
    465     * @param valueDelimiterMatcher
    466     *            the variable default value delimiter matcher, may be null
    467     * @throws IllegalArgumentException
    468     *             if the prefix or suffix is null
    469     */
    470    public StringSubstitutor(final StringLookup variableResolver, final StringMatcher prefixMatcher,
    471            final StringMatcher suffixMatcher, final char escape, final StringMatcher valueDelimiterMatcher) {
    472        this.setVariableResolver(variableResolver);
    473        this.setVariablePrefixMatcher(prefixMatcher);
    474        this.setVariableSuffixMatcher(suffixMatcher);
    475        this.setEscapeChar(escape);
    476        this.setValueDelimiterMatcher(valueDelimiterMatcher);
    477    }
    478
    479    /**
    480     * Checks if the specified variable is already in the stack (list) of variables.
    481     *
    482     * @param varName
    483     *            the variable name to check
    484     * @param priorVariables
    485     *            the list of prior variables
    486     */
    487    private void checkCyclicSubstitution(final String varName, final List<String> priorVariables) {
    488        if (!priorVariables.contains(varName)) {
    489            return;
    490        }
    491        final TextStringBuilder buf = new TextStringBuilder(256);
    492        buf.append("Infinite loop in property interpolation of ");
    493        buf.append(priorVariables.remove(0));
    494        buf.append(": ");
    495        buf.appendWithSeparators(priorVariables, "->");
    496        throw new IllegalStateException(buf.toString());
    497    }
    498
    499    // Escape
    500    // -----------------------------------------------------------------------
    501    /**
    502     * Returns the escape character.
    503     *
    504     * @return the character used for escaping variable references
    505     */
    506    public char getEscapeChar() {
    507        return this.escapeChar;
    508    }
    509
    510    // Resolver
    511    // -----------------------------------------------------------------------
    512    /**
    513     * Gets the StringLookup that is used to lookup variables.
    514     *
    515     * @return the StringLookup
    516     */
    517    public StringLookup getStringLookup() {
    518        return this.variableResolver;
    519    }
    520
    521    // Variable Default Value Delimiter
    522    // -----------------------------------------------------------------------
    523    /**
    524     * Gets the variable default value delimiter matcher currently in use.
    525     * <p>
    526     * The variable default value delimiter is the character or characters that delimite the variable name and the
    527     * variable default value. This delimiter is expressed in terms of a matcher allowing advanced variable default
    528     * value delimiter matches.
    529     * <p>
    530     * If it returns null, then the variable default value resolution is disabled.
    531     *
    532     * @return the variable default value delimiter matcher in use, may be null
    533     */
    534    public StringMatcher getValueDelimiterMatcher() {
    535        return valueDelimiterMatcher;
    536    }
    537
    538    // Prefix
    539    // -----------------------------------------------------------------------
    540    /**
    541     * Gets the variable prefix matcher currently in use.
    542     * <p>
    543     * The variable prefix is the character or characters that identify the start of a variable. This prefix is
    544     * expressed in terms of a matcher allowing advanced prefix matches.
    545     *
    546     * @return the prefix matcher in use
    547     */
    548    public StringMatcher getVariablePrefixMatcher() {
    549        return prefixMatcher;
    550    }
    551
    552    // Suffix
    553    // -----------------------------------------------------------------------
    554    /**
    555     * Gets the variable suffix matcher currently in use.
    556     * <p>
    557     * The variable suffix is the character or characters that identify the end of a variable. This suffix is expressed
    558     * in terms of a matcher allowing advanced suffix matches.
    559     *
    560     * @return the suffix matcher in use
    561     */
    562    public StringMatcher getVariableSuffixMatcher() {
    563        return suffixMatcher;
    564    }
    565
    566    /**
    567     * Returns a flag whether substitution is disabled in variable values.If set to <b>true</b>, the values of variables
    568     * can contain other variables will not be processed and substituted original variable is evaluated, e.g.
    569     *
    570     * <pre>
    571     * Map valuesMap = HashMap();
    572     * valuesMap.put(&quot;name&quot;, &quot;Douglas ${surname}&quot;);
    573     * valuesMap.put(&quot;surname&quot;, &quot;Crockford&quot;);
    574     * String templateString = &quot;Hi ${name}&quot;;
    575     * StrSubstitutor sub = new StrSubstitutor(valuesMap);
    576     * String resolvedString = sub.replace(templateString);
    577     * </pre>
    578     *
    579     * yielding:
    580     *
    581     * <pre>
    582     *      Hi Douglas ${surname}
    583     * </pre>
    584     *
    585     * @return the substitution in variable values flag
    586     */
    587    public boolean isDisableSubstitutionInValues() {
    588        return disableSubstitutionInValues;
    589    }
    590
    591    // Substitution support in variable names
    592    // -----------------------------------------------------------------------
    593    /**
    594     * Returns a flag whether substitution is done in variable names.
    595     *
    596     * @return the substitution in variable names flag
    597     */
    598    public boolean isEnableSubstitutionInVariables() {
    599        return enableSubstitutionInVariables;
    600    }
    601
    602    /**
    603     * Returns the flag controlling whether escapes are preserved during substitution.
    604     *
    605     * @return the preserve escape flag
    606     */
    607    public boolean isPreserveEscapes() {
    608        return preserveEscapes;
    609    }
    610
    611    // -----------------------------------------------------------------------
    612    /**
    613     * Replaces all the occurrences of variables with their matching values from the resolver using the given source
    614     * array as a template. The array is not altered by this method.
    615     *
    616     * @param source
    617     *            the character array to replace in, not altered, null returns null
    618     * @return the result of the replace operation
    619     */
    620    public String replace(final char[] source) {
    621        if (source == null) {
    622            return null;
    623        }
    624        final TextStringBuilder buf = new TextStringBuilder(source.length).append(source);
    625        substitute(buf, 0, source.length);
    626        return buf.toString();
    627    }
    628
    629    /**
    630     * Replaces all the occurrences of variables with their matching values from the resolver using the given source
    631     * array as a template. The array is not altered by this method.
    632     * <p>
    633     * Only the specified portion of the array will be processed. The rest of the array is not processed, and is not
    634     * returned.
    635     *
    636     * @param source
    637     *            the character array to replace in, not altered, null returns null
    638     * @param offset
    639     *            the start offset within the array, must be valid
    640     * @param length
    641     *            the length within the array to be processed, must be valid
    642     * @return the result of the replace operation
    643     */
    644    public String replace(final char[] source, final int offset, final int length) {
    645        if (source == null) {
    646            return null;
    647        }
    648        final TextStringBuilder buf = new TextStringBuilder(length).append(source, offset, length);
    649        substitute(buf, 0, length);
    650        return buf.toString();
    651    }
    652
    653    /**
    654     * Replaces all the occurrences of variables with their matching values from the resolver using the given source as
    655     * a template. The source is not altered by this method.
    656     *
    657     * @param source
    658     *            the buffer to use as a template, not changed, null returns null
    659     * @return the result of the replace operation
    660     */
    661    public String replace(final CharSequence source) {
    662        if (source == null) {
    663            return null;
    664        }
    665        return replace(source, 0, source.length());
    666    }
    667
    668    /**
    669     * Replaces all the occurrences of variables with their matching values from the resolver using the given source as
    670     * a template. The source is not altered by this method.
    671     * <p>
    672     * Only the specified portion of the buffer will be processed. The rest of the buffer is not processed, and is not
    673     * returned.
    674     *
    675     * @param source
    676     *            the buffer to use as a template, not changed, null returns null
    677     * @param offset
    678     *            the start offset within the array, must be valid
    679     * @param length
    680     *            the length within the array to be processed, must be valid
    681     * @return the result of the replace operation
    682     */
    683    public String replace(final CharSequence source, final int offset, final int length) {
    684        if (source == null) {
    685            return null;
    686        }
    687        final TextStringBuilder buf = new TextStringBuilder(length).append(source, offset, length);
    688        substitute(buf, 0, length);
    689        return buf.toString();
    690    }
    691
    692    // -----------------------------------------------------------------------
    693    /**
    694     * Replaces all the occurrences of variables in the given source object with their matching values from the
    695     * resolver. The input source object is converted to a string using <code>toString</code> and is not altered.
    696     *
    697     * @param source
    698     *            the source to replace in, null returns null
    699     * @return the result of the replace operation
    700     */
    701    public String replace(final Object source) {
    702        if (source == null) {
    703            return null;
    704        }
    705        final TextStringBuilder buf = new TextStringBuilder().append(source);
    706        substitute(buf, 0, buf.length());
    707        return buf.toString();
    708    }
    709
    710    // -----------------------------------------------------------------------
    711    /**
    712     * Replaces all the occurrences of variables with their matching values from the resolver using the given source
    713     * builder as a template. The builder is not altered by this method.
    714     *
    715     * @param source
    716     *            the builder to use as a template, not changed, null returns null
    717     * @return the result of the replace operation
    718     */
    719    public String replace(final TextStringBuilder source) {
    720        if (source == null) {
    721            return null;
    722        }
    723        final TextStringBuilder buf = new TextStringBuilder(source.length()).append(source);
    724        substitute(buf, 0, buf.length());
    725        return buf.toString();
    726    }
    727
    728    /**
    729     * Replaces all the occurrences of variables with their matching values from the resolver using the given source
    730     * builder as a template. The builder is not altered by this method.
    731     * <p>
    732     * Only the specified portion of the builder will be processed. The rest of the builder is not processed, and is not
    733     * returned.
    734     *
    735     * @param source
    736     *            the builder to use as a template, not changed, null returns null
    737     * @param offset
    738     *            the start offset within the array, must be valid
    739     * @param length
    740     *            the length within the array to be processed, must be valid
    741     * @return the result of the replace operation
    742     */
    743    public String replace(final TextStringBuilder source, final int offset, final int length) {
    744        if (source == null) {
    745            return null;
    746        }
    747        final TextStringBuilder buf = new TextStringBuilder(length).append(source, offset, length);
    748        substitute(buf, 0, length);
    749        return buf.toString();
    750    }
    751
    752    // -----------------------------------------------------------------------
    753    /**
    754     * Replaces all the occurrences of variables with their matching values from the resolver using the given source
    755     * string as a template.
    756     *
    757     * @param source
    758     *            the string to replace in, null returns null
    759     * @return the result of the replace operation
    760     */
    761    public String replace(final String source) {
    762        if (source == null) {
    763            return null;
    764        }
    765        final TextStringBuilder buf = new TextStringBuilder(source);
    766        if (!substitute(buf, 0, source.length())) {
    767            return source;
    768        }
    769        return buf.toString();
    770    }
    771
    772    /**
    773     * Replaces all the occurrences of variables with their matching values from the resolver using the given source
    774     * string as a template.
    775     * <p>
    776     * Only the specified portion of the string will be processed. The rest of the string is not processed, and is not
    777     * returned.
    778     *
    779     * @param source
    780     *            the string to replace in, null returns null
    781     * @param offset
    782     *            the start offset within the array, must be valid
    783     * @param length
    784     *            the length within the array to be processed, must be valid
    785     * @return the result of the replace operation
    786     */
    787    public String replace(final String source, final int offset, final int length) {
    788        if (source == null) {
    789            return null;
    790        }
    791        final TextStringBuilder buf = new TextStringBuilder(length).append(source, offset, length);
    792        if (!substitute(buf, 0, length)) {
    793            return source.substring(offset, offset + length);
    794        }
    795        return buf.toString();
    796    }
    797
    798    // -----------------------------------------------------------------------
    799    /**
    800     * Replaces all the occurrences of variables with their matching values from the resolver using the given source
    801     * buffer as a template. The buffer is not altered by this method.
    802     *
    803     * @param source
    804     *            the buffer to use as a template, not changed, null returns null
    805     * @return the result of the replace operation
    806     */
    807    public String replace(final StringBuffer source) {
    808        if (source == null) {
    809            return null;
    810        }
    811        final TextStringBuilder buf = new TextStringBuilder(source.length()).append(source);
    812        substitute(buf, 0, buf.length());
    813        return buf.toString();
    814    }
    815
    816    /**
    817     * Replaces all the occurrences of variables with their matching values from the resolver using the given source
    818     * buffer as a template. The buffer is not altered by this method.
    819     * <p>
    820     * Only the specified portion of the buffer will be processed. The rest of the buffer is not processed, and is not
    821     * returned.
    822     *
    823     * @param source
    824     *            the buffer to use as a template, not changed, null returns null
    825     * @param offset
    826     *            the start offset within the array, must be valid
    827     * @param length
    828     *            the length within the array to be processed, must be valid
    829     * @return the result of the replace operation
    830     */
    831    public String replace(final StringBuffer source, final int offset, final int length) {
    832        if (source == null) {
    833            return null;
    834        }
    835        final TextStringBuilder buf = new TextStringBuilder(length).append(source, offset, length);
    836        substitute(buf, 0, length);
    837        return buf.toString();
    838    }
    839
    840    // -----------------------------------------------------------------------
    841    /**
    842     * Replaces all the occurrences of variables within the given source builder with their matching values from the
    843     * resolver.
    844     *
    845     * @param source
    846     *            the builder to replace in, updated, null returns zero
    847     * @return true if altered
    848     */
    849    public boolean replaceIn(final TextStringBuilder source) {
    850        if (source == null) {
    851            return false;
    852        }
    853        return substitute(source, 0, source.length());
    854    }
    855
    856    /**
    857     * Replaces all the occurrences of variables within the given source builder with their matching values from the
    858     * resolver.
    859     * <p>
    860     * Only the specified portion of the builder will be processed. The rest of the builder is not processed, but it is
    861     * not deleted.
    862     *
    863     * @param source
    864     *            the builder to replace in, null returns zero
    865     * @param offset
    866     *            the start offset within the array, must be valid
    867     * @param length
    868     *            the length within the builder to be processed, must be valid
    869     * @return true if altered
    870     */
    871    public boolean replaceIn(final TextStringBuilder source, final int offset, final int length) {
    872        if (source == null) {
    873            return false;
    874        }
    875        return substitute(source, offset, length);
    876    }
    877
    878    // -----------------------------------------------------------------------
    879    /**
    880     * Replaces all the occurrences of variables within the given source buffer with their matching values from the
    881     * resolver. The buffer is updated with the result.
    882     *
    883     * @param source
    884     *            the buffer to replace in, updated, null returns zero
    885     * @return true if altered
    886     */
    887    public boolean replaceIn(final StringBuffer source) {
    888        if (source == null) {
    889            return false;
    890        }
    891        return replaceIn(source, 0, source.length());
    892    }
    893
    894    /**
    895     * Replaces all the occurrences of variables within the given source buffer with their matching values from the
    896     * resolver. The buffer is updated with the result.
    897     * <p>
    898     * Only the specified portion of the buffer will be processed. The rest of the buffer is not processed, but it is
    899     * not deleted.
    900     *
    901     * @param source
    902     *            the buffer to replace in, updated, null returns zero
    903     * @param offset
    904     *            the start offset within the array, must be valid
    905     * @param length
    906     *            the length within the buffer to be processed, must be valid
    907     * @return true if altered
    908     */
    909    public boolean replaceIn(final StringBuffer source, final int offset, final int length) {
    910        if (source == null) {
    911            return false;
    912        }
    913        final TextStringBuilder buf = new TextStringBuilder(length).append(source, offset, length);
    914        if (!substitute(buf, 0, length)) {
    915            return false;
    916        }
    917        source.replace(offset, offset + length, buf.toString());
    918        return true;
    919    }
    920
    921    // -----------------------------------------------------------------------
    922    /**
    923     * Replaces all the occurrences of variables within the given source buffer with their matching values from the
    924     * resolver. The buffer is updated with the result.
    925     *
    926     * @param source
    927     *            the buffer to replace in, updated, null returns zero
    928     * @return true if altered
    929     */
    930    public boolean replaceIn(final StringBuilder source) {
    931        if (source == null) {
    932            return false;
    933        }
    934        return replaceIn(source, 0, source.length());
    935    }
    936
    937    /**
    938     * Replaces all the occurrences of variables within the given source builder with their matching values from the
    939     * resolver. The builder is updated with the result.
    940     * <p>
    941     * Only the specified portion of the buffer will be processed. The rest of the buffer is not processed, but it is
    942     * not deleted.
    943     *
    944     * @param source
    945     *            the buffer to replace in, updated, null returns zero
    946     * @param offset
    947     *            the start offset within the array, must be valid
    948     * @param length
    949     *            the length within the buffer to be processed, must be valid
    950     * @return true if altered
    951     */
    952    public boolean replaceIn(final StringBuilder source, final int offset, final int length) {
    953        if (source == null) {
    954            return false;
    955        }
    956        final TextStringBuilder buf = new TextStringBuilder(length).append(source, offset, length);
    957        if (!substitute(buf, 0, length)) {
    958            return false;
    959        }
    960        source.replace(offset, offset + length, buf.toString());
    961        return true;
    962    }
    963
    964    /**
    965     * Internal method that resolves the value of a variable.
    966     * <p>
    967     * Most users of this class do not need to call this method. This method is called automatically by the substitution
    968     * process.
    969     * <p>
    970     * Writers of subclasses can override this method if they need to alter how each substitution occurs. The method is
    971     * passed the variable's name and must return the corresponding value. This implementation uses the
    972     * {@link #getStringLookup()} with the variable's name as the key.
    973     *
    974     * @param variableName
    975     *            the name of the variable, not null
    976     * @param buf
    977     *            the buffer where the substitution is occurring, not null
    978     * @param startPos
    979     *            the start position of the variable including the prefix, valid
    980     * @param endPos
    981     *            the end position of the variable including the suffix, valid
    982     * @return the variable's value or <b>null</b> if the variable is unknown
    983     */
    984    protected String resolveVariable(final String variableName, final TextStringBuilder buf, final int startPos,
    985            final int endPos) {
    986        final StringLookup resolver = getStringLookup();
    987        if (resolver == null) {
    988            return null;
    989        }
    990        return resolver.lookup(variableName);
    991    }
    992
    993    /**
    994     * Sets a flag whether substitution is done in variable values (recursive).
    995     *
    996     * @param disableSubstitutionInValues
    997     *            true if substitution in variable value are disabled
    998     * @return this, to enable chaining
    999     */
    1000    public StringSubstitutor setDisableSubstitutionInValues(final boolean disableSubstitutionInValues) {
    1001        this.disableSubstitutionInValues = disableSubstitutionInValues;
    1002        return this;
    1003    }
    1004
    1005    /**
    1006     * Sets a flag whether substitution is done in variable names. If set to <b>true</b>, the names of variables can
    1007     * contain other variables which are processed first before the original variable is evaluated, e.g.
    1008     * <code>${jre-${java.version}}</code>. The default value is <b>false</b>.
    1009     *
    1010     * @param enableSubstitutionInVariables
    1011     *            the new value of the flag
    1012     * @return this, to enable chaining
    1013     */
    1014    public StringSubstitutor setEnableSubstitutionInVariables(final boolean enableSubstitutionInVariables) {
    1015        this.enableSubstitutionInVariables = enableSubstitutionInVariables;
    1016        return this;
    1017    }
    1018
    1019    /**
    1020     * Sets the escape character. If this character is placed before a variable reference in the source text, this
    1021     * variable will be ignored.
    1022     *
    1023     * @param escapeCharacter
    1024     *            the escape character (0 for disabling escaping)
    1025     * @return this, to enable chaining
    1026     */
    1027    public StringSubstitutor setEscapeChar(final char escapeCharacter) {
    1028        this.escapeChar = escapeCharacter;
    1029        return this;
    1030    }
    1031
    1032    /**
    1033     * Sets a flag controlling whether escapes are preserved during substitution. If set to <b>true</b>, the escape
    1034     * character is retained during substitution (e.g. <code>$${this-is-escaped}</code> remains
    1035     * <code>$${this-is-escaped}</code>). If set to <b>false</b>, the escape character is removed during substitution
    1036     * (e.g. <code>$${this-is-escaped}</code> becomes <code>${this-is-escaped}</code>). The default value is
    1037     * <b>false</b>
    1038     *
    1039     * @param preserveEscapes
    1040     *            true if escapes are to be preserved
    1041     * @return this, to enable chaining
    1042     */
    1043    public StringSubstitutor setPreserveEscapes(final boolean preserveEscapes) {
    1044        this.preserveEscapes = preserveEscapes;
    1045        return this;
    1046    }
    1047
    1048    /**
    1049     * Sets the variable default value delimiter to use.
    1050     * <p>
    1051     * The variable default value delimiter is the character or characters that delimite the variable name and the
    1052     * variable default value. This method allows a single character variable default value delimiter to be easily set.
    1053     *
    1054     * @param valueDelimiter
    1055     *            the variable default value delimiter character to use
    1056     * @return this, to enable chaining
    1057     */
    1058    public StringSubstitutor setValueDelimiter(final char valueDelimiter) {
    1059        return setValueDelimiterMatcher(StringMatcherFactory.INSTANCE.charMatcher(valueDelimiter));
    1060    }
    1061
    1062    /**
    1063     * Sets the variable default value delimiter to use.
    1064     * <p>
    1065     * The variable default value delimiter is the character or characters that delimite the variable name and the
    1066     * variable default value. This method allows a string variable default value delimiter to be easily set.
    1067     * <p>
    1068     * If the <code>valueDelimiter</code> is null or empty string, then the variable default value resolution becomes
    1069     * disabled.
    1070     *
    1071     * @param valueDelimiter
    1072     *            the variable default value delimiter string to use, may be null or empty
    1073     * @return this, to enable chaining
    1074     */
    1075    public StringSubstitutor setValueDelimiter(final String valueDelimiter) {
    1076        if (valueDelimiter == null || valueDelimiter.length() == 0) {
    1077            setValueDelimiterMatcher(null);
    1078            return this;
    1079        }
    1080        return setValueDelimiterMatcher(StringMatcherFactory.INSTANCE.stringMatcher(valueDelimiter));
    1081    }
    1082
    1083    /**
    1084     * Sets the variable default value delimiter matcher to use.
    1085     * <p>
    1086     * The variable default value delimiter is the character or characters that delimite the variable name and the
    1087     * variable default value. This delimiter is expressed in terms of a matcher allowing advanced variable default
    1088     * value delimiter matches.
    1089     * <p>
    1090     * If the <code>valueDelimiterMatcher</code> is null, then the variable default value resolution becomes disabled.
    1091     *
    1092     * @param valueDelimiterMatcher
    1093     *            variable default value delimiter matcher to use, may be null
    1094     * @return this, to enable chaining
    1095     */
    1096    public StringSubstitutor setValueDelimiterMatcher(final StringMatcher valueDelimiterMatcher) {
    1097        this.valueDelimiterMatcher = valueDelimiterMatcher;
    1098        return this;
    1099    }
    1100
    1101    /**
    1102     * Sets the variable prefix to use.
    1103     * <p>
    1104     * The variable prefix is the character or characters that identify the start of a variable. This method allows a
    1105     * single character prefix to be easily set.
    1106     *
    1107     * @param prefix
    1108     *            the prefix character to use
    1109     * @return this, to enable chaining
    1110     */
    1111    public StringSubstitutor setVariablePrefix(final char prefix) {
    1112        return setVariablePrefixMatcher(StringMatcherFactory.INSTANCE.charMatcher(prefix));
    1113    }
    1114
    1115    /**
    1116     * Sets the variable prefix to use.
    1117     * <p>
    1118     * The variable prefix is the character or characters that identify the start of a variable. This method allows a
    1119     * string prefix to be easily set.
    1120     *
    1121     * @param prefix
    1122     *            the prefix for variables, not null
    1123     * @return this, to enable chaining
    1124     * @throws IllegalArgumentException
    1125     *             if the prefix is null
    1126     */
    1127    public StringSubstitutor setVariablePrefix(final String prefix) {
    1128        Validate.isTrue(prefix != null, "Variable prefix must not be null!");
    1129        return setVariablePrefixMatcher(StringMatcherFactory.INSTANCE.stringMatcher(prefix));
    1130    }
    1131
    1132    /**
    1133     * Sets the variable prefix matcher currently in use.
    1134     * <p>
    1135     * The variable prefix is the character or characters that identify the start of a variable. This prefix is
    1136     * expressed in terms of a matcher allowing advanced prefix matches.
    1137     *
    1138     * @param prefixMatcher
    1139     *            the prefix matcher to use, null ignored
    1140     * @return this, to enable chaining
    1141     * @throws IllegalArgumentException
    1142     *             if the prefix matcher is null
    1143     */
    1144    public StringSubstitutor setVariablePrefixMatcher(final StringMatcher prefixMatcher) {
    1145        Validate.isTrue(prefixMatcher != null, "Variable prefix matcher must not be null!");
    1146        this.prefixMatcher = prefixMatcher;
    1147        return this;
    1148    }
    1149
    1150    /**
    1151     * Sets the VariableResolver that is used to lookup variables.
    1152     *
    1153     * @param variableResolver
    1154     *            the VariableResolver
    1155     * @return this, to enable chaining
    1156     */
    1157    public StringSubstitutor setVariableResolver(final StringLookup variableResolver) {
    1158        this.variableResolver = variableResolver;
    1159        return this;
    1160    }
    1161
    1162    /**
    1163     * Sets the variable suffix to use.
    1164     * <p>
    1165     * The variable suffix is the character or characters that identify the end of a variable. This method allows a
    1166     * single character suffix to be easily set.
    1167     *
    1168     * @param suffix
    1169     *            the suffix character to use
    1170     * @return this, to enable chaining
    1171     */
    1172    public StringSubstitutor setVariableSuffix(final char suffix) {
    1173        return setVariableSuffixMatcher(StringMatcherFactory.INSTANCE.charMatcher(suffix));
    1174    }
    1175
    1176    /**
    1177     * Sets the variable suffix to use.
    1178     * <p>
    1179     * The variable suffix is the character or characters that identify the end of a variable. This method allows a
    1180     * string suffix to be easily set.
    1181     *
    1182     * @param suffix
    1183     *            the suffix for variables, not null
    1184     * @return this, to enable chaining
    1185     * @throws IllegalArgumentException
    1186     *             if the suffix is null
    1187     */
    1188    public StringSubstitutor setVariableSuffix(final String suffix) {
    1189        Validate.isTrue(suffix != null, "Variable suffix must not be null!");
    1190        return setVariableSuffixMatcher(StringMatcherFactory.INSTANCE.stringMatcher(suffix));
    1191    }
    1192
    1193    /**
    1194     * Sets the variable suffix matcher currently in use.
    1195     * <p>
    1196     * The variable suffix is the character or characters that identify the end of a variable. This suffix is expressed
    1197     * in terms of a matcher allowing advanced suffix matches.
    1198     *
    1199     * @param suffixMatcher
    1200     *            the suffix matcher to use, null ignored
    1201     * @return this, to enable chaining
    1202     * @throws IllegalArgumentException
    1203     *             if the suffix matcher is null
    1204     */
    1205    public StringSubstitutor setVariableSuffixMatcher(final StringMatcher suffixMatcher) {
    1206        Validate.isTrue(suffixMatcher != null, "Variable suffix matcher must not be null!");
    1207        this.suffixMatcher = suffixMatcher;
    1208        return this;
    1209    }
    1210
    1211    // -----------------------------------------------------------------------
    1212    /**
    1213     * Internal method that substitutes the variables.
    1214     * <p>
    1215     * Most users of this class do not need to call this method. This method will be called automatically by another
    1216     * (public) method.
    1217     * <p>
    1218     * Writers of subclasses can override this method if they need access to the substitution process at the start or
    1219     * end.
    1220     *
    1221     * @param buf
    1222     *            the string builder to substitute into, not null
    1223     * @param offset
    1224     *            the start offset within the builder, must be valid
    1225     * @param length
    1226     *            the length within the builder to be processed, must be valid
    1227     * @return true if altered
    1228     */
    1229    protected boolean substitute(final TextStringBuilder buf, final int offset, final int length) {
    1230        return substitute(buf, offset, length, null) > 0;
    1231    }
    1232
    1233    /**
    1234     * Recursive handler for multiple levels of interpolation. This is the main interpolation method, which resolves the
    1235     * values of all variable references contained in the passed in text.
    1236     *
    1237     * @param buf
    1238     *            the string builder to substitute into, not null
    1239     * @param offset
    1240     *            the start offset within the builder, must be valid
    1241     * @param length
    1242     *            the length within the builder to be processed, must be valid
    1243     * @param priorVariables
    1244     *            the stack keeping track of the replaced variables, may be null
    1245     * @return the length change that occurs, unless priorVariables is null when the int represents a boolean flag as to
    1246     *         whether any change occurred.
    1247     */
    1248    private int substitute(final TextStringBuilder buf, final int offset, final int length,
    1249            List<String> priorVariables) {
    1250        final StringMatcher pfxMatcher = getVariablePrefixMatcher();
    1251        final StringMatcher suffMatcher = getVariableSuffixMatcher();
    1252        final char escape = getEscapeChar();
    1253        final StringMatcher valueDelimMatcher = getValueDelimiterMatcher();
    1254        final boolean substitutionInVariablesEnabled = isEnableSubstitutionInVariables();
    1255        final boolean substitutionInValuesDisabled = isDisableSubstitutionInValues();
    1256
    1257        final boolean top = priorVariables == null;
    1258        boolean altered = false;
    1259        int lengthChange = 0;
    1260        char[] chars = buf.buffer;
    1261        int bufEnd = offset + length;
    1262        int pos = offset;
    1263        while (pos < bufEnd) {
    1264            final int startMatchLen = pfxMatcher.isMatch(chars, pos, offset, bufEnd);
    1265            if (startMatchLen == 0) {
    1266                pos++;
    1267            } else {
    1268                // found variable start marker
    1269                if (pos > offset && chars[pos - 1] == escape) {
    1270                    // escaped
    1271                    if (preserveEscapes) {
    1272                        pos++;
    1273                        continue;
    1274                    }
    1275                    buf.deleteCharAt(pos - 1);
    1276                    chars = buf.buffer; // in case buffer was altered
    1277                    lengthChange--;
    1278                    altered = true;
    1279                    bufEnd--;
    1280                } else {
    1281                    // find suffix
    1282                    final int startPos = pos;
    1283                    pos += startMatchLen;
    1284                    int endMatchLen = 0;
    1285                    int nestedVarCount = 0;
    1286                    while (pos < bufEnd) {
    1287                        if (substitutionInVariablesEnabled && pfxMatcher.isMatch(chars, pos, offset, bufEnd) != 0) {
    1288                            // found a nested variable start
    1289                            endMatchLen = pfxMatcher.isMatch(chars, pos, offset, bufEnd);
    1290                            nestedVarCount++;
    1291                            pos += endMatchLen;
    1292                            continue;
    1293                        }
    1294
    1295                        endMatchLen = suffMatcher.isMatch(chars, pos, offset, bufEnd);
    1296                        if (endMatchLen == 0) {
    1297                            pos++;
    1298                        } else {
    1299                            // found variable end marker
    1300                            if (nestedVarCount == 0) {
    1301                                String varNameExpr = new String(chars, startPos + startMatchLen,
    1302                                        pos - startPos - startMatchLen);
    1303                                if (substitutionInVariablesEnabled) {
    1304                                    final TextStringBuilder bufName = new TextStringBuilder(varNameExpr);
    1305                                    substitute(bufName, 0, bufName.length());
    1306                                    varNameExpr = bufName.toString();
    1307                                }
    1308                                pos += endMatchLen;
    1309                                final int endPos = pos;
    1310
    1311                                String varName = varNameExpr;
    1312                                String varDefaultValue = null;
    1313
    1314                                if (valueDelimMatcher != null) {
    1315                                    final char[] varNameExprChars = varNameExpr.toCharArray();
    1316                                    int valueDelimiterMatchLen = 0;
    1317                                    for (int i = 0; i < varNameExprChars.length; i++) {
    1318                                        // if there's any nested variable when nested variable substitution disabled,
    1319                                        // then stop resolving name and default value.
    1320                                        if (!substitutionInVariablesEnabled && pfxMatcher.isMatch(varNameExprChars, i,
    1321                                                i, varNameExprChars.length) != 0) {
    1322                                            break;
    1323                                        }
    1324                                        if (valueDelimMatcher.isMatch(varNameExprChars, i, 0,
    1325                                                varNameExprChars.length) != 0) {
    1326                                            valueDelimiterMatchLen = valueDelimMatcher.isMatch(varNameExprChars, i, 0,
    1327                                                    varNameExprChars.length);
    1328                                            varName = varNameExpr.substring(0, i);
    1329                                            varDefaultValue = varNameExpr.substring(i + valueDelimiterMatchLen);
    1330                                            break;
    1331                                        }
    1332                                    }
    1333                                }
    1334
    1335                                // on the first call initialize priorVariables
    1336                                if (priorVariables == null) {
    1337                                    priorVariables = new ArrayList<>();
    1338                                    priorVariables.add(new String(chars, offset, length));
    1339                                }
    1340
    1341                                // handle cyclic substitution
    1342                                checkCyclicSubstitution(varName, priorVariables);
    1343                                priorVariables.add(varName);
    1344
    1345                                // resolve the variable
    1346                                String varValue = resolveVariable(varName, buf, startPos, endPos);
    1347                                if (varValue == null) {
    1348                                    varValue = varDefaultValue;
    1349                                }
    1350                                if (varValue != null) {
    1351                                    final int varLen = varValue.length();
    1352                                    buf.replace(startPos, endPos, varValue);
    1353                                    altered = true;
    1354                                    int change = 0;
    1355                                    if (!substitutionInValuesDisabled) { // recursive replace
    1356                                        change = substitute(buf, startPos, varLen, priorVariables);
    1357                                    }
    1358                                    change = change + varLen - (endPos - startPos);
    1359                                    pos += change;
    1360                                    bufEnd += change;
    1361                                    lengthChange += change;
    1362                                    chars = buf.buffer; // in case buffer was
    1363                                                        // altered
    1364                                }
    1365
    1366                                // remove variable from the cyclic stack
    1367                                priorVariables.remove(priorVariables.size() - 1);
    1368                                break;
    1369                            }
    1370                            nestedVarCount--;
    1371                            pos += endMatchLen;
    1372                        }
    1373                    }
    1374                }
    1375            }
    1376        }
    1377        if (top) {
    1378            return altered ? 1 : 0;
    1379        }
    1380        return lengthChange;
    1381    }
    1382}