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