    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     */
    017    package org.apache.commons.lang3.text;
    018
    019    import java.util.ArrayList;
    020    import java.util.Enumeration;
    021    import java.util.HashMap;
    022    import java.util.List;
    023    import java.util.Map;
    024    import java.util.Properties;
    025
    026    /**
    027     * Substitutes variables within a string by values.
    028     * <p>
    029     * This class takes a piece of text and substitutes all the variables within it.
    030     * The default definition of a variable is <code>${variableName}</code>.
    031     * The prefix and suffix can be changed via constructors and set methods.
    032     * <p>
    033     * Variable values are typically resolved from a map, but could also be resolved
    034     * from system properties, or by supplying a custom variable resolver.
    035     * <p>
    036     * The simplest example is to use this class to replace Java System properties. For example:
    037     * <pre>
    038     * StrSubstitutor.replaceSystemProperties(
    039     *      "You are running with java.version = ${java.version} and os.name = ${os.name}.");
    040     * </pre>
    041     * <p>
    042     * Typical usage of this class follows the following pattern: First an instance is created
    043     * and initialized with the map that contains the values for the available variables.
    044     * If a prefix and/or suffix for variables should be used other than the default ones,
    045     * the appropriate settings can be performed. After that the <code>replace()</code>
    046     * method can be called passing in the source text for interpolation. In the returned
    047     * text all variable references (as long as their values are known) will be resolved.
    048     * The following example demonstrates this:
    049     * <pre>
    050     * Map valuesMap = HashMap();
    051     * valuesMap.put(&quot;animal&quot;, &quot;quick brown fox&quot;);
    052     * valuesMap.put(&quot;target&quot;, &quot;lazy dog&quot;);
    053     * String templateString = &quot;The ${animal} jumped over the ${target}.&quot;;
    054     * StrSubstitutor sub = new StrSubstitutor(valuesMap);
    055     * String resolvedString = sub.replace(templateString);
    056     * </pre>
    057     * yielding:
    058     * <pre>
    059     *      The quick brown fox jumped over the lazy dog.
    060     * </pre>
    061     * <p>
    062     * In addition to this usage pattern there are some static convenience methods that
    063     * cover the most common use cases. These methods can be used without the need of
    064     * manually creating an instance. However if multiple replace operations are to be
    065     * performed, creating and reusing an instance of this class will be more efficient.
    066     * <p>
    067     * Variable replacement works in a recursive way. Thus, if a variable value contains
    068     * a variable then that variable will also be replaced. Cyclic replacements are
    069     * detected and will cause an exception to be thrown.
    070     * <p>
    071     * Sometimes the interpolation's result must contain a variable prefix. As an example
    072     * take the following source text:
    073     * <pre>
    074     *   The variable ${${name}} must be used.
    075     * </pre>
    076     * Here only the variable's name referred to in the text should be replaced resulting
    077     * in the text (assuming that the value of the <code>name</code> variable is <code>x</code>):
    078     * <pre>
    079     *   The variable ${x} must be used.
    080     * </pre>
    081     * To achieve this effect there are two possibilities: Either set a different prefix
    082     * and suffix for variables which do not conflict with the result text you want to
    083     * produce. The other possibility is to use the escape character, by default '$'.
    084     * If this character is placed before a variable reference, this reference is ignored
    085     * and won't be replaced. For example:
    086     * <pre>
    087     *   The variable $${${name}} must be used.
    088     * </pre>
    089     * <p>
    090     * In some complex scenarios you might even want to perform substitution in the
    091     * names of variables, for instance
    092     * <pre>
    093     * ${jre-${java.specification.version}}
    094     * </pre>
    095     * <code>StrSubstitutor</code> supports this recursive substitution in variable
    096     * names, but it has to be enabled explicitly by setting the
    097     * {@link #setEnableSubstitutionInVariables(boolean) enableSubstitutionInVariables}
    098     * property to <b>true</b>.
    099     *
    100     * @version $Id: StrSubstitutor.java 1199894 2011-11-09 17:53:59Z ggregory $
    101     * @since 2.2
    102     */
    103    public class StrSubstitutor {
    104
    105        /**
    106         * Constant for the default escape character.
    107         */
    108        public static final char DEFAULT_ESCAPE = '$';
    109        /**
    110         * Constant for the default variable prefix.
    111         */
    112        public static final StrMatcher DEFAULT_PREFIX = StrMatcher.stringMatcher("${");
    113        /**
    114         * Constant for the default variable suffix.
    115         */
    116        public static final StrMatcher DEFAULT_SUFFIX = StrMatcher.stringMatcher("}");
    117
    118        /**
    119         * Stores the escape character.
    120         */
    121        private char escapeChar;
    122        /**
    123         * Stores the variable prefix.
    124         */
    125        private StrMatcher prefixMatcher;
    126        /**
    127         * Stores the variable suffix.
    128         */
    129        private StrMatcher suffixMatcher;
    130        /**
    131         * Variable resolution is delegated to an implementor of VariableResolver.
    132         */
    133        private StrLookup<?> variableResolver;
    134        /**
    135         * The flag whether substitution in variable names is enabled.
    136         */
    137        private boolean enableSubstitutionInVariables;
    138
    139        //-----------------------------------------------------------------------
    140        /**
    141         * Replaces all the occurrences of variables in the given source object with
    142         * their matching values from the map.
    143         *
    144         * @param <V> the type of the values in the map
    145         * @param source  the source text containing the variables to substitute, null returns null
    146         * @param valueMap  the map with the values, may be null
    147         * @return the result of the replace operation
    148         */
    149        public static <V> String replace(Object source, Map<String, V> valueMap) {
    150            return new StrSubstitutor(valueMap).replace(source);
    151        }
    152
    153        /**
    154         * Replaces all the occurrences of variables in the given source object with
    155         * their matching values from the map. This method allows to specifiy a
    156         * custom variable prefix and suffix
    157         *
    158         * @param <V> the type of the values in the map
    159         * @param source  the source text containing the variables to substitute, null returns null
    160         * @param valueMap  the map with the values, may be null
    161         * @param prefix  the prefix of variables, not null
    162         * @param suffix  the suffix of variables, not null
    163         * @return the result of the replace operation
    164         * @throws IllegalArgumentException if the prefix or suffix is null
    165         */
    166        public static <V> String replace(Object source, Map<String, V> valueMap, String prefix, String suffix) {
    167            return new StrSubstitutor(valueMap, prefix, suffix).replace(source);
    168        }
    169
    170        /**
    171         * Replaces all the occurrences of variables in the given source object with their matching
    172         * values from the properties.
    173         *
    174         * @param source the source text containing the variables to substitute, null returns null
    175         * @param valueProperties the properties with values, may be null
    176         * @return the result of the replace operation
    177         */
    178        public static String replace(Object source, Properties valueProperties) {
    179            if (valueProperties == null) {
    180                return source.toString();
    181            }
    182            Map<String,String> valueMap = new HashMap<String,String>();
    183            Enumeration<?> propNames = valueProperties.propertyNames();
    184            while (propNames.hasMoreElements()) {
    185                String propName = (String)propNames.nextElement();
    186                String propValue = valueProperties.getProperty(propName);
    187                valueMap.put(propName, propValue);
    188            }
    189            return StrSubstitutor.replace(source, valueMap);
    190        }
    191
    192        /**
    193         * Replaces all the occurrences of variables in the given source object with
    194         * their matching values from the system properties.
    195         *
    196         * @param source  the source text containing the variables to substitute, null returns null
    197         * @return the result of the replace operation
    198         */
    199        public static String replaceSystemProperties(Object source) {
    200            return new StrSubstitutor(StrLookup.systemPropertiesLookup()).replace(source);
    201        }
    202
    203        //-----------------------------------------------------------------------
    204        /**
    205         * Creates a new instance with defaults for variable prefix and suffix
    206         * and the escaping character.
    207         */
    208        public StrSubstitutor() {
    209            this((StrLookup<?>) null, DEFAULT_PREFIX, DEFAULT_SUFFIX, DEFAULT_ESCAPE);
    210        }
    211
    212        /**
    213         * Creates a new instance and initializes it. Uses defaults for variable
    214         * prefix and suffix and the escaping character.
    215         *
    216         * @param <V> the type of the values in the map
    217         * @param valueMap  the map with the variables' values, may be null
    218         */
    219        public <V> StrSubstitutor(Map<String, V> valueMap) {
    220            this(StrLookup.mapLookup(valueMap), DEFAULT_PREFIX, DEFAULT_SUFFIX, DEFAULT_ESCAPE);
    221        }
    222
    223        /**
    224         * Creates a new instance and initializes it. Uses a default escaping character.
    225         *
    226         * @param <V> the type of the values in the map
    227         * @param valueMap  the map with the variables' values, may be null
    228         * @param prefix  the prefix for variables, not null
    229         * @param suffix  the suffix for variables, not null
    230         * @throws IllegalArgumentException if the prefix or suffix is null
    231         */
    232        public <V> StrSubstitutor(Map<String, V> valueMap, String prefix, String suffix) {
    233            this(StrLookup.mapLookup(valueMap), prefix, suffix, DEFAULT_ESCAPE);
    234        }
    235
    236        /**
    237         * Creates a new instance and initializes it.
    238         *
    239         * @param <V> the type of the values in the map
    240         * @param valueMap  the map with the variables' values, may be null
    241         * @param prefix  the prefix for variables, not null
    242         * @param suffix  the suffix for variables, not null
    243         * @param escape  the escape character
    244         * @throws IllegalArgumentException if the prefix or suffix is null
    245         */
    246        public <V> StrSubstitutor(Map<String, V> valueMap, String prefix, String suffix, char escape) {
    247            this(StrLookup.mapLookup(valueMap), prefix, suffix, escape);
    248        }
    249
    250        /**
    251         * Creates a new instance and initializes it.
    252         *
    253         * @param variableResolver  the variable resolver, may be null
    254         */
    255        public StrSubstitutor(StrLookup<?> variableResolver) {
    256            this(variableResolver, DEFAULT_PREFIX, DEFAULT_SUFFIX, DEFAULT_ESCAPE);
    257        }
    258
    259        /**
    260         * Creates a new instance and initializes it.
    261         *
    262         * @param variableResolver  the variable resolver, may be null
    263         * @param prefix  the prefix for variables, not null
    264         * @param suffix  the suffix for variables, not null
    265         * @param escape  the escape character
    266         * @throws IllegalArgumentException if the prefix or suffix is null
    267         */
    268        public StrSubstitutor(StrLookup<?> variableResolver, String prefix, String suffix, char escape) {
    269            this.setVariableResolver(variableResolver);
    270            this.setVariablePrefix(prefix);
    271            this.setVariableSuffix(suffix);
    272            this.setEscapeChar(escape);
    273        }
    274
    275        /**
    276         * Creates a new instance and initializes it.
    277         *
    278         * @param variableResolver  the variable resolver, may be null
    279         * @param prefixMatcher  the prefix for variables, not null
    280         * @param suffixMatcher  the suffix for variables, not null
    281         * @param escape  the escape character
    282         * @throws IllegalArgumentException if the prefix or suffix is null
    283         */
    284        public StrSubstitutor(
    285                StrLookup<?> variableResolver, StrMatcher prefixMatcher, StrMatcher suffixMatcher, char escape) {
    286            this.setVariableResolver(variableResolver);
    287            this.setVariablePrefixMatcher(prefixMatcher);
    288            this.setVariableSuffixMatcher(suffixMatcher);
    289            this.setEscapeChar(escape);
    290        }
    291
    292        //-----------------------------------------------------------------------
    293        /**
    294         * Replaces all the occurrences of variables with their matching values
    295         * from the resolver using the given source string as a template.
    296         *
    297         * @param source  the string to replace in, null returns null
    298         * @return the result of the replace operation
    299         */
    300        public String replace(String source) {
    301            if (source == null) {
    302                return null;
    303            }
    304            StrBuilder buf = new StrBuilder(source);
    305            if (substitute(buf, 0, source.length()) == false) {
    306                return source;
    307            }
    308            return buf.toString();
    309        }
    310
    311        /**
    312         * Replaces all the occurrences of variables with their matching values
    313         * from the resolver using the given source string as a template.
    314         * <p>
    315         * Only the specified portion of the string will be processed.
    316         * The rest of the string is not processed, and is not returned.
    317         *
    318         * @param source  the string to replace in, null returns null
    319         * @param offset  the start offset within the array, must be valid
    320         * @param length  the length within the array to be processed, must be valid
    321         * @return the result of the replace operation
    322         */
    323        public String replace(String source, int offset, int length) {
    324            if (source == null) {
    325                return null;
    326            }
    327            StrBuilder buf = new StrBuilder(length).append(source, offset, length);
    328            if (substitute(buf, 0, length) == false) {
    329                return source.substring(offset, offset + length);
    330            }
    331            return buf.toString();
    332        }
    333
    334        //-----------------------------------------------------------------------
    335        /**
    336         * Replaces all the occurrences of variables with their matching values
    337         * from the resolver using the given source array as a template.
    338         * The array is not altered by this method.
    339         *
    340         * @param source  the character array to replace in, not altered, null returns null
    341         * @return the result of the replace operation
    342         */
    343        public String replace(char[] source) {
    344            if (source == null) {
    345                return null;
    346            }
    347            StrBuilder buf = new StrBuilder(source.length).append(source);
    348            substitute(buf, 0, source.length);
    349            return buf.toString();
    350        }
    351
    352        /**
    353         * Replaces all the occurrences of variables with their matching values
    354         * from the resolver using the given source array as a template.
    355         * The array is not altered by this method.
    356         * <p>
    357         * Only the specified portion of the array will be processed.
    358         * The rest of the array is not processed, and is not returned.
    359         *
    360         * @param source  the character array to replace in, not altered, null returns null
    361         * @param offset  the start offset within the array, must be valid
    362         * @param length  the length within the array to be processed, must be valid
    363         * @return the result of the replace operation
    364         */
    365        public String replace(char[] source, int offset, int length) {
    366            if (source == null) {
    367                return null;
    368            }
    369            StrBuilder buf = new StrBuilder(length).append(source, offset, length);
    370            substitute(buf, 0, length);
    371            return buf.toString();
    372        }
    373
    374        //-----------------------------------------------------------------------
    375        /**
    376         * Replaces all the occurrences of variables with their matching values
    377         * from the resolver using the given source buffer as a template.
    378         * The buffer is not altered by this method.
    379         *
    380         * @param source  the buffer to use as a template, not changed, null returns null
    381         * @return the result of the replace operation
    382         */
    383        public String replace(StringBuffer source) {
    384            if (source == null) {
    385                return null;
    386            }
    387            StrBuilder buf = new StrBuilder(source.length()).append(source);
    388            substitute(buf, 0, buf.length());
    389            return buf.toString();
    390        }
    391
    392        /**
    393         * Replaces all the occurrences of variables with their matching values
    394         * from the resolver using the given source buffer as a template.
    395         * The buffer is not altered by this method.
    396         * <p>
    397         * Only the specified portion of the buffer will be processed.
    398         * The rest of the buffer is not processed, and is not returned.
    399         *
    400         * @param source  the buffer to use as a template, not changed, null returns null
    401         * @param offset  the start offset within the array, must be valid
    402         * @param length  the length within the array to be processed, must be valid
    403         * @return the result of the replace operation
    404         */
    405        public String replace(StringBuffer source, int offset, int length) {
    406            if (source == null) {
    407                return null;
    408            }
    409            StrBuilder buf = new StrBuilder(length).append(source, offset, length);
    410            substitute(buf, 0, length);
    411            return buf.toString();
    412        }
    413
    414        //-----------------------------------------------------------------------
    415        /**
    416         * Replaces all the occurrences of variables with their matching values
    417         * from the resolver using the given source builder as a template.
    418         * The builder is not altered by this method.
    419         *
    420         * @param source  the builder to use as a template, not changed, null returns null
    421         * @return the result of the replace operation
    422         */
    423        public String replace(StrBuilder source) {
    424            if (source == null) {
    425                return null;
    426            }
    427            StrBuilder buf = new StrBuilder(source.length()).append(source);
    428            substitute(buf, 0, buf.length());
    429            return buf.toString();
    430        }
    431
    432        /**
    433         * Replaces all the occurrences of variables with their matching values
    434         * from the resolver using the given source builder as a template.
    435         * The builder is not altered by this method.
    436         * <p>
    437         * Only the specified portion of the builder will be processed.
    438         * The rest of the builder is not processed, and is not returned.
    439         *
    440         * @param source  the builder to use as a template, not changed, null returns null
    441         * @param offset  the start offset within the array, must be valid
    442         * @param length  the length within the array to be processed, must be valid
    443         * @return the result of the replace operation
    444         */
    445        public String replace(StrBuilder source, int offset, int length) {
    446            if (source == null) {
    447                return null;
    448            }
    449            StrBuilder buf = new StrBuilder(length).append(source, offset, length);
    450            substitute(buf, 0, length);
    451            return buf.toString();
    452        }
    453
    454        //-----------------------------------------------------------------------
    455        /**
    456         * Replaces all the occurrences of variables in the given source object with
    457         * their matching values from the resolver. The input source object is
    458         * converted to a string using <code>toString</code> and is not altered.
    459         *
    460         * @param source  the source to replace in, null returns null
    461         * @return the result of the replace operation
    462         */
    463        public String replace(Object source) {
    464            if (source == null) {
    465                return null;
    466            }
    467            StrBuilder buf = new StrBuilder().append(source);
    468            substitute(buf, 0, buf.length());
    469            return buf.toString();
    470        }
    471
    472        //-----------------------------------------------------------------------
    473        /**
    474         * Replaces all the occurrences of variables within the given source buffer
    475         * with their matching values from the resolver.
    476         * The buffer is updated with the result.
    477         *
    478         * @param source  the buffer to replace in, updated, null returns zero
    479         * @return true if altered
    480         */
    481        public boolean replaceIn(StringBuffer source) {
    482            if (source == null) {
    483                return false;
    484            }
    485            return replaceIn(source, 0, source.length());
    486        }
    487
    488        /**
    489         * Replaces all the occurrences of variables within the given source buffer
    490         * with their matching values from the resolver.
    491         * The buffer is updated with the result.
    492         * <p>
    493         * Only the specified portion of the buffer will be processed.
    494         * The rest of the buffer is not processed, but it is not deleted.
    495         *
    496         * @param source  the buffer to replace in, updated, null returns zero
    497         * @param offset  the start offset within the array, must be valid
    498         * @param length  the length within the buffer to be processed, must be valid
    499         * @return true if altered
    500         */
    501        public boolean replaceIn(StringBuffer source, int offset, int length) {
    502            if (source == null) {
    503                return false;
    504            }
    505            StrBuilder buf = new StrBuilder(length).append(source, offset, length);
    506            if (substitute(buf, 0, length) == false) {
    507                return false;
    508            }
    509            source.replace(offset, offset + length, buf.toString());
    510            return true;
    511        }
    512
    513        //-----------------------------------------------------------------------
    514        /**
    515         * Replaces all the occurrences of variables within the given source
    516         * builder with their matching values from the resolver.
    517         *
    518         * @param source  the builder to replace in, updated, null returns zero
    519         * @return true if altered
    520         */
    521        public boolean replaceIn(StrBuilder source) {
    522            if (source == null) {
    523                return false;
    524            }
    525            return substitute(source, 0, source.length());
    526        }
    527
    528        /**
    529         * Replaces all the occurrences of variables within the given source
    530         * builder with their matching values from the resolver.
    531         * <p>
    532         * Only the specified portion of the builder will be processed.
    533         * The rest of the builder is not processed, but it is not deleted.
    534         *
    535         * @param source  the builder to replace in, null returns zero
    536         * @param offset  the start offset within the array, must be valid
    537         * @param length  the length within the builder to be processed, must be valid
    538         * @return true if altered
    539         */
    540        public boolean replaceIn(StrBuilder source, int offset, int length) {
    541            if (source == null) {
    542                return false;
    543            }
    544            return substitute(source, offset, length);
    545        }
    546
    547        //-----------------------------------------------------------------------
    548        /**
    549         * Internal method that substitutes the variables.
    550         * <p>
    551         * Most users of this class do not need to call this method. This method will
    552         * be called automatically by another (public) method.
    553         * <p>
    554         * Writers of subclasses can override this method if they need access to
    555         * the substitution process at the start or end.
    556         *
    557         * @param buf  the string builder to substitute into, not null
    558         * @param offset  the start offset within the builder, must be valid
    559         * @param length  the length within the builder to be processed, must be valid
    560         * @return true if altered
    561         */
    562        protected boolean substitute(StrBuilder buf, int offset, int length) {
    563            return substitute(buf, offset, length, null) > 0;
    564        }
    565
    566        /**
    567         * Recursive handler for multiple levels of interpolation. This is the main
    568         * interpolation method, which resolves the values of all variable references
    569         * contained in the passed in text.
    570         *
    571         * @param buf  the string builder to substitute into, not null
    572         * @param offset  the start offset within the builder, must be valid
    573         * @param length  the length within the builder to be processed, must be valid
    574         * @param priorVariables  the stack keeping track of the replaced variables, may be null
    575         * @return the length change that occurs, unless priorVariables is null when the int
    576         *  represents a boolean flag as to whether any change occurred.
    577         */
    578        private int substitute(StrBuilder buf, int offset, int length, List<String> priorVariables) {
    579            StrMatcher prefixMatcher = getVariablePrefixMatcher();
    580            StrMatcher suffixMatcher = getVariableSuffixMatcher();
    581            char escape = getEscapeChar();
    582
    583            boolean top = priorVariables == null;
    584            boolean altered = false;
    585            int lengthChange = 0;
    586            char[] chars = buf.buffer;
    587            int bufEnd = offset + length;
    588            int pos = offset;
    589            while (pos < bufEnd) {
    590                int startMatchLen = prefixMatcher.isMatch(chars, pos, offset,
    591                        bufEnd);
    592                if (startMatchLen == 0) {
    593                    pos++;
    594                } else {
    595                    // found variable start marker
    596                    if (pos > offset && chars[pos - 1] == escape) {
    597                        // escaped
    598                        buf.deleteCharAt(pos - 1);
    599                        chars = buf.buffer; // in case buffer was altered
    600                        lengthChange--;
    601                        altered = true;
    602                        bufEnd--;
    603                    } else {
    604                        // find suffix
    605                        int startPos = pos;
    606                        pos += startMatchLen;
    607                        int endMatchLen = 0;
    608                        int nestedVarCount = 0;
    609                        while (pos < bufEnd) {
    610                            if (isEnableSubstitutionInVariables()
    611                                    && (endMatchLen = prefixMatcher.isMatch(chars,
    612                                            pos, offset, bufEnd)) != 0) {
    613                                // found a nested variable start
    614                                nestedVarCount++;
    615                                pos += endMatchLen;
    616                                continue;
    617                            }
    618
    619                            endMatchLen = suffixMatcher.isMatch(chars, pos, offset,
    620                                    bufEnd);
    621                            if (endMatchLen == 0) {
    622                                pos++;
    623                            } else {
    624                                // found variable end marker
    625                                if (nestedVarCount == 0) {
    626                                    String varName = new String(chars, startPos
    627                                            + startMatchLen, pos - startPos
    628                                            - startMatchLen);
    629                                    if (isEnableSubstitutionInVariables()) {
    630                                        StrBuilder bufName = new StrBuilder(varName);
    631                                        substitute(bufName, 0, bufName.length());
    632                                        varName = bufName.toString();
    633                                    }
    634                                    pos += endMatchLen;
    635                                    int endPos = pos;
    636
    637                                    // on the first call initialize priorVariables
    638                                    if (priorVariables == null) {
    639                                        priorVariables = new ArrayList<String>();
    640                                        priorVariables.add(new String(chars,
    641                                                offset, length));
    642                                    }
    643
    644                                    // handle cyclic substitution
    645                                    checkCyclicSubstitution(varName, priorVariables);
    646                                    priorVariables.add(varName);
    647
    648                                    // resolve the variable
    649                                    String varValue = resolveVariable(varName, buf,
    650                                            startPos, endPos);
    651                                    if (varValue != null) {
    652                                        // recursive replace
    653                                        int varLen = varValue.length();
    654                                        buf.replace(startPos, endPos, varValue);
    655                                        altered = true;
    656                                        int change = substitute(buf, startPos,
    657                                                varLen, priorVariables);
    658                                        change = change
    659                                                + varLen - (endPos - startPos);
    660                                        pos += change;
    661                                        bufEnd += change;
    662                                        lengthChange += change;
    663                                        chars = buf.buffer; // in case buffer was
    664                                                            // altered
    665                                    }
    666
    667                                    // remove variable from the cyclic stack
    668                                    priorVariables
    669                                            .remove(priorVariables.size() - 1);
    670                                    break;
    671                                } else {
    672                                    nestedVarCount--;
    673                                    pos += endMatchLen;
    674                                }
    675                            }
    676                        }
    677                    }
    678                }
    679            }
    680            if (top) {
    681                return altered ? 1 : 0;
    682            }
    683            return lengthChange;
    684        }
    685
    686        /**
    687         * Checks if the specified variable is already in the stack (list) of variables.
    688         *
    689         * @param varName  the variable name to check
    690         * @param priorVariables  the list of prior variables
    691         */
    692        private void checkCyclicSubstitution(String varName, List<String> priorVariables) {
    693            if (priorVariables.contains(varName) == false) {
    694                return;
    695            }
    696            StrBuilder buf = new StrBuilder(256);
    697            buf.append("Infinite loop in property interpolation of ");
    698            buf.append(priorVariables.remove(0));
    699            buf.append(": ");
    700            buf.appendWithSeparators(priorVariables, "->");
    701            throw new IllegalStateException(buf.toString());
    702        }
    703
    704        /**
    705         * Internal method that resolves the value of a variable.
    706         * <p>
    707         * Most users of this class do not need to call this method. This method is
    708         * called automatically by the substitution process.
    709         * <p>
    710         * Writers of subclasses can override this method if they need to alter
    711         * how each substitution occurs. The method is passed the variable's name
    712         * and must return the corresponding value. This implementation uses the
    713         * {@link #getVariableResolver()} with the variable's name as the key.
    714         *
    715         * @param variableName  the name of the variable, not null
    716         * @param buf  the buffer where the substitution is occurring, not null
    717         * @param startPos  the start position of the variable including the prefix, valid
    718         * @param endPos  the end position of the variable including the suffix, valid
    719         * @return the variable's value or <b>null</b> if the variable is unknown
    720         */
    721        protected String resolveVariable(String variableName, StrBuilder buf, int startPos, int endPos) {
    722            StrLookup<?> resolver = getVariableResolver();
    723            if (resolver == null) {
    724                return null;
    725            }
    726            return resolver.lookup(variableName);
    727        }
    728
    729        // Escape
    730        //-----------------------------------------------------------------------
    731        /**
    732         * Returns the escape character.
    733         *
    734         * @return the character used for escaping variable references
    735         */
    736        public char getEscapeChar() {
    737            return this.escapeChar;
    738        }
    739
    740        /**
    741         * Sets the escape character.
    742         * If this character is placed before a variable reference in the source
    743         * text, this variable will be ignored.
    744         *
    745         * @param escapeCharacter  the escape character (0 for disabling escaping)
    746         */
    747        public void setEscapeChar(char escapeCharacter) {
    748            this.escapeChar = escapeCharacter;
    749        }
    750
    751        // Prefix
    752        //-----------------------------------------------------------------------
    753        /**
    754         * Gets the variable prefix matcher currently in use.
    755         * <p>
    756         * The variable prefix is the characer or characters that identify the
    757         * start of a variable. This prefix is expressed in terms of a matcher
    758         * allowing advanced prefix matches.
    759         *
    760         * @return the prefix matcher in use
    761         */
    762        public StrMatcher getVariablePrefixMatcher() {
    763            return prefixMatcher;
    764        }
    765
    766        /**
    767         * Sets the variable prefix matcher currently in use.
    768         * <p>
    769         * The variable prefix is the characer or characters that identify the
    770         * start of a variable. This prefix is expressed in terms of a matcher
    771         * allowing advanced prefix matches.
    772         *
    773         * @param prefixMatcher  the prefix matcher to use, null ignored
    774         * @return this, to enable chaining
    775         * @throws IllegalArgumentException if the prefix matcher is null
    776         */
    777        public StrSubstitutor setVariablePrefixMatcher(StrMatcher prefixMatcher) {
    778            if (prefixMatcher == null) {
    779                throw new IllegalArgumentException("Variable prefix matcher must not be null!");
    780            }
    781            this.prefixMatcher = prefixMatcher;
    782            return this;
    783        }
    784
    785        /**
    786         * Sets the variable prefix to use.
    787         * <p>
    788         * The variable prefix is the character or characters that identify the
    789         * start of a variable. This method allows a single character prefix to
    790         * be easily set.
    791         *
    792         * @param prefix  the prefix character to use
    793         * @return this, to enable chaining
    794         */
    795        public StrSubstitutor setVariablePrefix(char prefix) {
    796            return setVariablePrefixMatcher(StrMatcher.charMatcher(prefix));
    797        }
    798
    799        /**
    800         * Sets the variable prefix to use.
    801         * <p>
    802         * The variable prefix is the characer or characters that identify the
    803         * start of a variable. This method allows a string prefix to be easily set.
    804         *
    805         * @param prefix  the prefix for variables, not null
    806         * @return this, to enable chaining
    807         * @throws IllegalArgumentException if the prefix is null
    808         */
    809        public StrSubstitutor setVariablePrefix(String prefix) {
    810           if (prefix == null) {
    811                throw new IllegalArgumentException("Variable prefix must not be null!");
    812            }
    813            return setVariablePrefixMatcher(StrMatcher.stringMatcher(prefix));
    814        }
    815
    816        // Suffix
    817        //-----------------------------------------------------------------------
    818        /**
    819         * Gets the variable suffix matcher currently in use.
    820         * <p>
    821         * The variable suffix is the characer or characters that identify the
    822         * end of a variable. This suffix is expressed in terms of a matcher
    823         * allowing advanced suffix matches.
    824         *
    825         * @return the suffix matcher in use
    826         */
    827        public StrMatcher getVariableSuffixMatcher() {
    828            return suffixMatcher;
    829        }
    830
    831        /**
    832         * Sets the variable suffix matcher currently in use.
    833         * <p>
    834         * The variable suffix is the characer or characters that identify the
    835         * end of a variable. This suffix is expressed in terms of a matcher
    836         * allowing advanced suffix matches.
    837         *
    838         * @param suffixMatcher  the suffix matcher to use, null ignored
    839         * @return this, to enable chaining
    840         * @throws IllegalArgumentException if the suffix matcher is null
    841         */
    842        public StrSubstitutor setVariableSuffixMatcher(StrMatcher suffixMatcher) {
    843            if (suffixMatcher == null) {
    844                throw new IllegalArgumentException("Variable suffix matcher must not be null!");
    845            }
    846            this.suffixMatcher = suffixMatcher;
    847            return this;
    848        }
    849
    850        /**
    851         * Sets the variable suffix to use.
    852         * <p>
    853         * The variable suffix is the characer or characters that identify the
    854         * end of a variable. This method allows a single character suffix to
    855         * be easily set.
    856         *
    857         * @param suffix  the suffix character to use
    858         * @return this, to enable chaining
    859         */
    860        public StrSubstitutor setVariableSuffix(char suffix) {
    861            return setVariableSuffixMatcher(StrMatcher.charMatcher(suffix));
    862        }
    863
    864        /**
    865         * Sets the variable suffix to use.
    866         * <p>
    867         * The variable suffix is the character or characters that identify the
    868         * end of a variable. This method allows a string suffix to be easily set.
    869         *
    870         * @param suffix  the suffix for variables, not null
    871         * @return this, to enable chaining
    872         * @throws IllegalArgumentException if the suffix is null
    873         */
    874        public StrSubstitutor setVariableSuffix(String suffix) {
    875           if (suffix == null) {
    876                throw new IllegalArgumentException("Variable suffix must not be null!");
    877            }
    878            return setVariableSuffixMatcher(StrMatcher.stringMatcher(suffix));
    879        }
    880
    881        // Resolver
    882        //-----------------------------------------------------------------------
    883        /**
    884         * Gets the VariableResolver that is used to lookup variables.
    885         *
    886         * @return the VariableResolver
    887         */
    888        public StrLookup<?> getVariableResolver() {
    889            return this.variableResolver;
    890        }
    891
    892        /**
    893         * Sets the VariableResolver that is used to lookup variables.
    894         *
    895         * @param variableResolver  the VariableResolver
    896         */
    897        public void setVariableResolver(StrLookup<?> variableResolver) {
    898            this.variableResolver = variableResolver;
    899        }
    900
    901        // Substitution support in variable names
    902        //-----------------------------------------------------------------------
    903        /**
    904         * Returns a flag whether substitution is done in variable names.
    905         *
    906         * @return the substitution in variable names flag
    907         * @since 3.0
    908         */
    909        public boolean isEnableSubstitutionInVariables() {
    910            return enableSubstitutionInVariables;
    911        }
    912
    913        /**
    914         * Sets a flag whether substitution is done in variable names. If set to
    915         * <b>true</b>, the names of variables can contain other variables which are
    916         * processed first before the original variable is evaluated, e.g.
    917         * <code>${jre-${java.version}}</code>. The default value is <b>false</b>.
    918         *
    919         * @param enableSubstitutionInVariables the new value of the flag
    920         * @since 3.0
    921         */
    922        public void setEnableSubstitutionInVariables(
    923                boolean enableSubstitutionInVariables) {
    924            this.enableSubstitutionInVariables = enableSubstitutionInVariables;
    925        }
    926    }