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

    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 <strong>true</strong>.
    121 * <p>This class is <strong>not</strong> thread safe.</p>
    122 *
    123 * @since 1.0
    124 * @deprecated Deprecated as of 1.3, use {@link StringSubstitutor} instead. This class will be removed in 2.0.
    125 */
    126@Deprecated
    127public class StrSubstitutor {
    128
    129    /**
    130     * Constant for the default escape character.
    131     */
    132    public static final char DEFAULT_ESCAPE = '$';
    133
    134    /**
    135     * Constant for the default variable prefix.
    136     */
    137    public static final StrMatcher DEFAULT_PREFIX = StrMatcher.stringMatcher("${");
    138
    139    /**
    140     * Constant for the default variable suffix.
    141     */
    142    public static final StrMatcher DEFAULT_SUFFIX = StrMatcher.stringMatcher("}");
    143
    144    /**
    145     * Constant for the default value delimiter of a variable.
    146     */
    147    public static final StrMatcher DEFAULT_VALUE_DELIMITER = StrMatcher.stringMatcher(":-");
    148
    149    /**
    150     * Replaces all the occurrences of variables in the given source object with
    151     * their matching values from the map.
    152     *
    153     * @param <V> the type of the values in the map
    154     * @param source  the source text containing the variables to substitute, null returns null
    155     * @param valueMap  the map with the values, may be null
    156     * @return The result of the replace operation
    157     */
    158    public static <V> String replace(final Object source, final Map<String, V> valueMap) {
    159        return new StrSubstitutor(valueMap).replace(source);
    160    }
    161
    162    /**
    163     * Replaces all the occurrences of variables in the given source object with
    164     * their matching values from the map. This method allows to specify a
    165     * custom variable prefix and suffix
    166     *
    167     * @param <V> the type of the values in the map
    168     * @param source  the source text containing the variables to substitute, null returns null
    169     * @param valueMap  the map with the values, may be null
    170     * @param prefix  the prefix of variables, not null
    171     * @param suffix  the suffix of variables, not null
    172     * @return The result of the replace operation
    173     * @throws IllegalArgumentException if the prefix or suffix is null
    174     */
    175    public static <V> String replace(final Object source,
    176                                     final Map<String, V> valueMap,
    177                                     final String prefix,
    178                                     final String suffix) {
    179        return new StrSubstitutor(valueMap, prefix, suffix).replace(source);
    180    }
    181
    182    /**
    183     * Replaces all the occurrences of variables in the given source object with their matching
    184     * values from the properties.
    185     *
    186     * @param source the source text containing the variables to substitute, null returns null
    187     * @param valueProperties the properties with values, may be null
    188     * @return The result of the replace operation
    189     */
    190    public static String replace(final Object source, final Properties valueProperties) {
    191        if (valueProperties == null) {
    192            return source.toString();
    193        }
    194        return 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(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        setVariableResolver(variableResolver);
    333        setVariablePrefix(prefix);
    334        setVariableSuffix(suffix);
    335        setEscapeChar(escape);
    336        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        setVariableResolver(variableResolver);
    352        setVariablePrefix(prefix);
    353        setVariableSuffix(suffix);
    354        setEscapeChar(escape);
    355        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        setVariableResolver(variableResolver);
    387        setVariablePrefixMatcher(prefixMatcher);
    388        setVariableSuffixMatcher(suffixMatcher);
    389        setEscapeChar(escape);
    390        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     * <strong>true</strong>, 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     * @since 1.2
    493     */
    494    public boolean isDisableSubstitutionInValues() {
    495        return disableSubstitutionInValues;
    496    }
    497
    498    /**
    499     * Returns a flag whether substitution is done in variable names.
    500     *
    501     * @return The substitution in variable names flag
    502     */
    503    public boolean isEnableSubstitutionInVariables() {
    504        return enableSubstitutionInVariables;
    505    }
    506
    507    /**
    508     * Returns the flag controlling whether escapes are preserved during
    509     * substitution.
    510     *
    511     * @return The preserve escape flag
    512     */
    513    public boolean isPreserveEscapes() {
    514        return preserveEscapes;
    515    }
    516
    517    /**
    518     * Replaces all the occurrences of variables with their matching values
    519     * from the resolver using the given source array as a template.
    520     * The array is not altered by this method.
    521     *
    522     * @param source  the character array to replace in, not altered, null returns null
    523     * @return The result of the replace operation
    524     */
    525    public String replace(final char[] source) {
    526        if (source == null) {
    527            return null;
    528        }
    529        final StrBuilder buf = new StrBuilder(source.length).append(source);
    530        substitute(buf, 0, source.length);
    531        return buf.toString();
    532    }
    533
    534    /**
    535     * Replaces all the occurrences of variables with their matching values
    536     * from the resolver using the given source array as a template.
    537     * The array is not altered by this method.
    538     * <p>
    539     * Only the specified portion of the array will be processed.
    540     * The rest of the array is not processed, and is not returned.
    541     * </p>
    542     *
    543     * @param source  the character array to replace in, not altered, null returns null
    544     * @param offset  the start offset within the array, must be valid
    545     * @param length  the length within the array to be processed, must be valid
    546     * @return The result of the replace operation
    547     */
    548    public String replace(final char[] source, final int offset, final int length) {
    549        if (source == null) {
    550            return null;
    551        }
    552        final StrBuilder buf = new StrBuilder(length).append(source, offset, length);
    553        substitute(buf, 0, length);
    554        return buf.toString();
    555    }
    556
    557    /**
    558     * Replaces all the occurrences of variables with their matching values
    559     * from the resolver using the given source as a template.
    560     * The source is not altered by this method.
    561     *
    562     * @param source  the buffer to use as a template, not changed, null returns null
    563     * @return The result of the replace operation
    564     */
    565    public String replace(final CharSequence source) {
    566        if (source == null) {
    567            return null;
    568        }
    569        return replace(source, 0, source.length());
    570    }
    571
    572    /**
    573     * Replaces all the occurrences of variables with their matching values
    574     * from the resolver using the given source as a template.
    575     * The source is not altered by this method.
    576     * <p>
    577     * Only the specified portion of the buffer will be processed.
    578     * The rest of the buffer is not processed, and is not returned.
    579     * </p>
    580     *
    581     * @param source  the buffer to use as a template, not changed, null returns null
    582     * @param offset  the start offset within the array, must be valid
    583     * @param length  the length within the array to be processed, must be valid
    584     * @return The result of the replace operation
    585     */
    586    public String replace(final CharSequence source, final int offset, final int length) {
    587        if (source == null) {
    588            return null;
    589        }
    590        final StrBuilder buf = new StrBuilder(length).append(source, offset, length);
    591        substitute(buf, 0, length);
    592        return buf.toString();
    593    }
    594
    595    /**
    596     * Replaces all the occurrences of variables in the given source object with
    597     * their matching values from the resolver. The input source object is
    598     * converted to a string using {@code toString} and is not altered.
    599     *
    600     * @param source  the source to replace in, null returns null
    601     * @return The result of the replace operation
    602     */
    603    public String replace(final Object source) {
    604        if (source == null) {
    605            return null;
    606        }
    607        final StrBuilder buf = new StrBuilder().append(source);
    608        substitute(buf, 0, buf.length());
    609        return buf.toString();
    610    }
    611
    612    /**
    613     * Replaces all the occurrences of variables with their matching values
    614     * from the resolver using the given source builder as a template.
    615     * The builder is not altered by this method.
    616     *
    617     * @param source  the builder to use as a template, not changed, null returns null
    618     * @return The result of the replace operation
    619     */
    620    public String replace(final StrBuilder source) {
    621        if (source == null) {
    622            return null;
    623        }
    624        final StrBuilder buf = new StrBuilder(source.length()).append(source);
    625        substitute(buf, 0, buf.length());
    626        return buf.toString();
    627    }
    628
    629    /**
    630     * Replaces all the occurrences of variables with their matching values
    631     * from the resolver using the given source builder as a template.
    632     * The builder is not altered by this method.
    633     * <p>
    634     * Only the specified portion of the builder will be processed.
    635     * The rest of the builder is not processed, and is not returned.
    636     * </p>
    637     *
    638     * @param source  the builder to use as a template, not changed, null returns null
    639     * @param offset  the start offset within the array, must be valid
    640     * @param length  the length within the array to be processed, must be valid
    641     * @return The result of the replace operation
    642     */
    643    public String replace(final StrBuilder source, final int offset, final int length) {
    644        if (source == null) {
    645            return null;
    646        }
    647        final StrBuilder buf = new StrBuilder(length).append(source, offset, length);
    648        substitute(buf, 0, length);
    649        return buf.toString();
    650    }
    651
    652    /**
    653     * Replaces all the occurrences of variables with their matching values
    654     * from the resolver using the given source string as a template.
    655     *
    656     * @param source  the string to replace in, null returns null
    657     * @return The result of the replace operation
    658     */
    659    public String replace(final String source) {
    660        if (source == null) {
    661            return null;
    662        }
    663        final StrBuilder buf = new StrBuilder(source);
    664        if (!substitute(buf, 0, source.length())) {
    665            return source;
    666        }
    667        return buf.toString();
    668    }
    669
    670    /**
    671     * Replaces all the occurrences of variables with their matching values
    672     * from the resolver using the given source string as a template.
    673     * <p>
    674     * Only the specified portion of the string will be processed.
    675     * The rest of the string is not processed, and is not returned.
    676     * </p>
    677     *
    678     * @param source  the string to replace in, null returns null
    679     * @param offset  the start offset within the array, must be valid
    680     * @param length  the length within the array to be processed, must be valid
    681     * @return The result of the replace operation
    682     */
    683    public String replace(final String source, final int offset, final int length) {
    684        if (source == null) {
    685            return null;
    686        }
    687        final StrBuilder buf = new StrBuilder(length).append(source, offset, length);
    688        if (!substitute(buf, 0, length)) {
    689            return source.substring(offset, offset + length);
    690        }
    691        return buf.toString();
    692    }
    693
    694    /**
    695     * Replaces all the occurrences of variables with their matching values
    696     * from the resolver using the given source buffer as a template.
    697     * The buffer is not altered by this method.
    698     *
    699     * @param source  the buffer to use as a template, not changed, null returns null
    700     * @return The result of the replace operation
    701     */
    702    public String replace(final StringBuffer source) {
    703        if (source == null) {
    704            return null;
    705        }
    706        final StrBuilder buf = new StrBuilder(source.length()).append(source);
    707        substitute(buf, 0, buf.length());
    708        return buf.toString();
    709    }
    710
    711    /**
    712     * Replaces all the occurrences of variables with their matching values
    713     * from the resolver using the given source buffer as a template.
    714     * The buffer is not altered by this method.
    715     * <p>
    716     * Only the specified portion of the buffer will be processed.
    717     * The rest of the buffer is not processed, and is not returned.
    718     * </p>
    719     *
    720     * @param source  the buffer to use as a template, not changed, null returns null
    721     * @param offset  the start offset within the array, must be valid
    722     * @param length  the length within the array to be processed, must be valid
    723     * @return The result of the replace operation
    724     */
    725    public String replace(final StringBuffer source, final int offset, final int length) {
    726        if (source == null) {
    727            return null;
    728        }
    729        final StrBuilder buf = new StrBuilder(length).append(source, offset, length);
    730        substitute(buf, 0, length);
    731        return buf.toString();
    732    }
    733
    734    /**
    735     * Replaces all the occurrences of variables within the given source
    736     * builder with their matching values from the resolver.
    737     *
    738     * @param source  the builder to replace in, updated, null returns zero
    739     * @return true if altered
    740     */
    741    public boolean replaceIn(final StrBuilder source) {
    742        if (source == null) {
    743            return false;
    744        }
    745        return substitute(source, 0, source.length());
    746    }
    747
    748    /**
    749     * Replaces all the occurrences of variables within the given source
    750     * builder with their matching values from the resolver.
    751     * <p>
    752     * Only the specified portion of the builder will be processed.
    753     * The rest of the builder is not processed, but it is not deleted.
    754     * </p>
    755     *
    756     * @param source  the builder to replace in, null returns zero
    757     * @param offset  the start offset within the array, must be valid
    758     * @param length  the length within the builder to be processed, must be valid
    759     * @return true if altered
    760     */
    761    public boolean replaceIn(final StrBuilder source, final int offset, final int length) {
    762        if (source == null) {
    763            return false;
    764        }
    765        return substitute(source, offset, length);
    766    }
    767
    768    /**
    769     * Replaces all the occurrences of variables within the given source buffer
    770     * with their matching values from the resolver.
    771     * The buffer is updated with the result.
    772     *
    773     * @param source  the buffer to replace in, updated, null returns zero
    774     * @return true if altered
    775     */
    776    public boolean replaceIn(final StringBuffer source) {
    777        if (source == null) {
    778            return false;
    779        }
    780        return replaceIn(source, 0, source.length());
    781    }
    782
    783    /**
    784     * Replaces all the occurrences of variables within the given source buffer
    785     * with their matching values from the resolver.
    786     * The buffer is updated with the result.
    787     * <p>
    788     * Only the specified portion of the buffer will be processed.
    789     * The rest of the buffer is not processed, but it is not deleted.
    790     * </p>
    791     *
    792     * @param source  the buffer to replace in, updated, null returns zero
    793     * @param offset  the start offset within the array, must be valid
    794     * @param length  the length within the buffer to be processed, must be valid
    795     * @return true if altered
    796     */
    797    public boolean replaceIn(final StringBuffer source, final int offset, final int length) {
    798        if (source == null) {
    799            return false;
    800        }
    801        final StrBuilder buf = new StrBuilder(length).append(source, offset, length);
    802        if (!substitute(buf, 0, length)) {
    803            return false;
    804        }
    805        source.replace(offset, offset + length, buf.toString());
    806        return true;
    807    }
    808
    809    /**
    810     * Replaces all the occurrences of variables within the given source buffer
    811     * with their matching values from the resolver.
    812     * The buffer is updated with the result.
    813     *
    814     * @param source  the buffer to replace in, updated, null returns zero
    815     * @return true if altered
    816     */
    817    public boolean replaceIn(final StringBuilder source) {
    818        if (source == null) {
    819            return false;
    820        }
    821        return replaceIn(source, 0, source.length());
    822    }
    823
    824    /**
    825     * Replaces all the occurrences of variables within the given source builder
    826     * with their matching values from the resolver.
    827     * The builder is updated with the result.
    828     * <p>
    829     * Only the specified portion of the buffer will be processed.
    830     * The rest of the buffer is not processed, but it is not deleted.
    831     * </p>
    832     *
    833     * @param source  the buffer to replace in, updated, null returns zero
    834     * @param offset  the start offset within the array, must be valid
    835     * @param length  the length within the buffer to be processed, must be valid
    836     * @return true if altered
    837     */
    838    public boolean replaceIn(final StringBuilder source, final int offset, final int length) {
    839        if (source == null) {
    840            return false;
    841        }
    842        final StrBuilder buf = new StrBuilder(length).append(source, offset, length);
    843        if (!substitute(buf, 0, length)) {
    844            return false;
    845        }
    846        source.replace(offset, offset + length, buf.toString());
    847        return true;
    848    }
    849
    850    /**
    851     * Internal method that resolves the value of a variable.
    852     * <p>
    853     * Most users of this class do not need to call this method. This method is
    854     * called automatically by the substitution process.
    855     * </p>
    856     * <p>
    857     * Writers of subclasses can override this method if they need to alter
    858     * how each substitution occurs. The method is passed the variable's name
    859     * and must return the corresponding value. This implementation uses the
    860     * {@link #getVariableResolver()} with the variable's name as the key.
    861     * </p>
    862     *
    863     * @param variableName  the name of the variable, not null
    864     * @param buf  the buffer where the substitution is occurring, not null
    865     * @param startPos  the start position of the variable including the prefix, valid
    866     * @param endPos  the end position of the variable including the suffix, valid
    867     * @return The variable's value or <strong>null</strong> if the variable is unknown
    868     */
    869    protected String resolveVariable(final String variableName,
    870                                     final StrBuilder buf,
    871                                     final int startPos,
    872                                     final int endPos) {
    873        final StrLookup<?> resolver = getVariableResolver();
    874        if (resolver == null) {
    875            return null;
    876        }
    877        return resolver.lookup(variableName);
    878    }
    879
    880    /**
    881     * Sets a flag whether substitution is done in variable values (recursive).
    882     *
    883     * @param disableSubstitutionInValues true if substitution in variable value are disabled
    884     * @since 1.2
    885     */
    886    public void setDisableSubstitutionInValues(final boolean disableSubstitutionInValues) {
    887        this.disableSubstitutionInValues = disableSubstitutionInValues;
    888    }
    889
    890    /**
    891     * Sets a flag whether substitution is done in variable names. If set to
    892     * <strong>true</strong>, the names of variables can contain other variables which are
    893     * processed first before the original variable is evaluated, e.g.
    894     * {@code ${jre-${java.version}}}. The default value is <strong>false</strong>.
    895     *
    896     * @param enableSubstitutionInVariables the new value of the flag
    897     */
    898    public void setEnableSubstitutionInVariables(
    899            final boolean enableSubstitutionInVariables) {
    900        this.enableSubstitutionInVariables = enableSubstitutionInVariables;
    901    }
    902
    903    /**
    904     * Sets the escape character.
    905     * If this character is placed before a variable reference in the source
    906     * text, this variable will be ignored.
    907     *
    908     * @param escapeCharacter  the escape character (0 for disabling escaping)
    909     */
    910    public void setEscapeChar(final char escapeCharacter) {
    911        this.escapeChar = escapeCharacter;
    912    }
    913
    914    /**
    915     * Sets a flag controlling whether escapes are preserved during
    916     * substitution.  If set to <strong>true</strong>, the escape character is retained
    917     * during substitution (e.g. {@code $${this-is-escaped}} remains
    918     * {@code $${this-is-escaped}}).  If set to <strong>false</strong>, the escape
    919     * character is removed during substitution (e.g.
    920     * {@code $${this-is-escaped}} becomes
    921     * {@code ${this-is-escaped}}).  The default value is <strong>false</strong>
    922     *
    923     * @param preserveEscapes true if escapes are to be preserved
    924     */
    925    public void setPreserveEscapes(final boolean preserveEscapes) {
    926        this.preserveEscapes = preserveEscapes;
    927    }
    928
    929    /**
    930     * Sets the variable default value delimiter to use.
    931     * <p>
    932     * The variable default value delimiter is the character or characters that delimit the
    933     * variable name and the variable default value. This method allows a single character
    934     * variable default value delimiter to be easily set.
    935     * </p>
    936     *
    937     * @param valueDelimiter  the variable default value delimiter character to use
    938     * @return this, to enable chaining
    939     */
    940    public StrSubstitutor setValueDelimiter(final char valueDelimiter) {
    941        return setValueDelimiterMatcher(StrMatcher.charMatcher(valueDelimiter));
    942    }
    943
    944    /**
    945     * Sets the variable default value delimiter to use.
    946     * <p>
    947     * The variable default value delimiter is the character or characters that delimit the
    948     * variable name and the variable default value. This method allows a string
    949     * variable default value delimiter to be easily set.
    950     * </p>
    951     * <p>
    952     * If the {@code valueDelimiter} is null or empty string, then the variable default
    953     * value resolution becomes disabled.
    954     * </p>
    955     *
    956     * @param valueDelimiter  the variable default value delimiter string to use, may be null or empty
    957     * @return this, to enable chaining
    958     */
    959    public StrSubstitutor setValueDelimiter(final String valueDelimiter) {
    960        if (valueDelimiter == null || valueDelimiter.isEmpty()) {
    961            setValueDelimiterMatcher(null);
    962            return this;
    963        }
    964        return setValueDelimiterMatcher(StrMatcher.stringMatcher(valueDelimiter));
    965    }
    966
    967    /**
    968     * Sets the variable default value delimiter matcher to use.
    969     * <p>
    970     * The variable default value delimiter is the character or characters that delimit the
    971     * variable name and the variable default value. This delimiter is expressed in terms of a matcher
    972     * allowing advanced variable default value delimiter matches.
    973     * </p>
    974     * <p>
    975     * If the {@code valueDelimiterMatcher} is null, then the variable default value resolution
    976     * becomes disabled.
    977     * </p>
    978     *
    979     * @param valueDelimiterMatcher  variable default value delimiter matcher to use, may be null
    980     * @return this, to enable chaining
    981     */
    982    public StrSubstitutor setValueDelimiterMatcher(final StrMatcher valueDelimiterMatcher) {
    983        this.valueDelimiterMatcher = valueDelimiterMatcher;
    984        return this;
    985    }
    986
    987    /**
    988     * Sets the variable prefix to use.
    989     * <p>
    990     * The variable prefix is the character or characters that identify the
    991     * start of a variable. This method allows a single character prefix to
    992     * be easily set.
    993     * </p>
    994     *
    995     * @param prefix  the prefix character to use
    996     * @return this, to enable chaining
    997     */
    998    public StrSubstitutor setVariablePrefix(final char prefix) {
    999        return setVariablePrefixMatcher(StrMatcher.charMatcher(prefix));
    1000    }
    1001
    1002    /**
    1003     * Sets the variable prefix to use.
    1004     * <p>
    1005     * The variable prefix is the character or characters that identify the
    1006     * start of a variable. This method allows a string prefix to be easily set.
    1007     * </p>
    1008     *
    1009     * @param prefix  the prefix for variables, not null
    1010     * @return this, to enable chaining
    1011     * @throws IllegalArgumentException if the prefix is null
    1012     */
    1013    public StrSubstitutor setVariablePrefix(final String prefix) {
    1014        Validate.isTrue(prefix != null, "Variable prefix must not be null!");
    1015        return setVariablePrefixMatcher(StrMatcher.stringMatcher(prefix));
    1016    }
    1017
    1018    /**
    1019     * Sets the variable prefix matcher currently in use.
    1020     * <p>
    1021     * The variable prefix is the character or characters that identify the
    1022     * start of a variable. This prefix is expressed in terms of a matcher
    1023     * allowing advanced prefix matches.
    1024     * </p>
    1025     *
    1026     * @param prefixMatcher  the prefix matcher to use, null ignored
    1027     * @return this, to enable chaining
    1028     * @throws IllegalArgumentException if the prefix matcher is null
    1029     */
    1030    public StrSubstitutor setVariablePrefixMatcher(final StrMatcher prefixMatcher) {
    1031        Validate.isTrue(prefixMatcher != null, "Variable prefix matcher must not be null!");
    1032        this.prefixMatcher = prefixMatcher;
    1033        return this;
    1034    }
    1035
    1036    /**
    1037     * Sets the VariableResolver that is used to lookup variables.
    1038     *
    1039     * @param variableResolver  the VariableResolver
    1040     */
    1041    public void setVariableResolver(final StrLookup<?> variableResolver) {
    1042        this.variableResolver = variableResolver;
    1043    }
    1044
    1045    /**
    1046     * Sets the variable suffix to use.
    1047     * <p>
    1048     * The variable suffix is the character or characters that identify the
    1049     * end of a variable. This method allows a single character suffix to
    1050     * be easily set.
    1051     * </p>
    1052     *
    1053     * @param suffix  the suffix character to use
    1054     * @return this, to enable chaining
    1055     */
    1056    public StrSubstitutor setVariableSuffix(final char suffix) {
    1057        return setVariableSuffixMatcher(StrMatcher.charMatcher(suffix));
    1058    }
    1059
    1060    /**
    1061     * Sets the variable suffix to use.
    1062     * <p>
    1063     * The variable suffix is the character or characters that identify the
    1064     * end of a variable. This method allows a string suffix to be easily set.
    1065     * </p>
    1066     *
    1067     * @param suffix  the suffix for variables, not null
    1068     * @return this, to enable chaining
    1069     * @throws IllegalArgumentException if the suffix is null
    1070     */
    1071    public StrSubstitutor setVariableSuffix(final String suffix) {
    1072        Validate.isTrue(suffix != null, "Variable suffix must not be null!");
    1073        return setVariableSuffixMatcher(StrMatcher.stringMatcher(suffix));
    1074    }
    1075
    1076    /**
    1077     * Sets the variable suffix matcher currently in use.
    1078     * <p>
    1079     * The variable suffix is the character or characters that identify the
    1080     * end of a variable. This suffix is expressed in terms of a matcher
    1081     * allowing advanced suffix matches.
    1082     * </p>
    1083     *
    1084     * @param suffixMatcher  the suffix matcher to use, null ignored
    1085     * @return this, to enable chaining
    1086     * @throws IllegalArgumentException if the suffix matcher is null
    1087     */
    1088    public StrSubstitutor setVariableSuffixMatcher(final StrMatcher suffixMatcher) {
    1089        Validate.isTrue(suffixMatcher != null, "Variable suffix matcher must not be null!");
    1090        this.suffixMatcher = suffixMatcher;
    1091        return this;
    1092    }
    1093
    1094    /**
    1095     * Internal method that substitutes the variables.
    1096     * <p>
    1097     * Most users of this class do not need to call this method. This method will
    1098     * be called automatically by another (public) method.
    1099     * </p>
    1100     * <p>
    1101     * Writers of subclasses can override this method if they need access to
    1102     * the substitution process at the start or end.
    1103     * </p>
    1104     *
    1105     * @param buf  the string builder to substitute into, not null
    1106     * @param offset  the start offset within the builder, must be valid
    1107     * @param length  the length within the builder to be processed, must be valid
    1108     * @return true if altered
    1109     */
    1110    protected boolean substitute(final StrBuilder buf, final int offset, final int length) {
    1111        return substitute(buf, offset, length, null) > 0;
    1112    }
    1113
    1114    /**
    1115     * Recursive handler for multiple levels of interpolation. This is the main
    1116     * interpolation method, which resolves the values of all variable references
    1117     * contained in the passed in text.
    1118     *
    1119     * @param buf  the string builder to substitute into, not null
    1120     * @param offset  the start offset within the builder, must be valid
    1121     * @param length  the length within the builder to be processed, must be valid
    1122     * @param priorVariables  the stack keeping track of the replaced variables, may be null
    1123     * @return The length change that occurs, unless priorVariables is null when the int
    1124     *  represents a boolean flag as to whether any change occurred.
    1125     */
    1126    private int substitute(final StrBuilder buf, final int offset, final int length, List<String> priorVariables) {
    1127        final StrMatcher pfxMatcher = getVariablePrefixMatcher();
    1128        final StrMatcher suffMatcher = getVariableSuffixMatcher();
    1129        final char escape = getEscapeChar();
    1130        final StrMatcher valueDelimMatcher = getValueDelimiterMatcher();
    1131        final boolean substitutionInVariablesEnabled = isEnableSubstitutionInVariables();
    1132        final boolean substitutionInValuesDisabled = isDisableSubstitutionInValues();
    1133
    1134        final boolean top = priorVariables == null;
    1135        boolean altered = false;
    1136        int lengthChange = 0;
    1137        char[] chars = buf.buffer;
    1138        int bufEnd = offset + length;
    1139        int pos = offset;
    1140        while (pos < bufEnd) {
    1141            final int startMatchLen = pfxMatcher.isMatch(chars, pos, offset,
    1142                    bufEnd);
    1143            if (startMatchLen == 0) {
    1144                pos++;
    1145            } else // found variable start marker
    1146            if (pos > offset && chars[pos - 1] == escape) {
    1147                // escaped
    1148                if (preserveEscapes) {
    1149                    pos++;
    1150                    continue;
    1151                }
    1152                buf.deleteCharAt(pos - 1);
    1153                chars = buf.buffer; // in case buffer was altered
    1154                lengthChange--;
    1155                altered = true;
    1156                bufEnd--;
    1157            } else {
    1158                // find suffix
    1159                final int startPos = pos;
    1160                pos += startMatchLen;
    1161                int endMatchLen = 0;
    1162                int nestedVarCount = 0;
    1163                while (pos < bufEnd) {
    1164                    if (substitutionInVariablesEnabled
    1165                            && pfxMatcher.isMatch(chars,
    1166                                    pos, offset, bufEnd) != 0) {
    1167                        // found a nested variable start
    1168                        endMatchLen = pfxMatcher.isMatch(chars,
    1169                                pos, offset, bufEnd);
    1170                        nestedVarCount++;
    1171                        pos += endMatchLen;
    1172                        continue;
    1173                    }
    1174
    1175                    endMatchLen = suffMatcher.isMatch(chars, pos, offset,
    1176                            bufEnd);
    1177                    if (endMatchLen == 0) {
    1178                        pos++;
    1179                    } else {
    1180                        // found variable end marker
    1181                        if (nestedVarCount == 0) {
    1182                            String varNameExpr = new String(chars, startPos
    1183                                    + startMatchLen, pos - startPos
    1184                                    - startMatchLen);
    1185                            if (substitutionInVariablesEnabled) {
    1186                                final StrBuilder bufName = new StrBuilder(varNameExpr);
    1187                                substitute(bufName, 0, bufName.length());
    1188                                varNameExpr = bufName.toString();
    1189                            }
    1190                            pos += endMatchLen;
    1191                            final int endPos = pos;
    1192
    1193                            String varName = varNameExpr;
    1194                            String varDefaultValue = null;
    1195
    1196                            if (valueDelimMatcher != null) {
    1197                                final char[] varNameExprChars = varNameExpr.toCharArray();
    1198                                int valueDelimiterMatchLen = 0;
    1199                                for (int i = 0; i < varNameExprChars.length; i++) {
    1200                                    // if there's any nested variable when nested variable substitution disabled,
    1201                                    // then stop resolving name and default value.
    1202                                    if (!substitutionInVariablesEnabled
    1203                                            && pfxMatcher.isMatch(varNameExprChars,
    1204                                                                    i,
    1205                                                                    i,
    1206                                                                    varNameExprChars.length) != 0) {
    1207                                        break;
    1208                                    }
    1209                                    if (valueDelimMatcher.isMatch(varNameExprChars, i) != 0) {
    1210                                        valueDelimiterMatchLen = valueDelimMatcher.isMatch(varNameExprChars, i);
    1211                                        varName = varNameExpr.substring(0, i);
    1212                                        varDefaultValue = varNameExpr.substring(i + valueDelimiterMatchLen);
    1213                                        break;
    1214                                    }
    1215                                }
    1216                            }
    1217
    1218                            // on the first call initialize priorVariables
    1219                            if (priorVariables == null) {
    1220                                priorVariables = new ArrayList<>();
    1221                                priorVariables.add(new String(chars,
    1222                                        offset, length));
    1223                            }
    1224
    1225                            // handle cyclic substitution
    1226                            checkCyclicSubstitution(varName, priorVariables);
    1227                            priorVariables.add(varName);
    1228
    1229                            // resolve the variable
    1230                            String varValue = resolveVariable(varName, buf,
    1231                                    startPos, endPos);
    1232                            if (varValue == null) {
    1233                                varValue = varDefaultValue;
    1234                            }
    1235                            if (varValue != null) {
    1236                                final int varLen = varValue.length();
    1237                                buf.replace(startPos, endPos, varValue);
    1238                                altered = true;
    1239                                int change = 0;
    1240                                if (!substitutionInValuesDisabled) { // recursive replace
    1241                                    change = substitute(buf, startPos,
    1242                                        varLen, priorVariables);
    1243                                }
    1244                                change = change
    1245                                    + varLen - (endPos - startPos);
    1246                                pos += change;
    1247                                bufEnd += change;
    1248                                lengthChange += change;
    1249                                chars = buf.buffer; // in case buffer was
    1250                                                    // altered
    1251                            }
    1252
    1253                            // remove variable from the cyclic stack
    1254                            priorVariables
    1255                                    .remove(priorVariables.size() - 1);
    1256                            break;
    1257                        }
    1258                        nestedVarCount--;
    1259                        pos += endMatchLen;
    1260                    }
    1261                }
    1262            }
    1263        }
    1264        if (top) {
    1265            return altered ? 1 : 0;
    1266        }
    1267        return lengthChange;
    1268    }
    1269}