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