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