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