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