Source code
Description: source: package: org.apache.commons.text, class: StringSubstitutor

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