    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 array as a template. The array is not
    806     * 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 returned.
    809     * </p>
    810     *
    811     * @param source the character array to replace in, not altered, null returns null.
    812     * @param offset the start offset within the array, must be valid.
    813     * @param length the length within the array to be processed, must be valid.
    814     * @return The result of the replace operation.
    815     * @throws IllegalArgumentException        if variable is not found when its allowed to throw exception.
    816     * @throws StringIndexOutOfBoundsException if {@code offset} is not in the range {@code 0 <= offset <= chars.length}.
    817     * @throws StringIndexOutOfBoundsException if {@code length < 0}.
    818     * @throws StringIndexOutOfBoundsException if {@code offset + length > chars.length}.
    819     */
    820    public String replace(final char[] source, final int offset, final int length) {
    821        if (source == null) {
    822            return null;
    823        }
    824        final TextStringBuilder buf = new TextStringBuilder(length).append(source, offset, length);
    825        substitute(buf, 0, length);
    826        return buf.toString();
    827    }
    828
    829    /**
    830     * Replaces all the occurrences of variables with their matching values from the resolver using the given source as
    831     * a template. The source is not altered by this method.
    832     *
    833     * @param source the buffer to use as a template, not changed, null returns null.
    834     * @return The result of the replace operation.
    835     * @throws IllegalArgumentException if variable is not found when its allowed to throw exception.
    836     */
    837    public String replace(final CharSequence source) {
    838        if (source == null) {
    839            return null;
    840        }
    841        return replace(source, 0, source.length());
    842    }
    843
    844    /**
    845     * Replaces all the occurrences of variables with their matching values from the resolver using the given source as
    846     * a template. The source is not altered by this method.
    847     * <p>
    848     * Only the specified portion of the buffer will be processed. The rest of the buffer is not processed, and is not
    849     * returned.
    850     * </p>
    851     *
    852     * @param source the buffer to use as a template, not changed, null returns null.
    853     * @param offset the start offset within the array, must be valid.
    854     * @param length the length within the array to be processed, must be valid.
    855     * @return The result of the replace operation.
    856     * @throws IllegalArgumentException if variable is not found when its allowed to throw exception.
    857     */
    858    public String replace(final CharSequence source, final int offset, final int length) {
    859        if (source == null) {
    860            return null;
    861        }
    862        final TextStringBuilder buf = new TextStringBuilder(length).append(source.toString(), offset, length);
    863        substitute(buf, 0, length);
    864        return buf.toString();
    865    }
    866
    867    /**
    868     * Replaces all the occurrences of variables in the given source object with their matching values from the
    869     * resolver. The input source object is converted to a string using {@code toString} and is not altered.
    870     *
    871     * @param source the source to replace in, null returns null.
    872     * @return The result of the replace operation.
    873     * @throws IllegalArgumentException if a variable is not found and enableUndefinedVariableException is true.
    874     */
    875    public String replace(final Object source) {
    876        if (source == null) {
    877            return null;
    878        }
    879        final TextStringBuilder buf = new TextStringBuilder().append(source);
    880        substitute(buf, 0, buf.length());
    881        return buf.toString();
    882    }
    883
    884    /**
    885     * Replaces all the occurrences of variables with their matching values from the resolver using the given source
    886     * string as a template.
    887     *
    888     * @param source the string to replace in, null returns null.
    889     * @return The result of the replace operation.
    890     * @throws IllegalArgumentException if variable is not found when its allowed to throw exception.
    891     */
    892    public String replace(final String source) {
    893        if (source == null) {
    894            return null;
    895        }
    896        final TextStringBuilder buf = new TextStringBuilder(source);
    897        if (!substitute(buf, 0, source.length())) {
    898            return source;
    899        }
    900        return buf.toString();
    901    }
    902
    903    /**
    904     * Replaces all the occurrences of variables with their matching values from the resolver using the given source string as a template.
    905     * <p>
    906     * Only the specified portion of the string will be processed. The rest of the string is not processed, and is not returned.
    907     * </p>
    908     *
    909     * @param source the string to replace in, null returns null.
    910     * @param offset the start offset within the source, must be valid.
    911     * @param length the length within the source to be processed, must be valid.
    912     * @return The result of the replace operation.
    913     * @throws IllegalArgumentException        if variable is not found when its allowed to throw exception.
    914     * @throws StringIndexOutOfBoundsException if {@code offset} is not in the range {@code 0 <= offset <= source.length()}.
    915     * @throws StringIndexOutOfBoundsException if {@code length < 0}.
    916     * @throws StringIndexOutOfBoundsException if {@code offset + length > source.length()}.
    917     */
    918    public String replace(final String 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        if (!substitute(buf, 0, length)) {
    924            return source.substring(offset, offset + length);
    925        }
    926        return buf.toString();
    927    }
    928
    929    /**
    930     * Replaces all the occurrences of variables with their matching values from the resolver using the given source
    931     * buffer as a template. The buffer is not altered by this method.
    932     *
    933     * @param source the buffer to use as a template, not changed, null returns null.
    934     * @return The result of the replace operation.
    935     * @throws IllegalArgumentException if variable is not found when its allowed to throw exception.
    936     */
    937    public String replace(final StringBuffer source) {
    938        if (source == null) {
    939            return null;
    940        }
    941        final TextStringBuilder buf = new TextStringBuilder(source.length()).append(source);
    942        substitute(buf, 0, buf.length());
    943        return buf.toString();
    944    }
    945
    946    /**
    947     * Replaces all the occurrences of variables with their matching values from the resolver using the given source
    948     * buffer as a template. The buffer is not altered by this method.
    949     * <p>
    950     * Only the specified portion of the buffer will be processed. The rest of the buffer is not processed, and is not
    951     * returned.
    952     * </p>
    953     *
    954     * @param source the buffer to use as a template, not changed, null returns null.
    955     * @param offset the start offset within the source, must be valid.
    956     * @param length the length within the source to be processed, must be valid.
    957     * @return The result of the replace operation.
    958     * @throws IllegalArgumentException if variable is not found when its allowed to throw exception.
    959     */
    960    public String replace(final StringBuffer source, final int offset, final int length) {
    961        if (source == null) {
    962            return null;
    963        }
    964        final TextStringBuilder buf = new TextStringBuilder(length).append(source, offset, length);
    965        substitute(buf, 0, length);
    966        return buf.toString();
    967    }
    968
    969    /**
    970     * Replaces all the occurrences of variables with their matching values from the resolver using the given source
    971     * builder as a template. The builder is not altered by this method.
    972     *
    973     * @param source the builder to use as a template, not changed, null returns null.
    974     * @return The result of the replace operation.
    975     * @throws IllegalArgumentException if variable is not found when its allowed to throw exception.
    976     */
    977    public String replace(final TextStringBuilder source) {
    978        if (source == null) {
    979            return null;
    980        }
    981        final TextStringBuilder builder = new TextStringBuilder(source.length()).append(source);
    982        substitute(builder, 0, builder.length());
    983        return builder.toString();
    984    }
    985
    986    /**
    987     * Replaces all the occurrences of variables with their matching values from the resolver using the given source
    988     * builder as a template. The builder is not altered by this method.
    989     * <p>
    990     * Only the specified portion of the builder will be processed. The rest of the builder is not processed, and is not
    991     * returned.
    992     * </p>
    993     *
    994     * @param source the builder to use as a template, not changed, null returns null.
    995     * @param offset the start offset within the source, must be valid.
    996     * @param length the length within the source to be processed, must be valid.
    997     * @return The result of the replace operation.
    998     * @throws IllegalArgumentException if variable is not found when its allowed to throw exception.
    999     */
    1000    public String replace(final TextStringBuilder source, final int offset, final int length) {
    1001        if (source == null) {
    1002            return null;
    1003        }
    1004        final TextStringBuilder buf = new TextStringBuilder(length).append(source, offset, length);
    1005        substitute(buf, 0, length);
    1006        return buf.toString();
    1007    }
    1008
    1009    /**
    1010     * Replaces all the occurrences of variables within the given source buffer with their matching values from the
    1011     * resolver. The buffer is updated with the result.
    1012     *
    1013     * @param source the buffer to replace in, updated, null returns zero.
    1014     * @return true if altered
    1015     */
    1016    public boolean replaceIn(final StringBuffer source) {
    1017        if (source == null) {
    1018            return false;
    1019        }
    1020        return replaceIn(source, 0, source.length());
    1021    }
    1022
    1023    /**
    1024     * Replaces all the occurrences of variables within the given source buffer with their matching values from the
    1025     * resolver. The buffer is updated with the result.
    1026     * <p>
    1027     * Only the specified portion of the buffer will be processed. The rest of the buffer is not processed, but it is
    1028     * not deleted.
    1029     * </p>
    1030     *
    1031     * @param source the buffer to replace in, updated, null returns zero.
    1032     * @param offset the start offset within the source, must be valid.
    1033     * @param length the length within the source to be processed, must be valid.
    1034     * @return true if altered.
    1035     * @throws IllegalArgumentException if variable is not found when its allowed to throw exception.
    1036     */
    1037    public boolean replaceIn(final StringBuffer source, final int offset, final int length) {
    1038        if (source == null) {
    1039            return false;
    1040        }
    1041        final TextStringBuilder buf = new TextStringBuilder(length).append(source, offset, length);
    1042        if (!substitute(buf, 0, length)) {
    1043            return false;
    1044        }
    1045        source.replace(offset, offset + length, buf.toString());
    1046        return true;
    1047    }
    1048
    1049    /**
    1050     * Replaces all the occurrences of variables within the given source buffer with their matching values from the
    1051     * resolver. The buffer is updated with the result.
    1052     *
    1053     * @param source the buffer to replace in, updated, null returns zero.
    1054     * @return true if altered.
    1055     */
    1056    public boolean replaceIn(final StringBuilder source) {
    1057        if (source == null) {
    1058            return false;
    1059        }
    1060        return replaceIn(source, 0, source.length());
    1061    }
    1062
    1063    /**
    1064     * Replaces all the occurrences of variables within the given source builder with their matching values from the
    1065     * resolver. The builder is updated with the result.
    1066     * <p>
    1067     * Only the specified portion of the buffer will be processed. The rest of the buffer is not processed, but it is
    1068     * not deleted.
    1069     * </p>
    1070     *
    1071     * @param source the buffer to replace in, updated, null returns zero.
    1072     * @param offset the start offset within the source, must be valid.
    1073     * @param length the length within the source to be processed, must be valid.
    1074     * @return true if altered.
    1075     * @throws IllegalArgumentException if variable is not found when its allowed to throw exception.
    1076     */
    1077    public boolean replaceIn(final StringBuilder source, final int offset, final int length) {
    1078        if (source == null) {
    1079            return false;
    1080        }
    1081        final TextStringBuilder buf = new TextStringBuilder(length).append(source, offset, length);
    1082        if (!substitute(buf, 0, length)) {
    1083            return false;
    1084        }
    1085        source.replace(offset, offset + length, buf.toString());
    1086        return true;
    1087    }
    1088
    1089    /**
    1090     * Replaces all the occurrences of variables within the given source builder with their matching values from the
    1091     * resolver.
    1092     *
    1093     * @param source the builder to replace in, updated, null returns zero.
    1094     * @return true if altered.
    1095     * @throws IllegalArgumentException if variable is not found when its allowed to throw exception.
    1096     */
    1097    public boolean replaceIn(final TextStringBuilder source) {
    1098        if (source == null) {
    1099            return false;
    1100        }
    1101        return substitute(source, 0, source.length());
    1102    }
    1103
    1104    /**
    1105     * Replaces all the occurrences of variables within the given source builder with their matching values from the
    1106     * resolver.
    1107     * <p>
    1108     * Only the specified portion of the builder will be processed. The rest of the builder is not processed, but it is
    1109     * not deleted.
    1110     * </p>
    1111     *
    1112     * @param source the builder to replace in, null returns zero.
    1113     * @param offset the start offset within the source, must be valid.
    1114     * @param length the length within the source to be processed, must be valid.
    1115     * @return true if altered.
    1116     * @throws IllegalArgumentException if variable is not found when its allowed to throw exception.
    1117     */
    1118    public boolean replaceIn(final TextStringBuilder source, final int offset, final int length) {
    1119        if (source == null) {
    1120            return false;
    1121        }
    1122        return substitute(source, offset, length);
    1123    }
    1124
    1125    /**
    1126     * Internal method that resolves the value of a variable.
    1127     * <p>
    1128     * Most users of this class do not need to call this method. This method is called automatically by the substitution
    1129     * process.
    1130     * </p>
    1131     * <p>
    1132     * Writers of subclasses can override this method if they need to alter how each substitution occurs. The method is
    1133     * passed the variable's name and must return the corresponding value. This implementation uses the
    1134     * {@link #getStringLookup()} with the variable's name as the key.
    1135     * </p>
    1136     *
    1137     * @param variableName the name of the variable, not null.
    1138     * @param buf the buffer where the substitution is occurring, not null.
    1139     * @param startPos the start position of the variable including the prefix, valid.
    1140     * @param endPos the end position of the variable including the suffix, valid.
    1141     * @return The variable's value or <strong>null</strong> if the variable is unknown.
    1142     */
    1143    protected String resolveVariable(final String variableName, final TextStringBuilder buf, final int startPos,
    1144        final int endPos) {
    1145        final StringLookup resolver = getStringLookup();
    1146        if (resolver == null) {
    1147            return null;
    1148        }
    1149        return resolver.apply(variableName);
    1150    }
    1151
    1152    /**
    1153     * Sets a flag whether substitution is done in variable values (recursive).
    1154     *
    1155     * @param disableSubstitutionInValues true if substitution in variable value are disabled.
    1156     * @return {@code this} instance.
    1157     */
    1158    public StringSubstitutor setDisableSubstitutionInValues(final boolean disableSubstitutionInValues) {
    1159        this.disableSubstitutionInValues = disableSubstitutionInValues;
    1160        return this;
    1161    }
    1162
    1163    /**
    1164     * Sets a flag whether substitution is done in variable names. If set to <strong>true</strong>, the names of variables can
    1165     * contain other variables which are processed first before the original variable is evaluated, e.g.
    1166     * {@code ${jre-${java.version}}}. The default value is <strong>false</strong>.
    1167     *
    1168     * @param enableSubstitutionInVariables the new value of the flag.
    1169     * @return {@code this} instance.
    1170     */
    1171    public StringSubstitutor setEnableSubstitutionInVariables(final boolean enableSubstitutionInVariables) {
    1172        this.enableSubstitutionInVariables = enableSubstitutionInVariables;
    1173        return this;
    1174    }
    1175
    1176    /**
    1177     * Sets a flag whether exception should be thrown if any variable is undefined.
    1178     *
    1179     * @param failOnUndefinedVariable true if exception should be thrown on undefined variable.
    1180     * @return {@code this} instance.
    1181     */
    1182    public StringSubstitutor setEnableUndefinedVariableException(final boolean failOnUndefinedVariable) {
    1183        this.failOnUndefinedVariable = failOnUndefinedVariable;
    1184        return this;
    1185    }
    1186
    1187    /**
    1188     * Sets the escape character. If this character is placed before a variable reference in the source text, this
    1189     * variable will be ignored.
    1190     *
    1191     * @param escapeChar the escape character (0 for disabling escaping).
    1192     * @return {@code this} instance.
    1193     */
    1194    public StringSubstitutor setEscapeChar(final char escapeChar) {
    1195        this.escapeChar = escapeChar;
    1196        return this;
    1197    }
    1198
    1199    /**
    1200     * Sets a flag controlling whether escapes are preserved during substitution. If set to <strong>true</strong>, the escape
    1201     * character is retained during substitution (e.g. {@code $${this-is-escaped}} remains {@code $${this-is-escaped}}).
    1202     * If set to <strong>false</strong>, the escape character is removed during substitution (e.g. {@code $${this-is-escaped}}
    1203     * becomes {@code ${this-is-escaped}}). The default value is <strong>false</strong>
    1204     *
    1205     * @param preserveEscapes true if escapes are to be preserved.
    1206     * @return {@code this} instance.
    1207     */
    1208    public StringSubstitutor setPreserveEscapes(final boolean preserveEscapes) {
    1209        this.preserveEscapes = preserveEscapes;
    1210        return this;
    1211    }
    1212
    1213    /**
    1214     * Sets the variable default value delimiter to use.
    1215     * <p>
    1216     * The variable default value delimiter is the character or characters that delimit the variable name and the
    1217     * variable default value. This method allows a single character variable default value delimiter to be easily set.
    1218     * </p>
    1219     *
    1220     * @param valueDelimiter the variable default value delimiter character to use.
    1221     * @return {@code this} instance.
    1222     */
    1223    public StringSubstitutor setValueDelimiter(final char valueDelimiter) {
    1224        return setValueDelimiterMatcher(StringMatcherFactory.INSTANCE.charMatcher(valueDelimiter));
    1225    }
    1226
    1227    /**
    1228     * Sets the variable default value delimiter to use.
    1229     * <p>
    1230     * The variable default value delimiter is the character or characters that delimit the variable name and the
    1231     * variable default value. This method allows a string variable default value delimiter to be easily set.
    1232     * </p>
    1233     * <p>
    1234     * If the {@code valueDelimiter} is null or empty string, then the variable default value resolution becomes
    1235     * disabled.
    1236     * </p>
    1237     *
    1238     * @param valueDelimiter the variable default value delimiter string to use, may be null or empty.
    1239     * @return {@code this} instance.
    1240     */
    1241    public StringSubstitutor setValueDelimiter(final String valueDelimiter) {
    1242        if (valueDelimiter == null || valueDelimiter.isEmpty()) {
    1243            setValueDelimiterMatcher(null);
    1244            return this;
    1245        }
    1246        return setValueDelimiterMatcher(StringMatcherFactory.INSTANCE.stringMatcher(valueDelimiter));
    1247    }
    1248
    1249    /**
    1250     * Sets the variable default value delimiter matcher to use.
    1251     * <p>
    1252     * The variable default value delimiter is the character or characters that delimit the variable name and the
    1253     * variable default value. This delimiter is expressed in terms of a matcher allowing advanced variable default
    1254     * value delimiter matches.
    1255     * </p>
    1256     * <p>
    1257     * If the {@code valueDelimiterMatcher} is null, then the variable default value resolution becomes disabled.
    1258     * </p>
    1259     *
    1260     * @param valueDelimiterMatcher variable default value delimiter matcher to use, may be null.
    1261     * @return {@code this} instance.
    1262     */
    1263    public StringSubstitutor setValueDelimiterMatcher(final StringMatcher valueDelimiterMatcher) {
    1264        this.valueDelimiterMatcher = valueDelimiterMatcher;
    1265        return this;
    1266    }
    1267
    1268    /**
    1269     * Sets the variable prefix to use.
    1270     * <p>
    1271     * The variable prefix is the character or characters that identify the start of a variable. This method allows a
    1272     * single character prefix to be easily set.
    1273     * </p>
    1274     *
    1275     * @param prefix the prefix character to use.
    1276     * @return {@code this} instance.
    1277     */
    1278    public StringSubstitutor setVariablePrefix(final char prefix) {
    1279        return setVariablePrefixMatcher(StringMatcherFactory.INSTANCE.charMatcher(prefix));
    1280    }
    1281
    1282    /**
    1283     * Sets the variable prefix to use.
    1284     * <p>
    1285     * The variable prefix is the character or characters that identify the start of a variable. This method allows a
    1286     * string prefix to be easily set.
    1287     * </p>
    1288     *
    1289     * @param prefix the prefix for variables, not null.
    1290     * @return {@code this} instance.
    1291     * @throws IllegalArgumentException if the prefix is null.
    1292     */
    1293    public StringSubstitutor setVariablePrefix(final String prefix) {
    1294        Validate.isTrue(prefix != null, "Variable prefix must not be null!");
    1295        return setVariablePrefixMatcher(StringMatcherFactory.INSTANCE.stringMatcher(prefix));
    1296    }
    1297
    1298    /**
    1299     * Sets the variable prefix matcher currently in use.
    1300     * <p>
    1301     * The variable prefix is the character or characters that identify the start of a variable. This prefix is
    1302     * expressed in terms of a matcher allowing advanced prefix matches.
    1303     * </p>
    1304     *
    1305     * @param prefixMatcher the prefix matcher to use, null ignored.
    1306     * @return {@code this} instance.
    1307     * @throws IllegalArgumentException if the prefix matcher is null.
    1308     */
    1309    public StringSubstitutor setVariablePrefixMatcher(final StringMatcher prefixMatcher) {
    1310        Validate.isTrue(prefixMatcher != null, "Variable prefix matcher must not be null!");
    1311        this.prefixMatcher = prefixMatcher;
    1312        return this;
    1313    }
    1314
    1315    /**
    1316     * Sets the VariableResolver that is used to lookup variables.
    1317     *
    1318     * @param variableResolver the VariableResolver.
    1319     * @return {@code this} instance.
    1320     */
    1321    public StringSubstitutor setVariableResolver(final StringLookup variableResolver) {
    1322        this.variableResolver = variableResolver;
    1323        return this;
    1324    }
    1325
    1326    /**
    1327     * Sets the variable suffix to use.
    1328     * <p>
    1329     * The variable suffix is the character or characters that identify the end of a variable. This method allows a
    1330     * single character suffix to be easily set.
    1331     * </p>
    1332     *
    1333     * @param suffix the suffix character to use.
    1334     * @return {@code this} instance.
    1335     */
    1336    public StringSubstitutor setVariableSuffix(final char suffix) {
    1337        return setVariableSuffixMatcher(StringMatcherFactory.INSTANCE.charMatcher(suffix));
    1338    }
    1339
    1340    /**
    1341     * Sets the variable suffix to use.
    1342     * <p>
    1343     * The variable suffix is the character or characters that identify the end of a variable. This method allows a
    1344     * string suffix to be easily set.
    1345     * </p>
    1346     *
    1347     * @param suffix the suffix for variables, not null.
    1348     * @return {@code this} instance.
    1349     * @throws IllegalArgumentException if the suffix is null.
    1350     */
    1351    public StringSubstitutor setVariableSuffix(final String suffix) {
    1352        Validate.isTrue(suffix != null, "Variable suffix must not be null!");
    1353        return setVariableSuffixMatcher(StringMatcherFactory.INSTANCE.stringMatcher(suffix));
    1354    }
    1355
    1356    /**
    1357     * Sets the variable suffix matcher currently in use.
    1358     * <p>
    1359     * The variable suffix is the character or characters that identify the end of a variable. This suffix is expressed
    1360     * in terms of a matcher allowing advanced suffix matches.
    1361     * </p>
    1362     *
    1363     * @param suffixMatcher the suffix matcher to use, null ignored.
    1364     * @return {@code this} instance.
    1365     * @throws IllegalArgumentException if the suffix matcher is null.
    1366     */
    1367    public StringSubstitutor setVariableSuffixMatcher(final StringMatcher suffixMatcher) {
    1368        Validate.isTrue(suffixMatcher != null, "Variable suffix matcher must not be null!");
    1369        this.suffixMatcher = suffixMatcher;
    1370        return this;
    1371    }
    1372
    1373    /**
    1374     * Internal method that substitutes the variables.
    1375     * <p>
    1376     * Most users of this class do not need to call this method. This method will be called automatically by another
    1377     * (public) method.
    1378     * </p>
    1379     * <p>
    1380     * Writers of subclasses can override this method if they need access to the substitution process at the start or
    1381     * end.
    1382     * </p>
    1383     *
    1384     * @param builder the string builder to substitute into, not null.
    1385     * @param offset the start offset within the builder, must be valid.
    1386     * @param length the length within the builder to be processed, must be valid.
    1387     * @return true if altered.
    1388     */
    1389    protected boolean substitute(final TextStringBuilder builder, final int offset, final int length) {
    1390        return substitute(builder, offset, length, null).altered;
    1391    }
    1392
    1393    /**
    1394     * Recursive handler for multiple levels of interpolation. This is the main interpolation method, which resolves the
    1395     * values of all variable references contained in the passed in text.
    1396     *
    1397     * @param builder the string builder to substitute into, not null.
    1398     * @param offset the start offset within the builder, must be valid.
    1399     * @param length the length within the builder to be processed, must be valid.
    1400     * @param priorVariables the stack keeping track of the replaced variables, may be null.
    1401     * @return The result.
    1402     * @throws IllegalArgumentException if variable is not found and <code>isEnableUndefinedVariableException() == true</code>.
    1403     * @since 1.9
    1404     */
    1405    private Result substitute(final TextStringBuilder builder, final int offset, final int length,
    1406        List<String> priorVariables) {
    1407        Objects.requireNonNull(builder, "builder");
    1408        final StringMatcher prefixMatcher = getVariablePrefixMatcher();
    1409        final StringMatcher suffixMatcher = getVariableSuffixMatcher();
    1410        final char escapeCh = getEscapeChar();
    1411        final StringMatcher valueDelimMatcher = getValueDelimiterMatcher();
    1412        final boolean substitutionInVariablesEnabled = isEnableSubstitutionInVariables();
    1413        final boolean substitutionInValuesDisabled = isDisableSubstitutionInValues();
    1414        final boolean undefinedVariableException = isEnableUndefinedVariableException();
    1415        final boolean preserveEscapes = isPreserveEscapes();
    1416
    1417        boolean altered = false;
    1418        int lengthChange = 0;
    1419        int bufEnd = offset + length;
    1420        int pos = offset;
    1421        int escPos = -1;
    1422        outer: while (pos < bufEnd) {
    1423            final int startMatchLen = prefixMatcher.isMatch(builder, pos, offset, bufEnd);
    1424            if (startMatchLen == 0) {
    1425                pos++;
    1426            } else {
    1427                // found variable start marker
    1428                if (pos > offset && builder.charAt(pos - 1) == escapeCh) {
    1429                    // escape detected
    1430                    if (preserveEscapes) {
    1431                        // keep escape
    1432                        pos++;
    1433                        continue;
    1434                    }
    1435                    // mark esc ch for deletion if we find a complete variable
    1436                    escPos = pos - 1;
    1437                }
    1438                // find suffix
    1439                int startPos = pos;
    1440                pos += startMatchLen;
    1441                int endMatchLen = 0;
    1442                int nestedVarCount = 0;
    1443                while (pos < bufEnd) {
    1444                    if (substitutionInVariablesEnabled && prefixMatcher.isMatch(builder, pos, offset, bufEnd) != 0) {
    1445                        // found a nested variable start
    1446                        endMatchLen = prefixMatcher.isMatch(builder, pos, offset, bufEnd);
    1447                        nestedVarCount++;
    1448                        pos += endMatchLen;
    1449                        continue;
    1450                    }
    1451
    1452                    endMatchLen = suffixMatcher.isMatch(builder, pos, offset, bufEnd);
    1453                    if (endMatchLen == 0) {
    1454                        pos++;
    1455                    } else {
    1456                        // found variable end marker
    1457                        if (nestedVarCount == 0) {
    1458                            if (escPos >= 0) {
    1459                                // delete escape
    1460                                builder.deleteCharAt(escPos);
    1461                                escPos = -1;
    1462                                lengthChange--;
    1463                                altered = true;
    1464                                bufEnd--;
    1465                                pos = startPos + 1;
    1466                                startPos--;
    1467                                continue outer;
    1468                            }
    1469                            // get var name
    1470                            String varNameExpr = builder.midString(startPos + startMatchLen,
    1471                                pos - startPos - startMatchLen);
    1472                            if (substitutionInVariablesEnabled) {
    1473                                final TextStringBuilder bufName = new TextStringBuilder(varNameExpr);
    1474                                substitute(bufName, 0, bufName.length());
    1475                                varNameExpr = bufName.toString();
    1476                            }
    1477                            pos += endMatchLen;
    1478                            final int endPos = pos;
    1479
    1480                            String varName = varNameExpr;
    1481                            String varDefaultValue = null;
    1482
    1483                            if (valueDelimMatcher != null) {
    1484                                final char[] varNameExprChars = varNameExpr.toCharArray();
    1485                                int valueDelimiterMatchLen = 0;
    1486                                for (int i = 0; i < varNameExprChars.length; i++) {
    1487                                    // if there's any nested variable when nested variable substitution disabled,
    1488                                    // then stop resolving name and default value.
    1489                                    if (!substitutionInVariablesEnabled && prefixMatcher.isMatch(varNameExprChars, i, i,
    1490                                        varNameExprChars.length) != 0) {
    1491                                        break;
    1492                                    }
    1493                                    if (valueDelimMatcher.isMatch(varNameExprChars, i, 0,
    1494                                        varNameExprChars.length) != 0) {
    1495                                        valueDelimiterMatchLen = valueDelimMatcher.isMatch(varNameExprChars, i, 0,
    1496                                            varNameExprChars.length);
    1497                                        varName = varNameExpr.substring(0, i);
    1498                                        varDefaultValue = varNameExpr.substring(i + valueDelimiterMatchLen);
    1499                                        break;
    1500                                    }
    1501                                }
    1502                            }
    1503
    1504                            // on the first call initialize priorVariables
    1505                            if (priorVariables == null) {
    1506                                priorVariables = new ArrayList<>();
    1507                                priorVariables.add(builder.midString(offset, length));
    1508                            }
    1509
    1510                            // handle cyclic substitution
    1511                            checkCyclicSubstitution(varName, priorVariables);
    1512                            priorVariables.add(varName);
    1513
    1514                            // resolve the variable
    1515                            String varValue = resolveVariable(varName, builder, startPos, endPos);
    1516                            if (varValue == null) {
    1517                                varValue = varDefaultValue;
    1518                            }
    1519                            if (varValue != null) {
    1520                                final int varLen = varValue.length();
    1521                                builder.replace(startPos, endPos, varValue);
    1522                                altered = true;
    1523                                int change = 0;
    1524                                if (!substitutionInValuesDisabled) { // recursive replace
    1525                                    change = substitute(builder, startPos, varLen, priorVariables).lengthChange;
    1526                                }
    1527                                change = change + varLen - (endPos - startPos);
    1528                                pos += change;
    1529                                bufEnd += change;
    1530                                lengthChange += change;
    1531                            } else if (undefinedVariableException) {
    1532                                throw new IllegalArgumentException(
    1533                                    String.format("Cannot resolve variable '%s' (enableSubstitutionInVariables=%s).",
    1534                                        varName, substitutionInVariablesEnabled));
    1535                            }
    1536
    1537                            // remove variable from the cyclic stack
    1538                            priorVariables.remove(priorVariables.size() - 1);
    1539                            break;
    1540                        }
    1541                        nestedVarCount--;
    1542                        pos += endMatchLen;
    1543                    }
    1544                }
    1545            }
    1546        }
    1547        return new Result(altered, lengthChange);
    1548    }
    1549
    1550    /**
    1551     * Returns a string representation of the object.
    1552     *
    1553     * @return a string representation of the object.
    1554     * @since 1.11.0
    1555     */
    1556    @Override
    1557    public String toString() {
    1558        // @formatter:off
    1559        return new StringBuilder()
    1560            .append("StringSubstitutor [disableSubstitutionInValues=")
    1561            .append(disableSubstitutionInValues)
    1562            .append(", enableSubstitutionInVariables=")
    1563            .append(enableSubstitutionInVariables)
    1564            .append(", enableUndefinedVariableException=")
    1565            .append(failOnUndefinedVariable)
    1566            .append(", escapeChar=")
    1567            .append(escapeChar)
    1568            .append(", prefixMatcher=")
    1569            .append(prefixMatcher)
    1570            .append(", preserveEscapes=")
    1571            .append(preserveEscapes)
    1572            .append(", suffixMatcher=")
    1573            .append(suffixMatcher)
    1574            .append(", valueDelimiterMatcher=")
    1575            .append(valueDelimiterMatcher)
    1576            .append(", variableResolver=")
    1577            .append(variableResolver)
    1578            .append("]")
    1579            .toString();
    1580        // @formatter:on
    1581    }
    1582}