    001/*
    002 * Licensed to the Apache Software Foundation (ASF) under one or more
    003 * contributor license agreements.  See the NOTICE file distributed with
    004 * this work for additional information regarding copyright ownership.
    005 * The ASF licenses this file to You under the Apache License, Version 2.0
    006 * (the "License"); you may not use this file except in compliance with
    007 * the License.  You may obtain a copy of the License at
    008 *
    009 *      https://www.apache.org/licenses/LICENSE-2.0
    010 *
    011 * Unless required by applicable law or agreed to in writing, software
    012 * distributed under the License is distributed on an "AS IS" BASIS,
    013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    014 * See the License for the specific language governing permissions and
    015 * limitations under the License.
    016 */
    017package org.apache.commons.text;
    018
    019import java.util.ArrayList;
    020import java.util.List;
    021import java.util.Map;
    022import java.util.Properties;
    023import java.util.function.Function;
    024import java.util.stream.Collectors;
    025
    026import org.apache.commons.lang3.Validate;
    027
    028/**
    029 * Substitutes variables within a string by values.
    030 * <p>
    031 * This class takes a piece of text and substitutes all the variables within it.
    032 * The default definition of a variable is {@code ${variableName}}.
    033 * The prefix and suffix can be changed via constructors and set methods.
    034 * <p>
    035 * Variable values are typically resolved from a map, but could also be resolved
    036 * from system properties, or by supplying a custom variable resolver.
    037 * <p>
    038 * The simplest example is to use this class to replace Java System properties. For example:
    039 * <pre>
    040 * StrSubstitutor.replaceSystemProperties(
    041 *      "You are running with java.version = ${java.version} and os.name = ${os.name}.");
    042 * </pre>
    043 * <p>
    044 * Typical usage of this class follows the following pattern: First an instance is created
    045 * and initialized with the map that contains the values for the available variables.
    046 * If a prefix and/or suffix for variables should be used other than the default ones,
    047 * the appropriate settings can be performed. After that the {@code replace()}
    048 * method can be called passing in the source text for interpolation. In the returned
    049 * text all variable references (as long as their values are known) will be resolved.
    050 * The following example demonstrates this:
    051 * <pre>
    052 * Map&lt;String, String&gt; valuesMap = new HashMap&lt;&gt;();
    053 * valuesMap.put(&quot;animal&quot;, &quot;quick brown fox&quot;);
    054 * valuesMap.put(&quot;target&quot;, &quot;lazy dog&quot;);
    055 * String templateString = &quot;The ${animal} jumped over the ${target}.&quot;;
    056 * StrSubstitutor sub = new StrSubstitutor(valuesMap);
    057 * String resolvedString = sub.replace(templateString);
    058 * </pre>
    059 * yielding:
    060 * <pre>
    061 *      The quick brown fox jumped over the lazy dog.
    062 * </pre>
    063 * <p>
    064 * Also, this class allows to set a default value for unresolved variables.
    065 * The default value for a variable can be appended to the variable name after the variable
    066 * default value delimiter. The default value of the variable default value delimiter is ':-',
    067 * as in bash and other *nix shells, as those are arguably where the default ${} delimiter set originated.
    068 * The variable default value delimiter can be manually set by calling {@link #setValueDelimiterMatcher(StrMatcher)},
    069 * {@link #setValueDelimiter(char)} or {@link #setValueDelimiter(String)}.
    070 * The following shows an example with variable default value settings:
    071 * <pre>
    072 * Map&lt;String, String&gt; valuesMap = new HashMap&lt;&gt;();
    073 * valuesMap.put(&quot;animal&quot;, &quot;quick brown fox&quot;);
    074 * valuesMap.put(&quot;target&quot;, &quot;lazy dog&quot;);
    075 * String templateString = &quot;The ${animal} jumped over the ${target}. ${undefined.number:-1234567890}.&quot;;
    076 * StrSubstitutor sub = new StrSubstitutor(valuesMap);
    077 * String resolvedString = sub.replace(templateString);
    078 * </pre>
    079 * yielding:
    080 * <pre>
    081 *      The quick brown fox jumped over the lazy dog. 1234567890.
    082 * </pre>
    083 * <p>
    084 * In addition to this usage pattern there are some static convenience methods that
    085 * cover the most common use cases. These methods can be used without the need of
    086 * manually creating an instance. However if multiple replace operations are to be
    087 * performed, creating and reusing an instance of this class will be more efficient.
    088 * <p>
    089 * Variable replacement works in a recursive way. Thus, if a variable value contains
    090 * a variable then that variable will also be replaced. Cyclic replacements are
    091 * detected and will cause an exception to be thrown.
    092 * <p>
    093 * Sometimes the interpolation's result must contain a variable prefix. As an example
    094 * take the following source text:
    095 * <pre>
    096 *   The variable ${${name}} must be used.
    097 * </pre>
    098 * Here only the variable's name referred to in the text should be replaced resulting
    099 * in the text (assuming that the value of the {@code name} variable is {@code x}):
    100 * <pre>
    101 *   The variable ${x} must be used.
    102 * </pre>
    103 * To achieve this effect there are two possibilities: Either set a different prefix
    104 * and suffix for variables which do not conflict with the result text you want to
    105 * produce. The other possibility is to use the escape character, by default '$'.
    106 * If this character is placed before a variable reference, this reference is ignored
    107 * and won't be replaced. For example:
    108 * <pre>
    109 *   The variable $${${name}} must be used.
    110 * </pre>
    111 * <p>
    112 * In some complex scenarios you might even want to perform substitution in the
    113 * names of variables, for instance
    114 * <pre>
    115 * ${jre-${java.specification.version}}
    116 * </pre>
    117 * {@code StrSubstitutor} supports this recursive substitution in variable
    118 * names, but it has to be enabled explicitly by setting the
    119 * {@link #setEnableSubstitutionInVariables(boolean) enableSubstitutionInVariables}
    120 * property to <strong>true</strong>.
    121 * <p>This class is <strong>not</strong> thread safe.</p>
    122 *
    123 * @since 1.0
    124 * @deprecated Deprecated as of 1.3, use {@link StringSubstitutor} instead. This class will be removed in 2.0.
    125 */
    126@Deprecated
    127public class StrSubstitutor {
    128
    129    /**
    130     * Constant for the default escape character.
    131     */
    132    public static final char DEFAULT_ESCAPE = '$';
    133
    134    /**
    135     * Constant for the default variable prefix.
    136     */
    137    public static final StrMatcher DEFAULT_PREFIX = StrMatcher.stringMatcher("${");
    138
    139    /**
    140     * Constant for the default variable suffix.
    141     */
    142    public static final StrMatcher DEFAULT_SUFFIX = StrMatcher.stringMatcher("}");
    143
    144    /**
    145     * Constant for the default value delimiter of a variable.
    146     */
    147    public static final StrMatcher DEFAULT_VALUE_DELIMITER = StrMatcher.stringMatcher(":-");
    148
    149    /**
    150     * Replaces all the occurrences of variables in the given source object with
    151     * their matching values from the map.
    152     *
    153     * @param <V> the type of the values in the map.
    154     * @param source  the source text containing the variables to substitute, null returns null.
    155     * @param valueMap  the map with the values, may be null.
    156     * @return The result of the replace operation.
    157     */
    158    public static <V> String replace(final Object source, final Map<String, V> valueMap) {
    159        return new StrSubstitutor(valueMap).replace(source);
    160    }
    161
    162    /**
    163     * Replaces all the occurrences of variables in the given source object with
    164     * their matching values from the map. This method allows to specify a
    165     * custom variable prefix and suffix
    166     *
    167     * @param <V> the type of the values in the map.
    168     * @param source  the source text containing the variables to substitute, null returns null.
    169     * @param valueMap  the map with the values, may be null.
    170     * @param prefix  the prefix of variables, not null.
    171     * @param suffix  the suffix of variables, not null.
    172     * @return The result of the replace operation.
    173     * @throws IllegalArgumentException if the prefix or suffix is null.
    174     */
    175    public static <V> String replace(final Object source,
    176                                     final Map<String, V> valueMap,
    177                                     final String prefix,
    178                                     final String suffix) {
    179        return new StrSubstitutor(valueMap, prefix, suffix).replace(source);
    180    }
    181
    182    /**
    183     * Replaces all the occurrences of variables in the given source object with their matching
    184     * values from the properties.
    185     *
    186     * @param source the source text containing the variables to substitute, null returns null.
    187     * @param valueProperties the properties with values, may be null.
    188     * @return The result of the replace operation.
    189     */
    190    public static String replace(final Object source, final Properties valueProperties) {
    191        if (valueProperties == null) {
    192            return source.toString();
    193        }
    194        return replace(source, valueProperties.stringPropertyNames().stream().collect(Collectors.toMap(Function.identity(), valueProperties::getProperty)));
    195    }
    196
    197    /**
    198     * Replaces all the occurrences of variables in the given source object with
    199     * their matching values from the system properties.
    200     *
    201     * @param source  the source text containing the variables to substitute, null returns null.
    202     * @return The result of the replace operation.
    203     */
    204    public static String replaceSystemProperties(final Object source) {
    205        return new StrSubstitutor(StrLookup.systemPropertiesLookup()).replace(source);
    206    }
    207
    208    /**
    209     * Stores the escape character.
    210     */
    211    private char escapeChar;
    212
    213    /**
    214     * Stores the variable prefix.
    215     */
    216    private StrMatcher prefixMatcher;
    217
    218    /**
    219     * Stores the variable suffix.
    220     */
    221    private StrMatcher suffixMatcher;
    222
    223    /**
    224     * Stores the default variable value delimiter.
    225     */
    226    private StrMatcher valueDelimiterMatcher;
    227
    228    /**
    229     * Variable resolution is delegated to an implementor of VariableResolver.
    230     */
    231    private StrLookup<?> variableResolver;
    232
    233    /**
    234     * The flag whether substitution in variable names is enabled.
    235     */
    236    private boolean enableSubstitutionInVariables;
    237
    238    /**
    239     * Whether escapes should be preserved.  Default is false;
    240     */
    241    private boolean preserveEscapes;
    242
    243    /**
    244     * The flag whether substitution in variable values is disabled.
    245     */
    246    private boolean disableSubstitutionInValues;
    247
    248    /**
    249     * Constructs a new instance with defaults for variable prefix and suffix
    250     * and the escaping character.
    251     */
    252    public StrSubstitutor() {
    253        this(null, DEFAULT_PREFIX, DEFAULT_SUFFIX, DEFAULT_ESCAPE);
    254    }
    255
    256    /**
    257     * Constructs a new instance and initializes it. Uses defaults for variable
    258     * prefix and suffix and the escaping character.
    259     *
    260     * @param <V> the type of the values in the map.
    261     * @param valueMap  the map with the variables' values, may be null.
    262     */
    263    public <V> StrSubstitutor(final Map<String, V> valueMap) {
    264        this(StrLookup.mapLookup(valueMap), DEFAULT_PREFIX, DEFAULT_SUFFIX, DEFAULT_ESCAPE);
    265    }
    266
    267    /**
    268     * Constructs a new instance and initializes it. Uses a default escaping character.
    269     *
    270     * @param <V> the type of the values in the map.
    271     * @param valueMap  the map with the variables' values, may be null.
    272     * @param prefix  the prefix for variables, not null.
    273     * @param suffix  the suffix for variables, not null.
    274     * @throws IllegalArgumentException if the prefix or suffix is null.
    275     */
    276    public <V> StrSubstitutor(final Map<String, V> valueMap, final String prefix, final String suffix) {
    277        this(StrLookup.mapLookup(valueMap), prefix, suffix, DEFAULT_ESCAPE);
    278    }
    279
    280    /**
    281     * Constructs a new instance and initializes it.
    282     *
    283     * @param <V> the type of the values in the map.
    284     * @param valueMap  the map with the variables' values, may be null.
    285     * @param prefix  the prefix for variables, not null.
    286     * @param suffix  the suffix for variables, not null.
    287     * @param escape  the escape character.
    288     * @throws IllegalArgumentException if the prefix or suffix is null.
    289     */
    290    public <V> StrSubstitutor(final Map<String, V> valueMap, final String prefix, final String suffix,
    291                              final char escape) {
    292        this(StrLookup.mapLookup(valueMap), prefix, suffix, escape);
    293    }
    294
    295    /**
    296     * Constructs a new instance and initializes it.
    297     *
    298     * @param <V> the type of the values in the map.
    299     * @param valueMap  the map with the variables' values, may be null.
    300     * @param prefix  the prefix for variables, not null.
    301     * @param suffix  the suffix for variables, not null.
    302     * @param escape  the escape character.
    303     * @param valueDelimiter  the variable default value delimiter, may be null.
    304     * @throws IllegalArgumentException if the prefix or suffix is null.
    305     */
    306    public <V> StrSubstitutor(final Map<String, V> valueMap, final String prefix, final String suffix,
    307                              final char escape, final String valueDelimiter) {
    308        this(StrLookup.mapLookup(valueMap), prefix, suffix, escape, valueDelimiter);
    309    }
    310
    311    /**
    312     * Constructs a new instance and initializes it.
    313     *
    314     * @param variableResolver  the variable resolver, may be null.
    315     */
    316    public StrSubstitutor(final StrLookup<?> variableResolver) {
    317        this(variableResolver, DEFAULT_PREFIX, DEFAULT_SUFFIX, DEFAULT_ESCAPE);
    318    }
    319
    320    /**
    321     * Constructs a new instance and initializes it.
    322     *
    323     * @param variableResolver  the variable resolver, may be null.
    324     * @param prefix  the prefix for variables, not null.
    325     * @param suffix  the suffix for variables, not null.
    326     * @param escape  the escape character.
    327     * @throws IllegalArgumentException if the prefix or suffix is null.
    328     */
    329    public StrSubstitutor(final StrLookup<?> variableResolver, final String prefix, final String suffix,
    330                          final char escape) {
    331        setVariableResolver(variableResolver);
    332        setVariablePrefix(prefix);
    333        setVariableSuffix(suffix);
    334        setEscapeChar(escape);
    335        setValueDelimiterMatcher(DEFAULT_VALUE_DELIMITER);
    336    }
    337
    338    /**
    339     * Constructs a new instance and initializes it.
    340     *
    341     * @param variableResolver  the variable resolver, may be null.
    342     * @param prefix  the prefix for variables, not null.
    343     * @param suffix  the suffix for variables, not null.
    344     * @param escape  the escape character.
    345     * @param valueDelimiter  the variable default value delimiter string, may be null.
    346     * @throws IllegalArgumentException if the prefix or suffix is null.
    347     */
    348    public StrSubstitutor(final StrLookup<?> variableResolver, final String prefix, final String suffix,
    349                          final char escape, final String valueDelimiter) {
    350        setVariableResolver(variableResolver);
    351        setVariablePrefix(prefix);
    352        setVariableSuffix(suffix);
    353        setEscapeChar(escape);
    354        setValueDelimiter(valueDelimiter);
    355    }
    356
    357    /**
    358     * Constructs a new instance and initializes it.
    359     *
    360     * @param variableResolver  the variable resolver, may be null.
    361     * @param prefixMatcher  the prefix for variables, not null.
    362     * @param suffixMatcher  the suffix for variables, not null.
    363     * @param escape  the escape character.
    364     * @throws IllegalArgumentException if the prefix or suffix is null.
    365     */
    366    public StrSubstitutor(
    367            final StrLookup<?> variableResolver, final StrMatcher prefixMatcher, final StrMatcher suffixMatcher,
    368            final char escape) {
    369        this(variableResolver, prefixMatcher, suffixMatcher, escape, DEFAULT_VALUE_DELIMITER);
    370    }
    371
    372    /**
    373     * Constructs a new instance and initializes it.
    374     *
    375     * @param variableResolver  the variable resolver, may be null.
    376     * @param prefixMatcher  the prefix for variables, not null.
    377     * @param suffixMatcher  the suffix for variables, not null.
    378     * @param escape  the escape character.
    379     * @param valueDelimiterMatcher  the variable default value delimiter matcher, may be null.
    380     * @throws IllegalArgumentException if the prefix or suffix is null.
    381     */
    382    public StrSubstitutor(
    383            final StrLookup<?> variableResolver, final StrMatcher prefixMatcher, final StrMatcher suffixMatcher,
    384            final char escape, final StrMatcher valueDelimiterMatcher) {
    385        setVariableResolver(variableResolver);
    386        setVariablePrefixMatcher(prefixMatcher);
    387        setVariableSuffixMatcher(suffixMatcher);
    388        setEscapeChar(escape);
    389        setValueDelimiterMatcher(valueDelimiterMatcher);
    390    }
    391
    392    /**
    393     * Checks if the specified variable is already in the stack (list) of variables.
    394     *
    395     * @param varName  the variable name to check.
    396     * @param priorVariables  the list of prior variables.
    397     */
    398    private void checkCyclicSubstitution(final String varName, final List<String> priorVariables) {
    399        if (!priorVariables.contains(varName)) {
    400            return;
    401        }
    402        final StrBuilder buf = new StrBuilder(256);
    403        buf.append("Infinite loop in property interpolation of ");
    404        buf.append(priorVariables.remove(0));
    405        buf.append(": ");
    406        buf.appendWithSeparators(priorVariables, "->");
    407        throw new IllegalStateException(buf.toString());
    408    }
    409
    410    /**
    411     * Returns the escape character.
    412     *
    413     * @return The character used for escaping variable references.
    414     */
    415    public char getEscapeChar() {
    416        return this.escapeChar;
    417    }
    418
    419    /**
    420     * Gets the variable default value delimiter matcher currently in use.
    421     * <p>
    422     * The variable default value delimiter is the character or characters that delimit the variable name and the variable default value. This delimiter is
    423     * expressed in terms of a matcher allowing advanced variable default value delimiter matches.
    424     * </p>
    425     * <p>
    426     * If it returns null, then the variable default value resolution is disabled.
    427     * </p>
    428     *
    429     * @return The variable default value delimiter matcher in use, may be null.
    430     */
    431    public StrMatcher getValueDelimiterMatcher() {
    432        return valueDelimiterMatcher;
    433    }
    434
    435    /**
    436     * Gets the variable prefix matcher currently in use.
    437     * <p>
    438     * The variable prefix is the character or characters that identify the start of a variable. This prefix is expressed in terms of a matcher allowing
    439     * advanced prefix matches.
    440     * </p>
    441     *
    442     * @return The prefix matcher in use.
    443     */
    444    public StrMatcher getVariablePrefixMatcher() {
    445        return prefixMatcher;
    446    }
    447
    448    /**
    449     * Gets the VariableResolver that is used to lookup variables.
    450     *
    451     * @return The VariableResolver.
    452     */
    453    public StrLookup<?> getVariableResolver() {
    454        return this.variableResolver;
    455    }
    456
    457    /**
    458     * Gets the variable suffix matcher currently in use.
    459     * <p>
    460     * The variable suffix is the character or characters that identify the end of a variable. This suffix is expressed in terms of a matcher allowing advanced
    461     * suffix matches.
    462     * </p>
    463     *
    464     * @return The suffix matcher in use.
    465     */
    466    public StrMatcher getVariableSuffixMatcher() {
    467        return suffixMatcher;
    468    }
    469
    470    /**
    471     * Returns a flag whether substitution is disabled in variable values.If set to
    472     * <strong>true</strong>, the values of variables can contain other variables will not be
    473     * processed and substituted original variable is evaluated, e.g.
    474     * <pre>
    475     * Map&lt;String, String&gt; valuesMap = new HashMap&lt;&gt;();
    476     * valuesMap.put(&quot;name&quot;, &quot;Douglas ${surname}&quot;);
    477     * valuesMap.put(&quot;surname&quot;, &quot;Crockford&quot;);
    478     * String templateString = &quot;Hi ${name}&quot;;
    479     * StrSubstitutor sub = new StrSubstitutor(valuesMap);
    480     * String resolvedString = sub.replace(templateString);
    481     * </pre>
    482     * <p>
    483     * yielding:
    484     * </p>
    485     * <pre>
    486     *      Hi Douglas ${surname}
    487     * </pre>
    488     *
    489     * @return The substitution in variable values flag.
    490     * @since 1.2
    491     */
    492    public boolean isDisableSubstitutionInValues() {
    493        return disableSubstitutionInValues;
    494    }
    495
    496    /**
    497     * Returns a flag whether substitution is done in variable names.
    498     *
    499     * @return The substitution in variable names flag.
    500     */
    501    public boolean isEnableSubstitutionInVariables() {
    502        return enableSubstitutionInVariables;
    503    }
    504
    505    /**
    506     * Returns the flag controlling whether escapes are preserved during substitution.
    507     *
    508     * @return The preserve escape flag.
    509     */
    510    public boolean isPreserveEscapes() {
    511        return preserveEscapes;
    512    }
    513
    514    /**
    515     * Replaces all the occurrences of variables with their matching values from the resolver using the given source array as a template. The array is not
    516     * altered by this method.
    517     *
    518     * @param source the character array to replace in, not altered, null returns null.
    519     * @return The result of the replace operation.
    520     */
    521    public String replace(final char[] source) {
    522        if (source == null) {
    523            return null;
    524        }
    525        final StrBuilder buf = new StrBuilder(source.length).append(source);
    526        substitute(buf, 0, source.length);
    527        return buf.toString();
    528    }
    529
    530    /**
    531     * Replaces all the occurrences of variables with their matching values from the resolver using the given source array as a template. The array is not
    532     * altered by this method.
    533     * <p>
    534     * Only the specified portion of the array will be processed. The rest of the array is not processed, and is not returned.
    535     * </p>
    536     *
    537     * @param source the character array to replace in, not altered, null returns null.
    538     * @param offset the start offset within the array, must be valid.
    539     * @param length the length within the array to be processed, must be valid.
    540     * @return The result of the replace operation.
    541     */
    542    public String replace(final char[] source, final int offset, final int length) {
    543        if (source == null) {
    544            return null;
    545        }
    546        final StrBuilder buf = new StrBuilder(length).append(source, offset, length);
    547        substitute(buf, 0, length);
    548        return buf.toString();
    549    }
    550
    551    /**
    552     * Replaces all the occurrences of variables with their matching values from the resolver using the given source as a template. The source is not altered by
    553     * this method.
    554     *
    555     * @param source the buffer to use as a template, not changed, null returns null.
    556     * @return The result of the replace operation.
    557     */
    558    public String replace(final CharSequence source) {
    559        if (source == null) {
    560            return null;
    561        }
    562        return replace(source, 0, source.length());
    563    }
    564
    565    /**
    566     * Replaces all the occurrences of variables with their matching values from the resolver using the given source as a template. The source is not altered by
    567     * this method.
    568     * <p>
    569     * Only the specified portion of the buffer will be processed. The rest of the buffer is not processed, and is not returned.
    570     * </p>
    571     *
    572     * @param source the buffer to use as a template, not changed, null returns null.
    573     * @param offset the start offset within the array, must be valid.
    574     * @param length the length within the array to be processed, must be valid.
    575     * @return The result of the replace operation.
    576     */
    577    public String replace(final CharSequence source, final int offset, final int length) {
    578        if (source == null) {
    579            return null;
    580        }
    581        final StrBuilder buf = new StrBuilder(length).append(source, offset, length);
    582        substitute(buf, 0, length);
    583        return buf.toString();
    584    }
    585
    586    /**
    587     * Replaces all the occurrences of variables in the given source object with their matching values from the resolver. The input source object is converted
    588     * to a string using {@code toString} and is not altered.
    589     *
    590     * @param source the source to replace in, null returns null.
    591     * @return The result of the replace operation.
    592     */
    593    public String replace(final Object source) {
    594        if (source == null) {
    595            return null;
    596        }
    597        final StrBuilder buf = new StrBuilder().append(source);
    598        substitute(buf, 0, buf.length());
    599        return buf.toString();
    600    }
    601
    602    /**
    603     * Replaces all the occurrences of variables with their matching values from the resolver using the given source builder as a template. The builder is not
    604     * altered by this method.
    605     *
    606     * @param source the builder to use as a template, not changed, null returns null.
    607     * @return The result of the replace operation.
    608     */
    609    public String replace(final StrBuilder source) {
    610        if (source == null) {
    611            return null;
    612        }
    613        final StrBuilder buf = new StrBuilder(source.length()).append(source);
    614        substitute(buf, 0, buf.length());
    615        return buf.toString();
    616    }
    617
    618    /**
    619     * Replaces all the occurrences of variables with their matching values from the resolver using the given source builder as a template. The builder is not
    620     * altered by this method.
    621     * <p>
    622     * Only the specified portion of the builder will be processed. The rest of the builder is not processed, and is not returned.
    623     * </p>
    624     *
    625     * @param source the builder to use as a template, not changed, null returns null.
    626     * @param offset the start offset within the array, must be valid.
    627     * @param length the length within the array to be processed, must be valid.
    628     * @return The result of the replace operation.
    629     */
    630    public String replace(final StrBuilder source, final int offset, final int length) {
    631        if (source == null) {
    632            return null;
    633        }
    634        final StrBuilder buf = new StrBuilder(length).append(source, offset, length);
    635        substitute(buf, 0, length);
    636        return buf.toString();
    637    }
    638
    639    /**
    640     * Replaces all the occurrences of variables with their matching values from the resolver using the given source string as a template.
    641     *
    642     * @param source the string to replace in, null returns null.
    643     * @return The result of the replace operation.
    644     */
    645    public String replace(final String source) {
    646        if (source == null) {
    647            return null;
    648        }
    649        final StrBuilder buf = new StrBuilder(source);
    650        if (!substitute(buf, 0, source.length())) {
    651            return source;
    652        }
    653        return buf.toString();
    654    }
    655
    656    /**
    657     * Replaces all the occurrences of variables with their matching values from the resolver using the given source string as a template.
    658     * <p>
    659     * Only the specified portion of the string will be processed. The rest of the string is not processed, and is not returned.
    660     * </p>
    661     *
    662     * @param source the string to replace in, null returns null.
    663     * @param offset the start offset within the array, must be valid.
    664     * @param length the length within the array to be processed, must be valid.
    665     * @return The result of the replace operation.
    666     */
    667    public String replace(final String source, final int offset, final int length) {
    668        if (source == null) {
    669            return null;
    670        }
    671        final StrBuilder buf = new StrBuilder(length).append(source, offset, length);
    672        if (!substitute(buf, 0, length)) {
    673            return source.substring(offset, offset + length);
    674        }
    675        return buf.toString();
    676    }
    677
    678    /**
    679     * Replaces all the occurrences of variables with their matching values from the resolver using the given source buffer as a template. The buffer is not
    680     * altered by this method.
    681     *
    682     * @param source the buffer to use as a template, not changed, null returns null.
    683     * @return The result of the replace operation.
    684     */
    685    public String replace(final StringBuffer source) {
    686        if (source == null) {
    687            return null;
    688        }
    689        final StrBuilder buf = new StrBuilder(source.length()).append(source);
    690        substitute(buf, 0, buf.length());
    691        return buf.toString();
    692    }
    693
    694    /**
    695     * Replaces all the occurrences of variables with their matching values from the resolver using the given source buffer as a template. The buffer is not
    696     * altered by this method.
    697     * <p>
    698     * Only the specified portion of the buffer will be processed. The rest of the buffer is not processed, and is not returned.
    699     * </p>
    700     *
    701     * @param source the buffer to use as a template, not changed, null returns null.
    702     * @param offset the start offset within the array, must be valid.
    703     * @param length the length within the array to be processed, must be valid.
    704     * @return The result of the replace operation.
    705     */
    706    public String replace(final StringBuffer source, final int offset, final int length) {
    707        if (source == null) {
    708            return null;
    709        }
    710        final StrBuilder buf = new StrBuilder(length).append(source, offset, length);
    711        substitute(buf, 0, length);
    712        return buf.toString();
    713    }
    714
    715    /**
    716     * Replaces all the occurrences of variables within the given source builder with their matching values from the resolver.
    717     *
    718     * @param source the builder to replace in, updated, null returns zero.
    719     * @return true if altered.
    720     */
    721    public boolean replaceIn(final StrBuilder source) {
    722        if (source == null) {
    723            return false;
    724        }
    725        return substitute(source, 0, source.length());
    726    }
    727
    728    /**
    729     * Replaces all the occurrences of variables within the given source builder with their matching values from the resolver.
    730     * <p>
    731     * Only the specified portion of the builder will be processed. The rest of the builder is not processed, but it is not deleted.
    732     * </p>
    733     *
    734     * @param source the builder to replace in, null returns zero.
    735     * @param offset the start offset within the array, must be valid.
    736     * @param length the length within the builder to be processed, must be valid.
    737     * @return true if altered.
    738     */
    739    public boolean replaceIn(final StrBuilder source, final int offset, final int length) {
    740        if (source == null) {
    741            return false;
    742        }
    743        return substitute(source, offset, length);
    744    }
    745
    746    /**
    747     * Replaces all the occurrences of variables within the given source buffer with their matching values from the resolver. The buffer is updated with the
    748     * result.
    749     *
    750     * @param source the buffer to replace in, updated, null returns zero.
    751     * @return true if altered.
    752     */
    753    public boolean replaceIn(final StringBuffer source) {
    754        if (source == null) {
    755            return false;
    756        }
    757        return replaceIn(source, 0, source.length());
    758    }
    759
    760    /**
    761     * Replaces all the occurrences of variables within the given source buffer with their matching values from the resolver. The buffer is updated with the
    762     * result.
    763     * <p>
    764     * Only the specified portion of the buffer will be processed. The rest of the buffer is not processed, but it is not deleted.
    765     * </p>
    766     *
    767     * @param source the buffer to replace in, updated, null returns zero.
    768     * @param offset the start offset within the array, must be valid.
    769     * @param length the length within the buffer to be processed, must be valid.
    770     * @return true if altered.
    771     */
    772    public boolean replaceIn(final StringBuffer source, final int offset, final int length) {
    773        if (source == null) {
    774            return false;
    775        }
    776        final StrBuilder buf = new StrBuilder(length).append(source, offset, length);
    777        if (!substitute(buf, 0, length)) {
    778            return false;
    779        }
    780        source.replace(offset, offset + length, buf.toString());
    781        return true;
    782    }
    783
    784    /**
    785     * Replaces all the occurrences of variables within the given source buffer with their matching values from the resolver. The buffer is updated with the
    786     * result.
    787     *
    788     * @param source the buffer to replace in, updated, null returns zero.
    789     * @return true if altered.
    790     */
    791    public boolean replaceIn(final StringBuilder source) {
    792        if (source == null) {
    793            return false;
    794        }
    795        return replaceIn(source, 0, source.length());
    796    }
    797
    798    /**
    799     * Replaces all the occurrences of variables within the given source builder with their matching values from the resolver. The builder is updated with the
    800     * result.
    801     * <p>
    802     * Only the specified portion of the buffer will be processed. The rest of the buffer is not processed, but it is not deleted.
    803     * </p>
    804     *
    805     * @param source the buffer to replace in, updated, null returns zero.
    806     * @param offset the start offset within the array, must be valid.
    807     * @param length the length within the buffer to be processed, must be valid.
    808     * @return true if altered.
    809     */
    810    public boolean replaceIn(final StringBuilder source, final int offset, final int length) {
    811        if (source == null) {
    812            return false;
    813        }
    814        final StrBuilder buf = new StrBuilder(length).append(source, offset, length);
    815        if (!substitute(buf, 0, length)) {
    816            return false;
    817        }
    818        source.replace(offset, offset + length, buf.toString());
    819        return true;
    820    }
    821
    822    /**
    823     * Internal method that resolves the value of a variable.
    824     * <p>
    825     * Most users of this class do not need to call this method. This method is called automatically by the substitution process.
    826     * </p>
    827     * <p>
    828     * Writers of subclasses can override this method if they need to alter how each substitution occurs. The method is passed the variable's name and must
    829     * return the corresponding value. This implementation uses the {@link #getVariableResolver()} with the variable's name as the key.
    830     * </p>
    831     *
    832     * @param variableName the name of the variable, not null.
    833     * @param buf          the buffer where the substitution is occurring, not null.
    834     * @param startPos     the start position of the variable including the prefix, valid.
    835     * @param endPos       the end position of the variable including the suffix, valid.
    836     * @return The variable's value or <strong>null</strong> if the variable is unknown.
    837     */
    838    protected String resolveVariable(final String variableName,
    839                                     final StrBuilder buf,
    840                                     final int startPos,
    841                                     final int endPos) {
    842        final StrLookup<?> resolver = getVariableResolver();
    843        if (resolver == null) {
    844            return null;
    845        }
    846        return resolver.apply(variableName);
    847    }
    848
    849    /**
    850     * Sets a flag whether substitution is done in variable values (recursive).
    851     *
    852     * @param disableSubstitutionInValues true if substitution in variable value are disabled.
    853     * @since 1.2
    854     */
    855    public void setDisableSubstitutionInValues(final boolean disableSubstitutionInValues) {
    856        this.disableSubstitutionInValues = disableSubstitutionInValues;
    857    }
    858
    859    /**
    860     * Sets a flag whether substitution is done in variable names. If set to <strong>true</strong>, the names of variables can contain other variables which are
    861     * processed first before the original variable is evaluated, e.g. {@code ${jre-${java.version}}}. The default value is <strong>false</strong>.
    862     *
    863     * @param enableSubstitutionInVariables the new value of the flag.
    864     */
    865    public void setEnableSubstitutionInVariables(final boolean enableSubstitutionInVariables) {
    866        this.enableSubstitutionInVariables = enableSubstitutionInVariables;
    867    }
    868
    869    /**
    870     * Sets the escape character. If this character is placed before a variable reference in the source text, this variable will be ignored.
    871     *
    872     * @param escapeCharacter the escape character (0 for disabling escaping)
    873     */
    874    public void setEscapeChar(final char escapeCharacter) {
    875        this.escapeChar = escapeCharacter;
    876    }
    877
    878    /**
    879     * Sets a flag controlling whether escapes are preserved during substitution. If set to <strong>true</strong>, the escape character is retained during
    880     * substitution (e.g. {@code $${this-is-escaped}} remains {@code $${this-is-escaped}}). If set to <strong>false</strong>, the escape character is removed
    881     * during substitution (e.g. {@code $${this-is-escaped}} becomes {@code ${this-is-escaped}}). The default value is <strong>false</strong>
    882     *
    883     * @param preserveEscapes true if escapes are to be preserved
    884     */
    885    public void setPreserveEscapes(final boolean preserveEscapes) {
    886        this.preserveEscapes = preserveEscapes;
    887    }
    888
    889    /**
    890     * Sets the variable default value delimiter to use.
    891     * <p>
    892     * The variable default value delimiter is the character or characters that delimit the variable name and the variable default value. This method allows a
    893     * single character variable default value delimiter to be easily set.
    894     * </p>
    895     *
    896     * @param valueDelimiter the variable default value delimiter character to use.
    897     * @return {@code this} instance.
    898     */
    899    public StrSubstitutor setValueDelimiter(final char valueDelimiter) {
    900        return setValueDelimiterMatcher(StrMatcher.charMatcher(valueDelimiter));
    901    }
    902
    903    /**
    904     * Sets the variable default value delimiter to use.
    905     * <p>
    906     * The variable default value delimiter is the character or characters that delimit the variable name and the variable default value. This method allows a
    907     * string variable default value delimiter to be easily set.
    908     * </p>
    909     * <p>
    910     * If the {@code valueDelimiter} is null or empty string, then the variable default value resolution becomes disabled.
    911     * </p>
    912     *
    913     * @param valueDelimiter the variable default value delimiter string to use, may be null or empty
    914     * @return {@code this} instance.
    915     */
    916    public StrSubstitutor setValueDelimiter(final String valueDelimiter) {
    917        if (valueDelimiter == null || valueDelimiter.isEmpty()) {
    918            setValueDelimiterMatcher(null);
    919            return this;
    920        }
    921        return setValueDelimiterMatcher(StrMatcher.stringMatcher(valueDelimiter));
    922    }
    923
    924    /**
    925     * Sets the variable default value delimiter matcher to use.
    926     * <p>
    927     * The variable default value delimiter is the character or characters that delimit the variable name and the variable default value. This delimiter is
    928     * expressed in terms of a matcher allowing advanced variable default value delimiter matches.
    929     * </p>
    930     * <p>
    931     * If the {@code valueDelimiterMatcher} is null, then the variable default value resolution becomes disabled.
    932     * </p>
    933     *
    934     * @param valueDelimiterMatcher variable default value delimiter matcher to use, may be null
    935     * @return {@code this} instance.
    936     */
    937    public StrSubstitutor setValueDelimiterMatcher(final StrMatcher valueDelimiterMatcher) {
    938        this.valueDelimiterMatcher = valueDelimiterMatcher;
    939        return this;
    940    }
    941
    942    /**
    943     * Sets the variable prefix to use.
    944     * <p>
    945     * The variable prefix is the character or characters that identify the start of a variable. This method allows a single character prefix to be easily set.
    946     * </p>
    947     *
    948     * @param prefix the prefix character to use.
    949     * @return {@code this} instance.
    950     */
    951    public StrSubstitutor setVariablePrefix(final char prefix) {
    952        return setVariablePrefixMatcher(StrMatcher.charMatcher(prefix));
    953    }
    954
    955    /**
    956     * Sets the variable prefix to use.
    957     * <p>
    958     * The variable prefix is the character or characters that identify the start of a variable. This method allows a string prefix to be easily set.
    959     * </p>
    960     *
    961     * @param prefix the prefix for variables, not null.
    962     * @return {@code this} instance.
    963     * @throws IllegalArgumentException if the prefix is null.
    964     */
    965    public StrSubstitutor setVariablePrefix(final String prefix) {
    966        Validate.isTrue(prefix != null, "Variable prefix must not be null!");
    967        return setVariablePrefixMatcher(StrMatcher.stringMatcher(prefix));
    968    }
    969
    970    /**
    971     * Sets the variable prefix matcher currently in use.
    972     * <p>
    973     * The variable prefix is the character or characters that identify the start of a variable. This prefix is expressed in terms of a matcher allowing
    974     * advanced prefix matches.
    975     * </p>
    976     *
    977     * @param prefixMatcher the prefix matcher to use, null ignored.
    978     * @return {@code this} instance.
    979     * @throws IllegalArgumentException if the prefix matcher is null.
    980     */
    981    public StrSubstitutor setVariablePrefixMatcher(final StrMatcher prefixMatcher) {
    982        Validate.isTrue(prefixMatcher != null, "Variable prefix matcher must not be null!");
    983        this.prefixMatcher = prefixMatcher;
    984        return this;
    985    }
    986
    987    /**
    988     * Sets the VariableResolver that is used to lookup variables.
    989     *
    990     * @param variableResolver the VariableResolver.
    991     */
    992    public void setVariableResolver(final StrLookup<?> variableResolver) {
    993        this.variableResolver = variableResolver;
    994    }
    995
    996    /**
    997     * Sets the variable suffix to use.
    998     * <p>
    999     * The variable suffix is the character or characters that identify the end of a variable. This method allows a single character suffix to be easily set.
    1000     * </p>
    1001     *
    1002     * @param suffix the suffix character to use.
    1003     * @return {@code this} instance.
    1004     */
    1005    public StrSubstitutor setVariableSuffix(final char suffix) {
    1006        return setVariableSuffixMatcher(StrMatcher.charMatcher(suffix));
    1007    }
    1008
    1009    /**
    1010     * Sets the variable suffix to use.
    1011     * <p>
    1012     * The variable suffix is the character or characters that identify the end of a variable. This method allows a string suffix to be easily set.
    1013     * </p>
    1014     *
    1015     * @param suffix the suffix for variables, not null.
    1016     * @return {@code this} instance.
    1017     * @throws IllegalArgumentException if the suffix is null.
    1018     */
    1019    public StrSubstitutor setVariableSuffix(final String suffix) {
    1020        Validate.isTrue(suffix != null, "Variable suffix must not be null!");
    1021        return setVariableSuffixMatcher(StrMatcher.stringMatcher(suffix));
    1022    }
    1023
    1024    /**
    1025     * Sets the variable suffix matcher currently in use.
    1026     * <p>
    1027     * The variable suffix is the character or characters that identify the end of a variable. This suffix is expressed in terms of a matcher allowing advanced
    1028     * suffix matches.
    1029     * </p>
    1030     *
    1031     * @param suffixMatcher the suffix matcher to use, null ignored.
    1032     * @return {@code this} instance.
    1033     * @throws IllegalArgumentException if the suffix matcher is null.
    1034     */
    1035    public StrSubstitutor setVariableSuffixMatcher(final StrMatcher suffixMatcher) {
    1036        Validate.isTrue(suffixMatcher != null, "Variable suffix matcher must not be null!");
    1037        this.suffixMatcher = suffixMatcher;
    1038        return this;
    1039    }
    1040
    1041    /**
    1042     * Internal method that substitutes the variables.
    1043     * <p>
    1044     * Most users of this class do not need to call this method. This method will be called automatically by another (public) method.
    1045     * </p>
    1046     * <p>
    1047     * Writers of subclasses can override this method if they need access to the substitution process at the start or end.
    1048     * </p>
    1049     *
    1050     * @param buf    the string builder to substitute into, not null.
    1051     * @param offset the start offset within the builder, must be valid.
    1052     * @param length the length within the builder to be processed, must be valid.
    1053     * @return true if altered.
    1054     */
    1055    protected boolean substitute(final StrBuilder buf, final int offset, final int length) {
    1056        return substitute(buf, offset, length, null) > 0;
    1057    }
    1058
    1059    /**
    1060     * Recursive handler for multiple levels of interpolation. This is the main interpolation method, which resolves the values of all variable references
    1061     * contained in the passed in text.
    1062     *
    1063     * @param buf            the string builder to substitute into, not null.
    1064     * @param offset         the start offset within the builder, must be valid.
    1065     * @param length         the length within the builder to be processed, must be valid.
    1066     * @param priorVariables the stack keeping track of the replaced variables, may be null.
    1067     * @return The length change that occurs, unless priorVariables is null when the int represents a boolean flag as to whether any change occurred.
    1068     */
    1069    private int substitute(final StrBuilder buf, final int offset, final int length, List<String> priorVariables) {
    1070        final StrMatcher pfxMatcher = getVariablePrefixMatcher();
    1071        final StrMatcher suffMatcher = getVariableSuffixMatcher();
    1072        final char escape = getEscapeChar();
    1073        final StrMatcher valueDelimMatcher = getValueDelimiterMatcher();
    1074        final boolean substitutionInVariablesEnabled = isEnableSubstitutionInVariables();
    1075        final boolean substitutionInValuesDisabled = isDisableSubstitutionInValues();
    1076
    1077        final boolean top = priorVariables == null;
    1078        boolean altered = false;
    1079        int lengthChange = 0;
    1080        char[] chars = buf.buffer;
    1081        int bufEnd = offset + length;
    1082        int pos = offset;
    1083        while (pos < bufEnd) {
    1084            final int startMatchLen = pfxMatcher.isMatch(chars, pos, offset,
    1085                    bufEnd);
    1086            if (startMatchLen == 0) {
    1087                pos++;
    1088            } else // found variable start marker
    1089            if (pos > offset && chars[pos - 1] == escape) {
    1090                // escaped
    1091                if (preserveEscapes) {
    1092                    pos++;
    1093                    continue;
    1094                }
    1095                buf.deleteCharAt(pos - 1);
    1096                chars = buf.buffer; // in case buffer was altered
    1097                lengthChange--;
    1098                altered = true;
    1099                bufEnd--;
    1100            } else {
    1101                // find suffix
    1102                final int startPos = pos;
    1103                pos += startMatchLen;
    1104                int endMatchLen = 0;
    1105                int nestedVarCount = 0;
    1106                while (pos < bufEnd) {
    1107                    if (substitutionInVariablesEnabled
    1108                            && pfxMatcher.isMatch(chars,
    1109                                    pos, offset, bufEnd) != 0) {
    1110                        // found a nested variable start
    1111                        endMatchLen = pfxMatcher.isMatch(chars,
    1112                                pos, offset, bufEnd);
    1113                        nestedVarCount++;
    1114                        pos += endMatchLen;
    1115                        continue;
    1116                    }
    1117
    1118                    endMatchLen = suffMatcher.isMatch(chars, pos, offset,
    1119                            bufEnd);
    1120                    if (endMatchLen == 0) {
    1121                        pos++;
    1122                    } else {
    1123                        // found variable end marker
    1124                        if (nestedVarCount == 0) {
    1125                            String varNameExpr = new String(chars, startPos
    1126                                    + startMatchLen, pos - startPos
    1127                                    - startMatchLen);
    1128                            if (substitutionInVariablesEnabled) {
    1129                                final StrBuilder bufName = new StrBuilder(varNameExpr);
    1130                                substitute(bufName, 0, bufName.length());
    1131                                varNameExpr = bufName.toString();
    1132                            }
    1133                            pos += endMatchLen;
    1134                            final int endPos = pos;
    1135
    1136                            String varName = varNameExpr;
    1137                            String varDefaultValue = null;
    1138
    1139                            if (valueDelimMatcher != null) {
    1140                                final char[] varNameExprChars = varNameExpr.toCharArray();
    1141                                int valueDelimiterMatchLen = 0;
    1142                                for (int i = 0; i < varNameExprChars.length; i++) {
    1143                                    // if there's any nested variable when nested variable substitution disabled,
    1144                                    // then stop resolving name and default value.
    1145                                    if (!substitutionInVariablesEnabled
    1146                                            && pfxMatcher.isMatch(varNameExprChars,
    1147                                                                    i,
    1148                                                                    i,
    1149                                                                    varNameExprChars.length) != 0) {
    1150                                        break;
    1151                                    }
    1152                                    if (valueDelimMatcher.isMatch(varNameExprChars, i) != 0) {
    1153                                        valueDelimiterMatchLen = valueDelimMatcher.isMatch(varNameExprChars, i);
    1154                                        varName = varNameExpr.substring(0, i);
    1155                                        varDefaultValue = varNameExpr.substring(i + valueDelimiterMatchLen);
    1156                                        break;
    1157                                    }
    1158                                }
    1159                            }
    1160
    1161                            // on the first call initialize priorVariables
    1162                            if (priorVariables == null) {
    1163                                priorVariables = new ArrayList<>();
    1164                                priorVariables.add(new String(chars,
    1165                                        offset, length));
    1166                            }
    1167
    1168                            // handle cyclic substitution
    1169                            checkCyclicSubstitution(varName, priorVariables);
    1170                            priorVariables.add(varName);
    1171
    1172                            // resolve the variable
    1173                            String varValue = resolveVariable(varName, buf,
    1174                                    startPos, endPos);
    1175                            if (varValue == null) {
    1176                                varValue = varDefaultValue;
    1177                            }
    1178                            if (varValue != null) {
    1179                                final int varLen = varValue.length();
    1180                                buf.replace(startPos, endPos, varValue);
    1181                                altered = true;
    1182                                int change = 0;
    1183                                if (!substitutionInValuesDisabled) { // recursive replace
    1184                                    change = substitute(buf, startPos,
    1185                                        varLen, priorVariables);
    1186                                }
    1187                                change = change
    1188                                    + varLen - (endPos - startPos);
    1189                                pos += change;
    1190                                bufEnd += change;
    1191                                lengthChange += change;
    1192                                chars = buf.buffer; // in case buffer was
    1193                                                    // altered
    1194                            }
    1195
    1196                            // remove variable from the cyclic stack
    1197                            priorVariables
    1198                                    .remove(priorVariables.size() - 1);
    1199                            break;
    1200                        }
    1201                        nestedVarCount--;
    1202                        pos += endMatchLen;
    1203                    }
    1204                }
    1205            }
    1206        }
    1207        if (top) {
    1208            return altered ? 1 : 0;
    1209        }
    1210        return lengthChange;
    1211    }
    1212}