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