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