    001/*
    002 * Licensed to the Apache Software Foundation (ASF) under one or more
    003 * contributor license agreements.  See the NOTICE file distributed with
    004 * this work for additional information regarding copyright ownership.
    005 * The ASF licenses this file to You under the Apache License, Version 2.0
    006 * (the "License"); you may not use this file except in compliance with
    007 * the License.  You may obtain a copy of the License at
    008 *
    009 *      http://www.apache.org/licenses/LICENSE-2.0
    010 *
    011 * Unless required by applicable law or agreed to in writing, software
    012 * distributed under the License is distributed on an "AS IS" BASIS,
    013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    014 * See the License for the specific language governing permissions and
    015 * limitations under the License.
    016 */
    017package org.apache.commons.text;
    018
    019import java.util.ArrayList;
    020import java.util.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 <b>not</b> 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_PROPERTIES}</td>
    349     * <td>{@link StringLookupFactory#propertiesStringLookup()}</td>
    350     * </tr>
    351     * <tr>
    352     * <td>{@value org.apache.commons.text.lookup.StringLookupFactory#KEY_RESOURCE_BUNDLE}</td>
    353     * <td>{@link StringLookupFactory#resourceBundleStringLookup()}</td>
    354     * </tr>
    355     * <tr>
    356     * <td>{@value org.apache.commons.text.lookup.StringLookupFactory#KEY_SYS}</td>
    357     * <td>{@link StringLookupFactory#systemPropertyStringLookup()}</td>
    358     * </tr>
    359     * <tr>
    360     * <td>{@value org.apache.commons.text.lookup.StringLookupFactory#KEY_URL_DECODER}</td>
    361     * <td>{@link StringLookupFactory#urlDecoderStringLookup()}</td>
    362     * </tr>
    363     * <tr>
    364     * <td>{@value org.apache.commons.text.lookup.StringLookupFactory#KEY_URL_ENCODER}</td>
    365     * <td>{@link StringLookupFactory#urlEncoderStringLookup()}</td>
    366     * </tr>
    367     * <tr>
    368     * <td>{@value org.apache.commons.text.lookup.StringLookupFactory#KEY_XML}</td>
    369     * <td>{@link StringLookupFactory#xmlStringLookup()}</td>
    370     * </tr>
    371     * <tr>
    372     * <td>{@value org.apache.commons.text.lookup.StringLookupFactory#KEY_XML_DECODER}</td>
    373     * <td>{@link StringLookupFactory#xmlDecoderStringLookup()}</td>
    374     * </tr>
    375     * <tr>
    376     * <td>{@value org.apache.commons.text.lookup.StringLookupFactory#KEY_XML_ENCODER}</td>
    377     * <td>{@link StringLookupFactory#xmlEncoderStringLookup()}</td>
    378     * </tr>
    379     * </table>
    380     *
    381     * @return a new instance using the interpolator string lookup.
    382     * @see StringLookupFactory#interpolatorStringLookup()
    383     * @since 1.8
    384     */
    385    public static StringSubstitutor createInterpolator() {
    386        return new StringSubstitutor(StringLookupFactory.INSTANCE.interpolatorStringLookup());
    387    }
    388
    389    /**
    390     * Replaces all the occurrences of variables in the given source object with their matching values from the map.
    391     *
    392     * @param <V> the type of the values in the map
    393     * @param source the source text containing the variables to substitute, null returns null
    394     * @param valueMap the map with the values, may be null
    395     * @return The result of the replace operation
    396     * @throws IllegalArgumentException if a variable is not found and enableUndefinedVariableException is true
    397     */
    398    public static <V> String replace(final Object source, final Map<String, V> valueMap) {
    399        return new StringSubstitutor(valueMap).replace(source);
    400    }
    401
    402    /**
    403     * Replaces all the occurrences of variables in the given source object with their matching values from the map.
    404     * This method allows to specify a custom variable prefix and suffix
    405     *
    406     * @param <V> the type of the values in the map
    407     * @param source the source text containing the variables to substitute, null returns null
    408     * @param valueMap the map with the values, may be null
    409     * @param prefix the prefix of variables, not null
    410     * @param suffix the suffix of variables, not null
    411     * @return The result of the replace operation
    412     * @throws IllegalArgumentException if the prefix or suffix is null
    413     * @throws IllegalArgumentException if a variable is not found and enableUndefinedVariableException is true
    414     */
    415    public static <V> String replace(final Object source, final Map<String, V> valueMap, final String prefix,
    416        final String suffix) {
    417        return new StringSubstitutor(valueMap, prefix, suffix).replace(source);
    418    }
    419
    420    /**
    421     * Replaces all the occurrences of variables in the given source object with their matching values from the
    422     * properties.
    423     *
    424     * @param source the source text containing the variables to substitute, null returns null
    425     * @param valueProperties the properties with values, may be null
    426     * @return The result of the replace operation
    427     * @throws IllegalArgumentException if a variable is not found and enableUndefinedVariableException is true
    428     */
    429    public static String replace(final Object source, final Properties valueProperties) {
    430        if (valueProperties == null) {
    431            return source.toString();
    432        }
    433        return StringSubstitutor.replace(source,
    434                valueProperties.stringPropertyNames().stream().collect(Collectors.toMap(Function.identity(), valueProperties::getProperty)));
    435    }
    436
    437    /**
    438     * Replaces all the occurrences of variables in the given source object with their matching values from the system
    439     * properties.
    440     *
    441     * @param source the source text containing the variables to substitute, null returns null
    442     * @return The result of the replace operation
    443     * @throws IllegalArgumentException if a variable is not found and enableUndefinedVariableException is true
    444     */
    445    public static String replaceSystemProperties(final Object source) {
    446        return new StringSubstitutor(StringLookupFactory.INSTANCE.systemPropertyStringLookup()).replace(source);
    447    }
    448
    449    /**
    450     * The flag whether substitution in variable values is disabled.
    451     */
    452    private boolean disableSubstitutionInValues;
    453
    454    /**
    455     * The flag whether substitution in variable names is enabled.
    456     */
    457    private boolean enableSubstitutionInVariables;
    458
    459    /**
    460     * The flag whether exception should be thrown on undefined variable.
    461     */
    462    private boolean enableUndefinedVariableException;
    463
    464    /**
    465     * Stores the escape character.
    466     */
    467    private char escapeChar;
    468
    469    /**
    470     * Stores the variable prefix.
    471     */
    472    private StringMatcher prefixMatcher;
    473
    474    /**
    475     * Whether escapes should be preserved. Default is false;
    476     */
    477    private boolean preserveEscapes;
    478
    479    /**
    480     * Stores the variable suffix.
    481     */
    482    private StringMatcher suffixMatcher;
    483
    484    /**
    485     * Stores the default variable value delimiter.
    486     */
    487    private StringMatcher valueDelimiterMatcher;
    488
    489    /**
    490     * Variable resolution is delegated to an implementor of {@link StringLookup}.
    491     */
    492    private StringLookup variableResolver;
    493
    494    /**
    495     * Constructs a new instance with defaults for variable prefix and suffix and the escaping character.
    496     */
    497    public StringSubstitutor() {
    498        this((StringLookup) null, DEFAULT_PREFIX, DEFAULT_SUFFIX, DEFAULT_ESCAPE);
    499    }
    500
    501    /**
    502     * Constructs a new initialized instance. Uses defaults for variable prefix and suffix and the escaping
    503     * character.
    504     *
    505     * @param <V> the type of the values in the map
    506     * @param valueMap the map with the variables' values, may be null
    507     */
    508    public <V> StringSubstitutor(final Map<String, V> valueMap) {
    509        this(StringLookupFactory.INSTANCE.mapStringLookup(valueMap), DEFAULT_PREFIX, DEFAULT_SUFFIX, DEFAULT_ESCAPE);
    510    }
    511
    512    /**
    513     * Constructs a new initialized instance. Uses a default escaping character.
    514     *
    515     * @param <V> the type of the values in the map
    516     * @param valueMap the map with the variables' values, may be null
    517     * @param prefix the prefix for variables, not null
    518     * @param suffix the suffix for variables, not null
    519     * @throws IllegalArgumentException if the prefix or suffix is null
    520     */
    521    public <V> StringSubstitutor(final Map<String, V> valueMap, final String prefix, final String suffix) {
    522        this(StringLookupFactory.INSTANCE.mapStringLookup(valueMap), prefix, suffix, DEFAULT_ESCAPE);
    523    }
    524
    525    /**
    526     * Constructs a new initialized instance.
    527     *
    528     * @param <V> the type of the values in the map
    529     * @param valueMap the map with the variables' values, may be null
    530     * @param prefix the prefix for variables, not null
    531     * @param suffix the suffix for variables, not null
    532     * @param escape the escape character
    533     * @throws IllegalArgumentException if the prefix or suffix is null
    534     */
    535    public <V> StringSubstitutor(final Map<String, V> valueMap, final String prefix, final String suffix,
    536        final char escape) {
    537        this(StringLookupFactory.INSTANCE.mapStringLookup(valueMap), prefix, suffix, escape);
    538    }
    539
    540    /**
    541     * Constructs a new initialized instance.
    542     *
    543     * @param <V> the type of the values in the map
    544     * @param valueMap the map with the variables' values, may be null
    545     * @param prefix the prefix for variables, not null
    546     * @param suffix the suffix for variables, not null
    547     * @param escape the escape character
    548     * @param valueDelimiter the variable default value delimiter, may be null
    549     * @throws IllegalArgumentException if the prefix or suffix is null
    550     */
    551    public <V> StringSubstitutor(final Map<String, V> valueMap, final String prefix, final String suffix,
    552        final char escape, final String valueDelimiter) {
    553        this(StringLookupFactory.INSTANCE.mapStringLookup(valueMap), prefix, suffix, escape, valueDelimiter);
    554    }
    555
    556    /**
    557     * Constructs a new initialized instance.
    558     *
    559     * @param variableResolver the variable resolver, may be null
    560     */
    561    public StringSubstitutor(final StringLookup variableResolver) {
    562        this(variableResolver, DEFAULT_PREFIX, DEFAULT_SUFFIX, DEFAULT_ESCAPE);
    563    }
    564
    565    /**
    566     * Constructs a new initialized instance.
    567     *
    568     * @param variableResolver the variable resolver, may be null
    569     * @param prefix the prefix for variables, not null
    570     * @param suffix the suffix for variables, not null
    571     * @param escape the escape character
    572     * @throws IllegalArgumentException if the prefix or suffix is null
    573     */
    574    public StringSubstitutor(final StringLookup variableResolver, final String prefix, final String suffix,
    575        final char escape) {
    576        this.setVariableResolver(variableResolver);
    577        this.setVariablePrefix(prefix);
    578        this.setVariableSuffix(suffix);
    579        this.setEscapeChar(escape);
    580        this.setValueDelimiterMatcher(DEFAULT_VALUE_DELIMITER);
    581    }
    582
    583    /**
    584     * Constructs a new initialized instance.
    585     *
    586     * @param variableResolver the variable resolver, may be null
    587     * @param prefix the prefix for variables, not null
    588     * @param suffix the suffix for variables, not null
    589     * @param escape the escape character
    590     * @param valueDelimiter the variable default value delimiter string, may be null
    591     * @throws IllegalArgumentException if the prefix or suffix is null
    592     */
    593    public StringSubstitutor(final StringLookup variableResolver, final String prefix, final String suffix,
    594        final char escape, final String valueDelimiter) {
    595        this.setVariableResolver(variableResolver);
    596        this.setVariablePrefix(prefix);
    597        this.setVariableSuffix(suffix);
    598        this.setEscapeChar(escape);
    599        this.setValueDelimiter(valueDelimiter);
    600    }
    601
    602    /**
    603     * Constructs a new initialized instance.
    604     *
    605     * @param variableResolver the variable resolver, may be null
    606     * @param prefixMatcher the prefix for variables, not null
    607     * @param suffixMatcher the suffix for variables, not null
    608     * @param escape the escape character
    609     * @throws IllegalArgumentException if the prefix or suffix is null
    610     */
    611    public StringSubstitutor(final StringLookup variableResolver, final StringMatcher prefixMatcher,
    612        final StringMatcher suffixMatcher, final char escape) {
    613        this(variableResolver, prefixMatcher, suffixMatcher, escape, DEFAULT_VALUE_DELIMITER);
    614    }
    615
    616    /**
    617     * Constructs a new initialized instance.
    618     *
    619     * @param variableResolver the variable resolver, may be null
    620     * @param prefixMatcher the prefix for variables, not null
    621     * @param suffixMatcher the suffix for variables, not null
    622     * @param escape the escape character
    623     * @param valueDelimiterMatcher the variable default value delimiter matcher, may be null
    624     * @throws IllegalArgumentException if the prefix or suffix is null
    625     */
    626    public StringSubstitutor(final StringLookup variableResolver, final StringMatcher prefixMatcher,
    627        final StringMatcher suffixMatcher, final char escape, final StringMatcher valueDelimiterMatcher) {
    628        this.setVariableResolver(variableResolver);
    629        this.setVariablePrefixMatcher(prefixMatcher);
    630        this.setVariableSuffixMatcher(suffixMatcher);
    631        this.setEscapeChar(escape);
    632        this.setValueDelimiterMatcher(valueDelimiterMatcher);
    633    }
    634
    635    /**
    636     * Creates a new instance based on the given StringSubstitutor.
    637     *
    638     * @param other The StringSubstitutor used as the source.
    639     * @since 1.9
    640     */
    641    public StringSubstitutor(final StringSubstitutor other) {
    642        disableSubstitutionInValues = other.isDisableSubstitutionInValues();
    643        enableSubstitutionInVariables = other.isEnableSubstitutionInVariables();
    644        enableUndefinedVariableException = other.isEnableUndefinedVariableException();
    645        escapeChar = other.getEscapeChar();
    646        prefixMatcher = other.getVariablePrefixMatcher();
    647        preserveEscapes = other.isPreserveEscapes();
    648        suffixMatcher = other.getVariableSuffixMatcher();
    649        valueDelimiterMatcher = other.getValueDelimiterMatcher();
    650        variableResolver = other.getStringLookup();
    651    }
    652
    653    /**
    654     * Checks if the specified variable is already in the stack (list) of variables.
    655     *
    656     * @param varName the variable name to check
    657     * @param priorVariables the list of prior variables
    658     */
    659    private void checkCyclicSubstitution(final String varName, final List<String> priorVariables) {
    660        if (!priorVariables.contains(varName)) {
    661            return;
    662        }
    663        final TextStringBuilder buf = new TextStringBuilder(256);
    664        buf.append("Infinite loop in property interpolation of ");
    665        buf.append(priorVariables.remove(0));
    666        buf.append(": ");
    667        buf.appendWithSeparators(priorVariables, "->");
    668        throw new IllegalStateException(buf.toString());
    669    }
    670
    671    // Escape
    672    /**
    673     * Returns the escape character.
    674     *
    675     * @return The character used for escaping variable references
    676     */
    677    public char getEscapeChar() {
    678        return this.escapeChar;
    679    }
    680
    681    /**
    682     * Gets the StringLookup that is used to lookup variables.
    683     *
    684     * @return The StringLookup
    685     */
    686    public StringLookup getStringLookup() {
    687        return this.variableResolver;
    688    }
    689
    690    /**
    691     * Gets the variable default value delimiter matcher currently in use.
    692     * <p>
    693     * The variable default value delimiter is the character or characters that delimit the variable name and the
    694     * variable default value. This delimiter is expressed in terms of a matcher allowing advanced variable default
    695     * value delimiter matches.
    696     * </p>
    697     * <p>
    698     * If it returns null, then the variable default value resolution is disabled.
    699     *
    700     * @return The variable default value delimiter matcher in use, may be null
    701     */
    702    public StringMatcher getValueDelimiterMatcher() {
    703        return valueDelimiterMatcher;
    704    }
    705
    706    /**
    707     * Gets the variable prefix matcher currently in use.
    708     * <p>
    709     * The variable prefix is the character or characters that identify the start of a variable. This prefix is
    710     * expressed in terms of a matcher allowing advanced prefix matches.
    711     * </p>
    712     *
    713     * @return The prefix matcher in use
    714     */
    715    public StringMatcher getVariablePrefixMatcher() {
    716        return prefixMatcher;
    717    }
    718
    719    /**
    720     * Gets the variable suffix matcher currently in use.
    721     * <p>
    722     * The variable suffix is the character or characters that identify the end of a variable. This suffix is expressed
    723     * in terms of a matcher allowing advanced suffix matches.
    724     * </p>
    725     *
    726     * @return The suffix matcher in use
    727     */
    728    public StringMatcher getVariableSuffixMatcher() {
    729        return suffixMatcher;
    730    }
    731
    732    /**
    733     * Returns a flag whether substitution is disabled in variable values.If set to <b>true</b>, the values of variables
    734     * can contain other variables will not be processed and substituted original variable is evaluated, e.g.
    735     *
    736     * <pre>
    737     * Map&lt;String, String&gt; valuesMap = new HashMap&lt;&gt;();
    738     * valuesMap.put(&quot;name&quot;, &quot;Douglas ${surname}&quot;);
    739     * valuesMap.put(&quot;surname&quot;, &quot;Crockford&quot;);
    740     * String templateString = &quot;Hi ${name}&quot;;
    741     * StrSubstitutor sub = new StrSubstitutor(valuesMap);
    742     * String resolvedString = sub.replace(templateString);
    743     * </pre>
    744     *
    745     * yielding:
    746     *
    747     * <pre>
    748     *      Hi Douglas ${surname}
    749     * </pre>
    750     *
    751     * @return The substitution in variable values flag
    752     */
    753    public boolean isDisableSubstitutionInValues() {
    754        return disableSubstitutionInValues;
    755    }
    756
    757    /**
    758     * Returns a flag whether substitution is done in variable names.
    759     *
    760     * @return The substitution in variable names flag
    761     */
    762    public boolean isEnableSubstitutionInVariables() {
    763        return enableSubstitutionInVariables;
    764    }
    765
    766    /**
    767     * Returns a flag whether exception can be thrown upon undefined variable.
    768     *
    769     * @return The fail on undefined variable flag
    770     */
    771    public boolean isEnableUndefinedVariableException() {
    772        return enableUndefinedVariableException;
    773    }
    774
    775    /**
    776     * Returns the flag controlling whether escapes are preserved during substitution.
    777     *
    778     * @return The preserve escape flag
    779     */
    780    public boolean isPreserveEscapes() {
    781        return preserveEscapes;
    782    }
    783
    784    /**
    785     * Replaces all the occurrences of variables with their matching values from the resolver using the given source
    786     * array as a template. The array is not altered by this method.
    787     *
    788     * @param source the character array to replace in, not altered, null returns null
    789     * @return The result of the replace operation
    790     * @throws IllegalArgumentException if variable is not found when its allowed to throw exception
    791     */
    792    public String replace(final char[] source) {
    793        if (source == null) {
    794            return null;
    795        }
    796        final TextStringBuilder buf = new TextStringBuilder(source.length).append(source);
    797        substitute(buf, 0, source.length);
    798        return buf.toString();
    799    }
    800
    801    /**
    802     * Replaces all the occurrences of variables with their matching values from the resolver using the given source
    803     * array as a template. The array is not altered by this method.
    804     * <p>
    805     * Only the specified portion of the array will be processed. The rest of the array is not processed, and is not
    806     * returned.
    807     * </p>
    808     *
    809     * @param source the character array to replace in, not altered, null returns null
    810     * @param offset the start offset within the array, must be valid
    811     * @param length the length within the array to be processed, must be valid
    812     * @return The result of the replace operation
    813     * @throws IllegalArgumentException if variable is not found when its allowed to throw exception
    814     * @throws StringIndexOutOfBoundsException if {@code offset} is not in the
    815     *  range {@code 0 <= offset <= chars.length}
    816     * @throws StringIndexOutOfBoundsException if {@code length < 0}
    817     * @throws StringIndexOutOfBoundsException if {@code offset + length > chars.length}
    818     */
    819    public String replace(final char[] source, final int offset, final int length) {
    820        if (source == null) {
    821            return null;
    822        }
    823        final TextStringBuilder buf = new TextStringBuilder(length).append(source, offset, length);
    824        substitute(buf, 0, length);
    825        return buf.toString();
    826    }
    827
    828    /**
    829     * Replaces all the occurrences of variables with their matching values from the resolver using the given source as
    830     * a template. The source is not altered by this method.
    831     *
    832     * @param source the buffer to use as a template, not changed, null returns null
    833     * @return The result of the replace operation
    834     * @throws IllegalArgumentException if variable is not found when its allowed to throw exception
    835     */
    836    public String replace(final CharSequence source) {
    837        if (source == null) {
    838            return null;
    839        }
    840        return replace(source, 0, source.length());
    841    }
    842
    843    /**
    844     * Replaces all the occurrences of variables with their matching values from the resolver using the given source as
    845     * a template. The source is not altered by this method.
    846     * <p>
    847     * Only the specified portion of the buffer will be processed. The rest of the buffer is not processed, and is not
    848     * returned.
    849     * </p>
    850     *
    851     * @param source the buffer to use as a template, not changed, null returns null
    852     * @param offset the start offset within the array, must be valid
    853     * @param length the length within the array to be processed, must be valid
    854     * @return The result of the replace operation
    855     * @throws IllegalArgumentException if variable is not found when its allowed to throw exception
    856     */
    857    public String replace(final CharSequence source, final int offset, final int length) {
    858        if (source == null) {
    859            return null;
    860        }
    861        final TextStringBuilder buf = new TextStringBuilder(length).append(source.toString(), offset, length);
    862        substitute(buf, 0, length);
    863        return buf.toString();
    864    }
    865
    866    /**
    867     * Replaces all the occurrences of variables in the given source object with their matching values from the
    868     * resolver. The input source object is converted to a string using {@code toString} and is not altered.
    869     *
    870     * @param source the source to replace in, null returns null
    871     * @return The result of the replace operation
    872     * @throws IllegalArgumentException if a variable is not found and enableUndefinedVariableException is true
    873     */
    874    public String replace(final Object source) {
    875        if (source == null) {
    876            return null;
    877        }
    878        final TextStringBuilder buf = new TextStringBuilder().append(source);
    879        substitute(buf, 0, buf.length());
    880        return buf.toString();
    881    }
    882
    883    /**
    884     * Replaces all the occurrences of variables with their matching values from the resolver using the given source
    885     * string as a template.
    886     *
    887     * @param source the string to replace in, null returns null
    888     * @return The result of the replace operation
    889     * @throws IllegalArgumentException if variable is not found when its allowed to throw exception
    890     */
    891    public String replace(final String source) {
    892        if (source == null) {
    893            return null;
    894        }
    895        final TextStringBuilder buf = new TextStringBuilder(source);
    896        if (!substitute(buf, 0, source.length())) {
    897            return source;
    898        }
    899        return buf.toString();
    900    }
    901
    902    /**
    903     * Replaces all the occurrences of variables with their matching values from the resolver using the given source
    904     * 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
    907     * returned.
    908     * </p>
    909     *
    910     * @param source the string to replace in, null returns null
    911     * @param offset the start offset within the source, must be valid
    912     * @param length the length within the source to be processed, must be valid
    913     * @return The result of the replace operation
    914     * @throws IllegalArgumentException if variable is not found when its allowed to throw exception
    915     * @throws StringIndexOutOfBoundsException if {@code offset} is not in the
    916     *  range {@code 0 <= offset <= source.length()}
    917     * @throws StringIndexOutOfBoundsException if {@code length < 0}
    918     * @throws StringIndexOutOfBoundsException if {@code offset + length > source.length()}
    919     */
    920    public String replace(final String source, final int offset, final int length) {
    921        if (source == null) {
    922            return null;
    923        }
    924        final TextStringBuilder buf = new TextStringBuilder(length).append(source, offset, length);
    925        if (!substitute(buf, 0, length)) {
    926            return source.substring(offset, offset + length);
    927        }
    928        return buf.toString();
    929    }
    930
    931    /**
    932     * Replaces all the occurrences of variables with their matching values from the resolver using the given source
    933     * buffer as a template. The buffer is not altered by this method.
    934     *
    935     * @param source the buffer to use as a template, not changed, null returns null
    936     * @return The result of the replace operation
    937     * @throws IllegalArgumentException if variable is not found when its allowed to throw exception
    938     */
    939    public String replace(final StringBuffer source) {
    940        if (source == null) {
    941            return null;
    942        }
    943        final TextStringBuilder buf = new TextStringBuilder(source.length()).append(source);
    944        substitute(buf, 0, buf.length());
    945        return buf.toString();
    946    }
    947
    948    /**
    949     * Replaces all the occurrences of variables with their matching values from the resolver using the given source
    950     * buffer as a template. The buffer is not altered by this method.
    951     * <p>
    952     * Only the specified portion of the buffer will be processed. The rest of the buffer is not processed, and is not
    953     * returned.
    954     * </p>
    955     *
    956     * @param source the buffer to use as a template, not changed, null returns null
    957     * @param offset the start offset within the source, must be valid
    958     * @param length the length within the source to be processed, must be valid
    959     * @return The result of the replace operation
    960     * @throws IllegalArgumentException if variable is not found when its allowed to throw exception
    961     */
    962    public String replace(final StringBuffer source, final int offset, final int length) {
    963        if (source == null) {
    964            return null;
    965        }
    966        final TextStringBuilder buf = new TextStringBuilder(length).append(source, offset, length);
    967        substitute(buf, 0, length);
    968        return buf.toString();
    969    }
    970
    971    /**
    972     * Replaces all the occurrences of variables with their matching values from the resolver using the given source
    973     * builder as a template. The builder is not altered by this method.
    974     *
    975     * @param source the builder to use as a template, not changed, null returns null
    976     * @return The result of the replace operation
    977     * @throws IllegalArgumentException if variable is not found when its allowed to throw exception
    978     */
    979    public String replace(final TextStringBuilder source) {
    980        if (source == null) {
    981            return null;
    982        }
    983        final TextStringBuilder builder = new TextStringBuilder(source.length()).append(source);
    984        substitute(builder, 0, builder.length());
    985        return builder.toString();
    986    }
    987
    988    /**
    989     * Replaces all the occurrences of variables with their matching values from the resolver using the given source
    990     * builder as a template. The builder is not altered by this method.
    991     * <p>
    992     * Only the specified portion of the builder will be processed. The rest of the builder is not processed, and is not
    993     * returned.
    994     * </p>
    995     *
    996     * @param source the builder to use as a template, not changed, null returns null
    997     * @param offset the start offset within the source, must be valid
    998     * @param length the length within the source to be processed, must be valid
    999     * @return The result of the replace operation
    1000     * @throws IllegalArgumentException if variable is not found when its allowed to throw exception
    1001     */
    1002    public String replace(final TextStringBuilder source, final int offset, final int length) {
    1003        if (source == null) {
    1004            return null;
    1005        }
    1006        final TextStringBuilder buf = new TextStringBuilder(length).append(source, offset, length);
    1007        substitute(buf, 0, length);
    1008        return buf.toString();
    1009    }
    1010
    1011    /**
    1012     * Replaces all the occurrences of variables within the given source buffer with their matching values from the
    1013     * resolver. The buffer is updated with the result.
    1014     *
    1015     * @param source the buffer to replace in, updated, null returns zero
    1016     * @return true if altered
    1017     */
    1018    public boolean replaceIn(final StringBuffer source) {
    1019        if (source == null) {
    1020            return false;
    1021        }
    1022        return replaceIn(source, 0, source.length());
    1023    }
    1024
    1025    /**
    1026     * Replaces all the occurrences of variables within the given source buffer with their matching values from the
    1027     * resolver. The buffer is updated with the result.
    1028     * <p>
    1029     * Only the specified portion of the buffer will be processed. The rest of the buffer is not processed, but it is
    1030     * not deleted.
    1031     * </p>
    1032     *
    1033     * @param source the buffer to replace in, updated, null returns zero
    1034     * @param offset the start offset within the source, must be valid
    1035     * @param length the length within the source to be processed, must be valid
    1036     * @return true if altered
    1037     * @throws IllegalArgumentException if variable is not found when its allowed to throw exception
    1038     */
    1039    public boolean replaceIn(final StringBuffer source, final int offset, final int length) {
    1040        if (source == null) {
    1041            return false;
    1042        }
    1043        final TextStringBuilder buf = new TextStringBuilder(length).append(source, offset, length);
    1044        if (!substitute(buf, 0, length)) {
    1045            return false;
    1046        }
    1047        source.replace(offset, offset + length, buf.toString());
    1048        return true;
    1049    }
    1050
    1051    /**
    1052     * Replaces all the occurrences of variables within the given source buffer with their matching values from the
    1053     * resolver. The buffer is updated with the result.
    1054     *
    1055     * @param source the buffer to replace in, updated, null returns zero
    1056     * @return true if altered
    1057     */
    1058    public boolean replaceIn(final StringBuilder source) {
    1059        if (source == null) {
    1060            return false;
    1061        }
    1062        return replaceIn(source, 0, source.length());
    1063    }
    1064
    1065    /**
    1066     * Replaces all the occurrences of variables within the given source builder with their matching values from the
    1067     * resolver. The builder is updated with the result.
    1068     * <p>
    1069     * Only the specified portion of the buffer will be processed. The rest of the buffer is not processed, but it is
    1070     * not deleted.
    1071     * </p>
    1072     *
    1073     * @param source the buffer to replace in, updated, null returns zero
    1074     * @param offset the start offset within the source, must be valid
    1075     * @param length the length within the source to be processed, must be valid
    1076     * @return true if altered
    1077     * @throws IllegalArgumentException if variable is not found when its allowed to throw exception
    1078     */
    1079    public boolean replaceIn(final StringBuilder source, final int offset, final int length) {
    1080        if (source == null) {
    1081            return false;
    1082        }
    1083        final TextStringBuilder buf = new TextStringBuilder(length).append(source, offset, length);
    1084        if (!substitute(buf, 0, length)) {
    1085            return false;
    1086        }
    1087        source.replace(offset, offset + length, buf.toString());
    1088        return true;
    1089    }
    1090
    1091    /**
    1092     * Replaces all the occurrences of variables within the given source builder with their matching values from the
    1093     * resolver.
    1094     *
    1095     * @param source the builder to replace in, updated, null returns zero
    1096     * @return true if altered
    1097     * @throws IllegalArgumentException if variable is not found when its allowed to throw exception
    1098     */
    1099    public boolean replaceIn(final TextStringBuilder source) {
    1100        if (source == null) {
    1101            return false;
    1102        }
    1103        return substitute(source, 0, source.length());
    1104    }
    1105
    1106    /**
    1107     * Replaces all the occurrences of variables within the given source builder with their matching values from the
    1108     * resolver.
    1109     * <p>
    1110     * Only the specified portion of the builder will be processed. The rest of the builder is not processed, but it is
    1111     * not deleted.
    1112     * </p>
    1113     *
    1114     * @param source the builder to replace in, null returns zero
    1115     * @param offset the start offset within the source, must be valid
    1116     * @param length the length within the source to be processed, must be valid
    1117     * @return true if altered
    1118     * @throws IllegalArgumentException if variable is not found when its allowed to throw exception
    1119     */
    1120    public boolean replaceIn(final TextStringBuilder source, final int offset, final int length) {
    1121        if (source == null) {
    1122            return false;
    1123        }
    1124        return substitute(source, offset, length);
    1125    }
    1126
    1127    /**
    1128     * Internal method that resolves the value of a variable.
    1129     * <p>
    1130     * Most users of this class do not need to call this method. This method is called automatically by the substitution
    1131     * process.
    1132     * </p>
    1133     * <p>
    1134     * Writers of subclasses can override this method if they need to alter how each substitution occurs. The method is
    1135     * passed the variable's name and must return the corresponding value. This implementation uses the
    1136     * {@link #getStringLookup()} with the variable's name as the key.
    1137     * </p>
    1138     *
    1139     * @param variableName the name of the variable, not null
    1140     * @param buf the buffer where the substitution is occurring, not null
    1141     * @param startPos the start position of the variable including the prefix, valid
    1142     * @param endPos the end position of the variable including the suffix, valid
    1143     * @return The variable's value or <b>null</b> if the variable is unknown
    1144     */
    1145    protected String resolveVariable(final String variableName, final TextStringBuilder buf, final int startPos,
    1146        final int endPos) {
    1147        final StringLookup resolver = getStringLookup();
    1148        if (resolver == null) {
    1149            return null;
    1150        }
    1151        return resolver.lookup(variableName);
    1152    }
    1153
    1154    /**
    1155     * Sets a flag whether substitution is done in variable values (recursive).
    1156     *
    1157     * @param disableSubstitutionInValues true if substitution in variable value are disabled
    1158     * @return this, to enable chaining
    1159     */
    1160    public StringSubstitutor setDisableSubstitutionInValues(final boolean disableSubstitutionInValues) {
    1161        this.disableSubstitutionInValues = disableSubstitutionInValues;
    1162        return this;
    1163    }
    1164
    1165    /**
    1166     * Sets a flag whether substitution is done in variable names. If set to <b>true</b>, the names of variables can
    1167     * contain other variables which are processed first before the original variable is evaluated, e.g.
    1168     * {@code ${jre-${java.version}}}. The default value is <b>false</b>.
    1169     *
    1170     * @param enableSubstitutionInVariables the new value of the flag
    1171     * @return this, to enable chaining
    1172     */
    1173    public StringSubstitutor setEnableSubstitutionInVariables(final boolean enableSubstitutionInVariables) {
    1174        this.enableSubstitutionInVariables = enableSubstitutionInVariables;
    1175        return this;
    1176    }
    1177
    1178    /**
    1179     * Sets a flag whether exception should be thrown if any variable is undefined.
    1180     *
    1181     * @param failOnUndefinedVariable true if exception should be thrown on undefined variable
    1182     * @return this, to enable chaining
    1183     */
    1184    public StringSubstitutor setEnableUndefinedVariableException(final boolean failOnUndefinedVariable) {
    1185        this.enableUndefinedVariableException = failOnUndefinedVariable;
    1186        return this;
    1187    }
    1188
    1189    /**
    1190     * Sets the escape character. If this character is placed before a variable reference in the source text, this
    1191     * variable will be ignored.
    1192     *
    1193     * @param escapeCharacter the escape character (0 for disabling escaping)
    1194     * @return this, to enable chaining
    1195     */
    1196    public StringSubstitutor setEscapeChar(final char escapeCharacter) {
    1197        this.escapeChar = escapeCharacter;
    1198        return this;
    1199    }
    1200
    1201    /**
    1202     * Sets a flag controlling whether escapes are preserved during substitution. If set to <b>true</b>, the escape
    1203     * character is retained during substitution (e.g. {@code $${this-is-escaped}} remains {@code $${this-is-escaped}}).
    1204     * If set to <b>false</b>, the escape character is removed during substitution (e.g. {@code $${this-is-escaped}}
    1205     * becomes {@code ${this-is-escaped}}). The default value is <b>false</b>
    1206     *
    1207     * @param preserveEscapes true if escapes are to be preserved
    1208     * @return this, to enable chaining
    1209     */
    1210    public StringSubstitutor setPreserveEscapes(final boolean preserveEscapes) {
    1211        this.preserveEscapes = preserveEscapes;
    1212        return this;
    1213    }
    1214
    1215    /**
    1216     * Sets the variable default value delimiter to use.
    1217     * <p>
    1218     * The variable default value delimiter is the character or characters that delimit the variable name and the
    1219     * variable default value. This method allows a single character variable default value delimiter to be easily set.
    1220     * </p>
    1221     *
    1222     * @param valueDelimiter the variable default value delimiter character to use
    1223     * @return this, to enable chaining
    1224     */
    1225    public StringSubstitutor setValueDelimiter(final char valueDelimiter) {
    1226        return setValueDelimiterMatcher(StringMatcherFactory.INSTANCE.charMatcher(valueDelimiter));
    1227    }
    1228
    1229    /**
    1230     * Sets the variable default value delimiter to use.
    1231     * <p>
    1232     * The variable default value delimiter is the character or characters that delimit the variable name and the
    1233     * variable default value. This method allows a string variable default value delimiter to be easily set.
    1234     * </p>
    1235     * <p>
    1236     * If the {@code valueDelimiter} is null or empty string, then the variable default value resolution becomes
    1237     * disabled.
    1238     * </p>
    1239     *
    1240     * @param valueDelimiter the variable default value delimiter string to use, may be null or empty
    1241     * @return this, to enable chaining
    1242     */
    1243    public StringSubstitutor setValueDelimiter(final String valueDelimiter) {
    1244        if (valueDelimiter == null || valueDelimiter.isEmpty()) {
    1245            setValueDelimiterMatcher(null);
    1246            return this;
    1247        }
    1248        return setValueDelimiterMatcher(StringMatcherFactory.INSTANCE.stringMatcher(valueDelimiter));
    1249    }
    1250
    1251    /**
    1252     * Sets the variable default value delimiter matcher to use.
    1253     * <p>
    1254     * The variable default value delimiter is the character or characters that delimit the variable name and the
    1255     * variable default value. This delimiter is expressed in terms of a matcher allowing advanced variable default
    1256     * value delimiter matches.
    1257     * </p>
    1258     * <p>
    1259     * If the {@code valueDelimiterMatcher} is null, then the variable default value resolution becomes disabled.
    1260     * </p>
    1261     *
    1262     * @param valueDelimiterMatcher variable default value delimiter matcher to use, may be null
    1263     * @return this, to enable chaining
    1264     */
    1265    public StringSubstitutor setValueDelimiterMatcher(final StringMatcher valueDelimiterMatcher) {
    1266        this.valueDelimiterMatcher = valueDelimiterMatcher;
    1267        return this;
    1268    }
    1269
    1270    /**
    1271     * Sets the variable prefix to use.
    1272     * <p>
    1273     * The variable prefix is the character or characters that identify the start of a variable. This method allows a
    1274     * single character prefix to be easily set.
    1275     * </p>
    1276     *
    1277     * @param prefix the prefix character to use
    1278     * @return this, to enable chaining
    1279     */
    1280    public StringSubstitutor setVariablePrefix(final char prefix) {
    1281        return setVariablePrefixMatcher(StringMatcherFactory.INSTANCE.charMatcher(prefix));
    1282    }
    1283
    1284    /**
    1285     * Sets the variable prefix to use.
    1286     * <p>
    1287     * The variable prefix is the character or characters that identify the start of a variable. This method allows a
    1288     * string prefix to be easily set.
    1289     * </p>
    1290     *
    1291     * @param prefix the prefix for variables, not null
    1292     * @return this, to enable chaining
    1293     * @throws IllegalArgumentException if the prefix is null
    1294     */
    1295    public StringSubstitutor setVariablePrefix(final String prefix) {
    1296        Validate.isTrue(prefix != null, "Variable prefix must not be null!");
    1297        return setVariablePrefixMatcher(StringMatcherFactory.INSTANCE.stringMatcher(prefix));
    1298    }
    1299
    1300    /**
    1301     * Sets the variable prefix matcher currently in use.
    1302     * <p>
    1303     * The variable prefix is the character or characters that identify the start of a variable. This prefix is
    1304     * expressed in terms of a matcher allowing advanced prefix matches.
    1305     * </p>
    1306     *
    1307     * @param prefixMatcher the prefix matcher to use, null ignored
    1308     * @return this, to enable chaining
    1309     * @throws IllegalArgumentException if the prefix matcher is null
    1310     */
    1311    public StringSubstitutor setVariablePrefixMatcher(final StringMatcher prefixMatcher) {
    1312        Validate.isTrue(prefixMatcher != null, "Variable prefix matcher must not be null!");
    1313        this.prefixMatcher = prefixMatcher;
    1314        return this;
    1315    }
    1316
    1317    /**
    1318     * Sets the VariableResolver that is used to lookup variables.
    1319     *
    1320     * @param variableResolver the VariableResolver
    1321     * @return this, to enable chaining
    1322     */
    1323    public StringSubstitutor setVariableResolver(final StringLookup variableResolver) {
    1324        this.variableResolver = variableResolver;
    1325        return this;
    1326    }
    1327
    1328    /**
    1329     * Sets the variable suffix to use.
    1330     * <p>
    1331     * The variable suffix is the character or characters that identify the end of a variable. This method allows a
    1332     * single character suffix to be easily set.
    1333     * </p>
    1334     *
    1335     * @param suffix the suffix character to use
    1336     * @return this, to enable chaining
    1337     */
    1338    public StringSubstitutor setVariableSuffix(final char suffix) {
    1339        return setVariableSuffixMatcher(StringMatcherFactory.INSTANCE.charMatcher(suffix));
    1340    }
    1341
    1342    /**
    1343     * Sets the variable suffix to use.
    1344     * <p>
    1345     * The variable suffix is the character or characters that identify the end of a variable. This method allows a
    1346     * string suffix to be easily set.
    1347     * </p>
    1348     *
    1349     * @param suffix the suffix for variables, not null
    1350     * @return this, to enable chaining
    1351     * @throws IllegalArgumentException if the suffix is null
    1352     */
    1353    public StringSubstitutor setVariableSuffix(final String suffix) {
    1354        Validate.isTrue(suffix != null, "Variable suffix must not be null!");
    1355        return setVariableSuffixMatcher(StringMatcherFactory.INSTANCE.stringMatcher(suffix));
    1356    }
    1357
    1358    /**
    1359     * Sets the variable suffix matcher currently in use.
    1360     * <p>
    1361     * The variable suffix is the character or characters that identify the end of a variable. This suffix is expressed
    1362     * in terms of a matcher allowing advanced suffix matches.
    1363     * </p>
    1364     *
    1365     * @param suffixMatcher the suffix matcher to use, null ignored
    1366     * @return this, to enable chaining
    1367     * @throws IllegalArgumentException if the suffix matcher is null
    1368     */
    1369    public StringSubstitutor setVariableSuffixMatcher(final StringMatcher suffixMatcher) {
    1370        Validate.isTrue(suffixMatcher != null, "Variable suffix matcher must not be null!");
    1371        this.suffixMatcher = suffixMatcher;
    1372        return this;
    1373    }
    1374
    1375    /**
    1376     * Internal method that substitutes the variables.
    1377     * <p>
    1378     * Most users of this class do not need to call this method. This method will be called automatically by another
    1379     * (public) method.
    1380     * </p>
    1381     * <p>
    1382     * Writers of subclasses can override this method if they need access to the substitution process at the start or
    1383     * end.
    1384     * </p>
    1385     *
    1386     * @param builder the string builder to substitute into, not null
    1387     * @param offset the start offset within the builder, must be valid
    1388     * @param length the length within the builder to be processed, must be valid
    1389     * @return true if altered
    1390     */
    1391    protected boolean substitute(final TextStringBuilder builder, final int offset, final int length) {
    1392        return substitute(builder, offset, length, null).altered;
    1393    }
    1394
    1395    /**
    1396     * Recursive handler for multiple levels of interpolation. This is the main interpolation method, which resolves the
    1397     * values of all variable references contained in the passed in text.
    1398     *
    1399     * @param builder the string builder to substitute into, not null
    1400     * @param offset the start offset within the builder, must be valid
    1401     * @param length the length within the builder to be processed, must be valid
    1402     * @param priorVariables the stack keeping track of the replaced variables, may be null
    1403     * @return The result.
    1404     * @throws IllegalArgumentException if variable is not found and <pre>isEnableUndefinedVariableException()==true</pre>
    1405     * @since 1.9
    1406     */
    1407    private Result substitute(final TextStringBuilder builder, final int offset, final int length,
    1408        List<String> priorVariables) {
    1409        Objects.requireNonNull(builder, "builder");
    1410        final StringMatcher prefixMatcher = getVariablePrefixMatcher();
    1411        final StringMatcher suffixMatcher = getVariableSuffixMatcher();
    1412        final char escapeCh = getEscapeChar();
    1413        final StringMatcher valueDelimMatcher = getValueDelimiterMatcher();
    1414        final boolean substitutionInVariablesEnabled = isEnableSubstitutionInVariables();
    1415        final boolean substitutionInValuesDisabled = isDisableSubstitutionInValues();
    1416        final boolean undefinedVariableException = isEnableUndefinedVariableException();
    1417        final boolean preserveEscapes = isPreserveEscapes();
    1418
    1419        boolean altered = false;
    1420        int lengthChange = 0;
    1421        int bufEnd = offset + length;
    1422        int pos = offset;
    1423        int escPos = -1;
    1424        outer: while (pos < bufEnd) {
    1425            final int startMatchLen = prefixMatcher.isMatch(builder, pos, offset, bufEnd);
    1426            if (startMatchLen == 0) {
    1427                pos++;
    1428            } else {
    1429                // found variable start marker
    1430                if (pos > offset && builder.charAt(pos - 1) == escapeCh) {
    1431                    // escape detected
    1432                    if (preserveEscapes) {
    1433                        // keep escape
    1434                        pos++;
    1435                        continue;
    1436                    }
    1437                    // mark esc ch for deletion if we find a complete variable
    1438                    escPos = pos - 1;
    1439                }
    1440                // find suffix
    1441                int startPos = pos;
    1442                pos += startMatchLen;
    1443                int endMatchLen = 0;
    1444                int nestedVarCount = 0;
    1445                while (pos < bufEnd) {
    1446                    if (substitutionInVariablesEnabled && prefixMatcher.isMatch(builder, pos, offset, bufEnd) != 0) {
    1447                        // found a nested variable start
    1448                        endMatchLen = prefixMatcher.isMatch(builder, pos, offset, bufEnd);
    1449                        nestedVarCount++;
    1450                        pos += endMatchLen;
    1451                        continue;
    1452                    }
    1453
    1454                    endMatchLen = suffixMatcher.isMatch(builder, pos, offset, bufEnd);
    1455                    if (endMatchLen == 0) {
    1456                        pos++;
    1457                    } else {
    1458                        // found variable end marker
    1459                        if (nestedVarCount == 0) {
    1460                            if (escPos >= 0) {
    1461                                // delete escape
    1462                                builder.deleteCharAt(escPos);
    1463                                escPos = -1;
    1464                                lengthChange--;
    1465                                altered = true;
    1466                                bufEnd--;
    1467                                pos = startPos + 1;
    1468                                startPos--;
    1469                                continue outer;
    1470                            }
    1471                            // get var name
    1472                            String varNameExpr = builder.midString(startPos + startMatchLen,
    1473                                pos - startPos - startMatchLen);
    1474                            if (substitutionInVariablesEnabled) {
    1475                                final TextStringBuilder bufName = new TextStringBuilder(varNameExpr);
    1476                                substitute(bufName, 0, bufName.length());
    1477                                varNameExpr = bufName.toString();
    1478                            }
    1479                            pos += endMatchLen;
    1480                            final int endPos = pos;
    1481
    1482                            String varName = varNameExpr;
    1483                            String varDefaultValue = null;
    1484
    1485                            if (valueDelimMatcher != null) {
    1486                                final char[] varNameExprChars = varNameExpr.toCharArray();
    1487                                int valueDelimiterMatchLen = 0;
    1488                                for (int i = 0; i < varNameExprChars.length; i++) {
    1489                                    // if there's any nested variable when nested variable substitution disabled,
    1490                                    // then stop resolving name and default value.
    1491                                    if (!substitutionInVariablesEnabled && prefixMatcher.isMatch(varNameExprChars, i, i,
    1492                                        varNameExprChars.length) != 0) {
    1493                                        break;
    1494                                    }
    1495                                    if (valueDelimMatcher.isMatch(varNameExprChars, i, 0,
    1496                                        varNameExprChars.length) != 0) {
    1497                                        valueDelimiterMatchLen = valueDelimMatcher.isMatch(varNameExprChars, i, 0,
    1498                                            varNameExprChars.length);
    1499                                        varName = varNameExpr.substring(0, i);
    1500                                        varDefaultValue = varNameExpr.substring(i + valueDelimiterMatchLen);
    1501                                        break;
    1502                                    }
    1503                                }
    1504                            }
    1505
    1506                            // on the first call initialize priorVariables
    1507                            if (priorVariables == null) {
    1508                                priorVariables = new ArrayList<>();
    1509                                priorVariables.add(builder.midString(offset, length));
    1510                            }
    1511
    1512                            // handle cyclic substitution
    1513                            checkCyclicSubstitution(varName, priorVariables);
    1514                            priorVariables.add(varName);
    1515
    1516                            // resolve the variable
    1517                            String varValue = resolveVariable(varName, builder, startPos, endPos);
    1518                            if (varValue == null) {
    1519                                varValue = varDefaultValue;
    1520                            }
    1521                            if (varValue != null) {
    1522                                final int varLen = varValue.length();
    1523                                builder.replace(startPos, endPos, varValue);
    1524                                altered = true;
    1525                                int change = 0;
    1526                                if (!substitutionInValuesDisabled) { // recursive replace
    1527                                    change = substitute(builder, startPos, varLen, priorVariables).lengthChange;
    1528                                }
    1529                                change = change + varLen - (endPos - startPos);
    1530                                pos += change;
    1531                                bufEnd += change;
    1532                                lengthChange += change;
    1533                            } else if (undefinedVariableException) {
    1534                                throw new IllegalArgumentException(
    1535                                    String.format("Cannot resolve variable '%s' (enableSubstitutionInVariables=%s).",
    1536                                        varName, substitutionInVariablesEnabled));
    1537                            }
    1538
    1539                            // remove variable from the cyclic stack
    1540                            priorVariables.remove(priorVariables.size() - 1);
    1541                            break;
    1542                        }
    1543                        nestedVarCount--;
    1544                        pos += endMatchLen;
    1545                    }
    1546                }
    1547            }
    1548        }
    1549        return new Result(altered, lengthChange);
    1550    }
    1551
    1552    /**
    1553     * Returns a string representation of the object.
    1554     *
    1555     * @return a string representation of the object.
    1556     * @since 1.11.0
    1557     */
    1558    @Override
    1559    public String toString() {
    1560        final StringBuilder builder = new StringBuilder();
    1561        builder.append("StringSubstitutor [disableSubstitutionInValues=").append(disableSubstitutionInValues).append(", enableSubstitutionInVariables=")
    1562                .append(enableSubstitutionInVariables).append(", enableUndefinedVariableException=").append(enableUndefinedVariableException)
    1563                .append(", escapeChar=").append(escapeChar).append(", prefixMatcher=").append(prefixMatcher).append(", preserveEscapes=")
    1564                .append(preserveEscapes).append(", suffixMatcher=").append(suffixMatcher).append(", valueDelimiterMatcher=").append(valueDelimiterMatcher)
    1565                .append(", variableResolver=").append(variableResolver).append("]");
    1566        return builder.toString();
    1567    }
    1568}