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