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