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