View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      https://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.lang3.text;
18  
19  import java.util.ArrayList;
20  import java.util.Enumeration;
21  import java.util.HashMap;
22  import java.util.List;
23  import java.util.Map;
24  import java.util.Objects;
25  import java.util.Properties;
26  
27  import org.apache.commons.lang3.StringUtils;
28  
29  /**
30   * Substitutes variables within a string by values.
31   * <p>
32   * This class takes a piece of text and substitutes all the variables within it.
33   * The default definition of a variable is {@code ${variableName}}.
34   * The prefix and suffix can be changed via constructors and set methods.
35   * </p>
36   * <p>
37   * Variable values are typically resolved from a map, but could also be resolved
38   * from system properties, or by supplying a custom variable resolver.
39   * </p>
40   * <p>
41   * The simplest example is to use this class to replace Java System properties. For example:
42   * </p>
43   * <pre>
44   * StrSubstitutor.replaceSystemProperties(
45   *      "You are running with java.version = ${java.version} and os.name = ${os.name}.");
46   * </pre>
47   * <p>
48   * Typical usage of this class follows the following pattern: First an instance is created
49   * and initialized with the map that contains the values for the available variables.
50   * If a prefix and/or suffix for variables should be used other than the default ones,
51   * the appropriate settings can be performed. After that the {@code replace()}
52   * method can be called passing in the source text for interpolation. In the returned
53   * text all variable references (as long as their values are known) will be resolved.
54   * The following example demonstrates this:
55   * </p>
56   * <pre>
57   * Map valuesMap = HashMap();
58   * valuesMap.put(&quot;animal&quot;, &quot;quick brown fox&quot;);
59   * valuesMap.put(&quot;target&quot;, &quot;lazy dog&quot;);
60   * String templateString = &quot;The ${animal} jumps over the ${target}.&quot;;
61   * StrSubstitutor sub = new StrSubstitutor(valuesMap);
62   * String resolvedString = sub.replace(templateString);
63   * </pre>
64   * yielding:
65   * <pre>
66   *      The quick brown fox jumps over the lazy dog.
67   * </pre>
68   * <p>
69   * Also, this class allows to set a default value for unresolved variables.
70   * The default value for a variable can be appended to the variable name after the variable
71   * default value delimiter. The default value of the variable default value delimiter is ':-',
72   * as in bash and other *nix shells, as those are arguably where the default ${} delimiter set originated.
73   * The variable default value delimiter can be manually set by calling {@link #setValueDelimiterMatcher(StrMatcher)},
74   * {@link #setValueDelimiter(char)} or {@link #setValueDelimiter(String)}.
75   * The following shows an example with variable default value settings:
76   * </p>
77   * <pre>
78   * Map valuesMap = HashMap();
79   * valuesMap.put(&quot;animal&quot;, &quot;quick brown fox&quot;);
80   * valuesMap.put(&quot;target&quot;, &quot;lazy dog&quot;);
81   * String templateString = &quot;The ${animal} jumps over the ${target}. ${undefined.number:-1234567890}.&quot;;
82   * StrSubstitutor sub = new StrSubstitutor(valuesMap);
83   * String resolvedString = sub.replace(templateString);
84   * </pre>
85   * <p>
86   * yielding:
87   * </p>
88   * <pre>
89   *      The quick brown fox jumps over the lazy dog. 1234567890.
90   * </pre>
91   * <p>
92   * In addition to this usage pattern there are some static convenience methods that
93   * cover the most common use cases. These methods can be used without the need of
94   * manually creating an instance. However if multiple replace operations are to be
95   * performed, creating and reusing an instance of this class will be more efficient.
96   * </p>
97   * <p>
98   * Variable replacement works in a recursive way. Thus, if a variable value contains
99   * 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
149 public 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 }