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