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