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  /**
27   * Substitutes variables within a string by values.
28   * <p>
29   * This class takes a piece of text and substitutes all the variables within it.
30   * The default definition of a variable is <code>${variableName}</code>.
31   * The prefix and suffix can be changed via constructors and set methods.
32   * <p>
33   * Variable values are typically resolved from a map, but could also be resolved
34   * from system properties, or by supplying a custom variable resolver.
35   * <p>
36   * The simplest example is to use this class to replace Java System properties. For example:
37   * <pre>
38   * StrSubstitutor.replaceSystemProperties(
39   *      "You are running with java.version = ${java.version} and os.name = ${os.name}.");
40   * </pre>
41   * <p>
42   * Typical usage of this class follows the following pattern: First an instance is created
43   * and initialized with the map that contains the values for the available variables.
44   * If a prefix and/or suffix for variables should be used other than the default ones,
45   * the appropriate settings can be performed. After that the <code>replace()</code>
46   * method can be called passing in the source text for interpolation. In the returned
47   * text all variable references (as long as their values are known) will be resolved.
48   * The following example demonstrates this:
49   * <pre>
50   * Map valuesMap = HashMap();
51   * valuesMap.put(&quot;animal&quot;, &quot;quick brown fox&quot;);
52   * valuesMap.put(&quot;target&quot;, &quot;lazy dog&quot;);
53   * String templateString = &quot;The ${animal} jumped over the ${target}.&quot;;
54   * StrSubstitutor sub = new StrSubstitutor(valuesMap);
55   * String resolvedString = sub.replace(templateString);
56   * </pre>
57   * yielding:
58   * <pre>
59   *      The quick brown fox jumped over the lazy dog.
60   * </pre>
61   * <p>
62   * In addition to this usage pattern there are some static convenience methods that
63   * cover the most common use cases. These methods can be used without the need of
64   * manually creating an instance. However if multiple replace operations are to be
65   * performed, creating and reusing an instance of this class will be more efficient.
66   * <p>
67   * Variable replacement works in a recursive way. Thus, if a variable value contains
68   * a variable then that variable will also be replaced. Cyclic replacements are
69   * detected and will cause an exception to be thrown.
70   * <p>
71   * Sometimes the interpolation's result must contain a variable prefix. As an example
72   * take the following source text:
73   * <pre>
74   *   The variable ${${name}} must be used.
75   * </pre>
76   * Here only the variable's name referred to in the text should be replaced resulting
77   * in the text (assuming that the value of the <code>name</code> variable is <code>x</code>):
78   * <pre>
79   *   The variable ${x} must be used.
80   * </pre>
81   * To achieve this effect there are two possibilities: Either set a different prefix
82   * and suffix for variables which do not conflict with the result text you want to
83   * produce. The other possibility is to use the escape character, by default '$'.
84   * If this character is placed before a variable reference, this reference is ignored
85   * and won't be replaced. For example:
86   * <pre>
87   *   The variable $${${name}} must be used.
88   * </pre>
89   * <p>
90   * In some complex scenarios you might even want to perform substitution in the
91   * names of variables, for instance
92   * <pre>
93   * ${jre-${java.specification.version}}
94   * </pre>
95   * <code>StrSubstitutor</code> supports this recursive substitution in variable
96   * names, but it has to be enabled explicitly by setting the
97   * {@link #setEnableSubstitutionInVariables(boolean) enableSubstitutionInVariables}
98   * property to <b>true</b>.
99   *
100  * @version $Id: StrSubstitutor.java 1436770 2013-01-22 07:09:45Z ggregory $
101  * @since 2.2
102  */
103 public class StrSubstitutor {
104 
105     /**
106      * Constant for the default escape character.
107      */
108     public static final char DEFAULT_ESCAPE = '$';
109     /**
110      * Constant for the default variable prefix.
111      */
112     public static final StrMatcher DEFAULT_PREFIX = StrMatcher.stringMatcher("${");
113     /**
114      * Constant for the default variable suffix.
115      */
116     public static final StrMatcher DEFAULT_SUFFIX = StrMatcher.stringMatcher("}");
117 
118     /**
119      * Stores the escape character.
120      */
121     private char escapeChar;
122     /**
123      * Stores the variable prefix.
124      */
125     private StrMatcher prefixMatcher;
126     /**
127      * Stores the variable suffix.
128      */
129     private StrMatcher suffixMatcher;
130     /**
131      * Variable resolution is delegated to an implementor of VariableResolver.
132      */
133     private StrLookup<?> variableResolver;
134     /**
135      * The flag whether substitution in variable names is enabled.
136      */
137     private boolean enableSubstitutionInVariables;
138 
139     //-----------------------------------------------------------------------
140     /**
141      * Replaces all the occurrences of variables in the given source object with
142      * their matching values from the map.
143      *
144      * @param <V> the type of the values in the map
145      * @param source  the source text containing the variables to substitute, null returns null
146      * @param valueMap  the map with the values, may be null
147      * @return the result of the replace operation
148      */
149     public static <V> String replace(final Object source, final Map<String, V> valueMap) {
150         return new StrSubstitutor(valueMap).replace(source);
151     }
152 
153     /**
154      * Replaces all the occurrences of variables in the given source object with
155      * their matching values from the map. This method allows to specifiy a
156      * custom variable prefix and suffix
157      *
158      * @param <V> the type of the values in the map
159      * @param source  the source text containing the variables to substitute, null returns null
160      * @param valueMap  the map with the values, may be null
161      * @param prefix  the prefix of variables, not null
162      * @param suffix  the suffix of variables, not null
163      * @return the result of the replace operation
164      * @throws IllegalArgumentException if the prefix or suffix is null
165      */
166     public static <V> String replace(final Object source, final Map<String, V> valueMap, final String prefix, final String suffix) {
167         return new StrSubstitutor(valueMap, prefix, suffix).replace(source);
168     }
169 
170     /**
171      * Replaces all the occurrences of variables in the given source object with their matching
172      * values from the properties.
173      *
174      * @param source the source text containing the variables to substitute, null returns null
175      * @param valueProperties the properties with values, may be null
176      * @return the result of the replace operation
177      */
178     public static String replace(final Object source, final Properties valueProperties) {
179         if (valueProperties == null) {
180             return source.toString();
181         }
182         final Map<String,String> valueMap = new HashMap<String,String>();
183         final Enumeration<?> propNames = valueProperties.propertyNames();
184         while (propNames.hasMoreElements()) {
185             final String propName = (String)propNames.nextElement();
186             final String propValue = valueProperties.getProperty(propName);
187             valueMap.put(propName, propValue);
188         }
189         return StrSubstitutor.replace(source, valueMap);
190     }
191 
192     /**
193      * Replaces all the occurrences of variables in the given source object with
194      * their matching values from the system properties.
195      *
196      * @param source  the source text containing the variables to substitute, null returns null
197      * @return the result of the replace operation
198      */
199     public static String replaceSystemProperties(final Object source) {
200         return new StrSubstitutor(StrLookup.systemPropertiesLookup()).replace(source);
201     }
202 
203     //-----------------------------------------------------------------------
204     /**
205      * Creates a new instance with defaults for variable prefix and suffix
206      * and the escaping character.
207      */
208     public StrSubstitutor() {
209         this((StrLookup<?>) null, DEFAULT_PREFIX, DEFAULT_SUFFIX, DEFAULT_ESCAPE);
210     }
211 
212     /**
213      * Creates a new instance and initializes it. Uses defaults for variable
214      * prefix and suffix and the escaping character.
215      *
216      * @param <V> the type of the values in the map
217      * @param valueMap  the map with the variables' values, may be null
218      */
219     public <V> StrSubstitutor(final Map<String, V> valueMap) {
220         this(StrLookup.mapLookup(valueMap), DEFAULT_PREFIX, DEFAULT_SUFFIX, DEFAULT_ESCAPE);
221     }
222 
223     /**
224      * Creates a new instance and initializes it. Uses a default escaping character.
225      *
226      * @param <V> the type of the values in the map
227      * @param valueMap  the map with the variables' values, may be null
228      * @param prefix  the prefix for variables, not null
229      * @param suffix  the suffix for variables, not null
230      * @throws IllegalArgumentException if the prefix or suffix is null
231      */
232     public <V> StrSubstitutor(final Map<String, V> valueMap, final String prefix, final String suffix) {
233         this(StrLookup.mapLookup(valueMap), prefix, suffix, DEFAULT_ESCAPE);
234     }
235 
236     /**
237      * Creates a new instance and initializes it.
238      *
239      * @param <V> the type of the values in the map
240      * @param valueMap  the map with the variables' values, may be null
241      * @param prefix  the prefix for variables, not null
242      * @param suffix  the suffix for variables, not null
243      * @param escape  the escape character
244      * @throws IllegalArgumentException if the prefix or suffix is null
245      */
246     public <V> StrSubstitutor(final Map<String, V> valueMap, final String prefix, final String suffix, final char escape) {
247         this(StrLookup.mapLookup(valueMap), prefix, suffix, escape);
248     }
249 
250     /**
251      * Creates a new instance and initializes it.
252      *
253      * @param variableResolver  the variable resolver, may be null
254      */
255     public StrSubstitutor(final StrLookup<?> variableResolver) {
256         this(variableResolver, DEFAULT_PREFIX, DEFAULT_SUFFIX, DEFAULT_ESCAPE);
257     }
258 
259     /**
260      * Creates a new instance and initializes it.
261      *
262      * @param variableResolver  the variable resolver, may be null
263      * @param prefix  the prefix for variables, not null
264      * @param suffix  the suffix for variables, not null
265      * @param escape  the escape character
266      * @throws IllegalArgumentException if the prefix or suffix is null
267      */
268     public StrSubstitutor(final StrLookup<?> variableResolver, final String prefix, final String suffix, final char escape) {
269         this.setVariableResolver(variableResolver);
270         this.setVariablePrefix(prefix);
271         this.setVariableSuffix(suffix);
272         this.setEscapeChar(escape);
273     }
274 
275     /**
276      * Creates a new instance and initializes it.
277      *
278      * @param variableResolver  the variable resolver, may be null
279      * @param prefixMatcher  the prefix for variables, not null
280      * @param suffixMatcher  the suffix for variables, not null
281      * @param escape  the escape character
282      * @throws IllegalArgumentException if the prefix or suffix is null
283      */
284     public StrSubstitutor(
285             final StrLookup<?> variableResolver, final StrMatcher prefixMatcher, final StrMatcher suffixMatcher, final char escape) {
286         this.setVariableResolver(variableResolver);
287         this.setVariablePrefixMatcher(prefixMatcher);
288         this.setVariableSuffixMatcher(suffixMatcher);
289         this.setEscapeChar(escape);
290     }
291 
292     //-----------------------------------------------------------------------
293     /**
294      * Replaces all the occurrences of variables with their matching values
295      * from the resolver using the given source string as a template.
296      *
297      * @param source  the string to replace in, null returns null
298      * @return the result of the replace operation
299      */
300     public String replace(final String source) {
301         if (source == null) {
302             return null;
303         }
304         final StrBuilder buf = new StrBuilder(source);
305         if (substitute(buf, 0, source.length()) == false) {
306             return source;
307         }
308         return buf.toString();
309     }
310 
311     /**
312      * Replaces all the occurrences of variables with their matching values
313      * from the resolver using the given source string as a template.
314      * <p>
315      * Only the specified portion of the string will be processed.
316      * The rest of the string is not processed, and is not returned.
317      *
318      * @param source  the string to replace in, null returns null
319      * @param offset  the start offset within the array, must be valid
320      * @param length  the length within the array to be processed, must be valid
321      * @return the result of the replace operation
322      */
323     public String replace(final String source, final int offset, final int length) {
324         if (source == null) {
325             return null;
326         }
327         final StrBuilder buf = new StrBuilder(length).append(source, offset, length);
328         if (substitute(buf, 0, length) == false) {
329             return source.substring(offset, offset + length);
330         }
331         return buf.toString();
332     }
333 
334     //-----------------------------------------------------------------------
335     /**
336      * Replaces all the occurrences of variables with their matching values
337      * from the resolver using the given source array as a template.
338      * The array is not altered by this method.
339      *
340      * @param source  the character array to replace in, not altered, null returns null
341      * @return the result of the replace operation
342      */
343     public String replace(final char[] source) {
344         if (source == null) {
345             return null;
346         }
347         final StrBuilder buf = new StrBuilder(source.length).append(source);
348         substitute(buf, 0, source.length);
349         return buf.toString();
350     }
351 
352     /**
353      * Replaces all the occurrences of variables with their matching values
354      * from the resolver using the given source array as a template.
355      * The array is not altered by this method.
356      * <p>
357      * Only the specified portion of the array will be processed.
358      * The rest of the array is not processed, and is not returned.
359      *
360      * @param source  the character array to replace in, not altered, null returns null
361      * @param offset  the start offset within the array, must be valid
362      * @param length  the length within the array to be processed, must be valid
363      * @return the result of the replace operation
364      */
365     public String replace(final char[] source, final int offset, final int length) {
366         if (source == null) {
367             return null;
368         }
369         final StrBuilder buf = new StrBuilder(length).append(source, offset, length);
370         substitute(buf, 0, length);
371         return buf.toString();
372     }
373 
374     //-----------------------------------------------------------------------
375     /**
376      * Replaces all the occurrences of variables with their matching values
377      * from the resolver using the given source buffer as a template.
378      * The buffer is not altered by this method.
379      *
380      * @param source  the buffer to use as a template, not changed, null returns null
381      * @return the result of the replace operation
382      */
383     public String replace(final StringBuffer source) {
384         if (source == null) {
385             return null;
386         }
387         final StrBuilder buf = new StrBuilder(source.length()).append(source);
388         substitute(buf, 0, buf.length());
389         return buf.toString();
390     }
391 
392     /**
393      * Replaces all the occurrences of variables with their matching values
394      * from the resolver using the given source buffer as a template.
395      * The buffer is not altered by this method.
396      * <p>
397      * Only the specified portion of the buffer will be processed.
398      * The rest of the buffer is not processed, and is not returned.
399      *
400      * @param source  the buffer to use as a template, not changed, null returns null
401      * @param offset  the start offset within the array, must be valid
402      * @param length  the length within the array to be processed, must be valid
403      * @return the result of the replace operation
404      */
405     public String replace(final StringBuffer source, final int offset, final int length) {
406         if (source == null) {
407             return null;
408         }
409         final StrBuilder buf = new StrBuilder(length).append(source, offset, length);
410         substitute(buf, 0, length);
411         return buf.toString();
412     }
413 
414     //-----------------------------------------------------------------------
415     /**
416      * Replaces all the occurrences of variables with their matching values
417      * from the resolver using the given source builder as a template.
418      * The builder is not altered by this method.
419      *
420      * @param source  the builder to use as a template, not changed, null returns null
421      * @return the result of the replace operation
422      */
423     public String replace(final StrBuilder source) {
424         if (source == null) {
425             return null;
426         }
427         final StrBuilder buf = new StrBuilder(source.length()).append(source);
428         substitute(buf, 0, buf.length());
429         return buf.toString();
430     }
431 
432     /**
433      * Replaces all the occurrences of variables with their matching values
434      * from the resolver using the given source builder as a template.
435      * The builder is not altered by this method.
436      * <p>
437      * Only the specified portion of the builder will be processed.
438      * The rest of the builder is not processed, and is not returned.
439      *
440      * @param source  the builder to use as a template, not changed, null returns null
441      * @param offset  the start offset within the array, must be valid
442      * @param length  the length within the array to be processed, must be valid
443      * @return the result of the replace operation
444      */
445     public String replace(final StrBuilder source, final int offset, final int length) {
446         if (source == null) {
447             return null;
448         }
449         final StrBuilder buf = new StrBuilder(length).append(source, offset, length);
450         substitute(buf, 0, length);
451         return buf.toString();
452     }
453 
454     //-----------------------------------------------------------------------
455     /**
456      * Replaces all the occurrences of variables in the given source object with
457      * their matching values from the resolver. The input source object is
458      * converted to a string using <code>toString</code> and is not altered.
459      *
460      * @param source  the source to replace in, null returns null
461      * @return the result of the replace operation
462      */
463     public String replace(final Object source) {
464         if (source == null) {
465             return null;
466         }
467         final StrBuilder buf = new StrBuilder().append(source);
468         substitute(buf, 0, buf.length());
469         return buf.toString();
470     }
471 
472     //-----------------------------------------------------------------------
473     /**
474      * Replaces all the occurrences of variables within the given source buffer
475      * with their matching values from the resolver.
476      * The buffer is updated with the result.
477      *
478      * @param source  the buffer to replace in, updated, null returns zero
479      * @return true if altered
480      */
481     public boolean replaceIn(final StringBuffer source) {
482         if (source == null) {
483             return false;
484         }
485         return replaceIn(source, 0, source.length());
486     }
487 
488     /**
489      * Replaces all the occurrences of variables within the given source buffer
490      * with their matching values from the resolver.
491      * The buffer is updated with the result.
492      * <p>
493      * Only the specified portion of the buffer will be processed.
494      * The rest of the buffer is not processed, but it is not deleted.
495      *
496      * @param source  the buffer to replace in, updated, null returns zero
497      * @param offset  the start offset within the array, must be valid
498      * @param length  the length within the buffer to be processed, must be valid
499      * @return true if altered
500      */
501     public boolean replaceIn(final StringBuffer source, final int offset, final int length) {
502         if (source == null) {
503             return false;
504         }
505         final StrBuilder buf = new StrBuilder(length).append(source, offset, length);
506         if (substitute(buf, 0, length) == false) {
507             return false;
508         }
509         source.replace(offset, offset + length, buf.toString());
510         return true;
511     }
512 
513     //-----------------------------------------------------------------------
514     /**
515      * Replaces all the occurrences of variables within the given source
516      * builder with their matching values from the resolver.
517      *
518      * @param source  the builder to replace in, updated, null returns zero
519      * @return true if altered
520      */
521     public boolean replaceIn(final StrBuilder source) {
522         if (source == null) {
523             return false;
524         }
525         return substitute(source, 0, source.length());
526     }
527 
528     /**
529      * Replaces all the occurrences of variables within the given source
530      * builder with their matching values from the resolver.
531      * <p>
532      * Only the specified portion of the builder will be processed.
533      * The rest of the builder is not processed, but it is not deleted.
534      *
535      * @param source  the builder to replace in, null returns zero
536      * @param offset  the start offset within the array, must be valid
537      * @param length  the length within the builder to be processed, must be valid
538      * @return true if altered
539      */
540     public boolean replaceIn(final StrBuilder source, final int offset, final int length) {
541         if (source == null) {
542             return false;
543         }
544         return substitute(source, offset, length);
545     }
546 
547     //-----------------------------------------------------------------------
548     /**
549      * Internal method that substitutes the variables.
550      * <p>
551      * Most users of this class do not need to call this method. This method will
552      * be called automatically by another (public) method.
553      * <p>
554      * Writers of subclasses can override this method if they need access to
555      * the substitution process at the start or end.
556      *
557      * @param buf  the string builder to substitute into, not null
558      * @param offset  the start offset within the builder, must be valid
559      * @param length  the length within the builder to be processed, must be valid
560      * @return true if altered
561      */
562     protected boolean substitute(final StrBuilder buf, final int offset, final int length) {
563         return substitute(buf, offset, length, null) > 0;
564     }
565 
566     /**
567      * Recursive handler for multiple levels of interpolation. This is the main
568      * interpolation method, which resolves the values of all variable references
569      * contained in the passed in text.
570      *
571      * @param buf  the string builder to substitute into, not null
572      * @param offset  the start offset within the builder, must be valid
573      * @param length  the length within the builder to be processed, must be valid
574      * @param priorVariables  the stack keeping track of the replaced variables, may be null
575      * @return the length change that occurs, unless priorVariables is null when the int
576      *  represents a boolean flag as to whether any change occurred.
577      */
578     private int substitute(final StrBuilder buf, final int offset, final int length, List<String> priorVariables) {
579         final StrMatcher prefixMatcher = getVariablePrefixMatcher();
580         final StrMatcher suffixMatcher = getVariableSuffixMatcher();
581         final char escape = getEscapeChar();
582 
583         final boolean top = priorVariables == null;
584         boolean altered = false;
585         int lengthChange = 0;
586         char[] chars = buf.buffer;
587         int bufEnd = offset + length;
588         int pos = offset;
589         while (pos < bufEnd) {
590             final int startMatchLen = prefixMatcher.isMatch(chars, pos, offset,
591                     bufEnd);
592             if (startMatchLen == 0) {
593                 pos++;
594             } else {
595                 // found variable start marker
596                 if (pos > offset && chars[pos - 1] == escape) {
597                     // escaped
598                     buf.deleteCharAt(pos - 1);
599                     chars = buf.buffer; // in case buffer was altered
600                     lengthChange--;
601                     altered = true;
602                     bufEnd--;
603                 } else {
604                     // find suffix
605                     final int startPos = pos;
606                     pos += startMatchLen;
607                     int endMatchLen = 0;
608                     int nestedVarCount = 0;
609                     while (pos < bufEnd) {
610                         if (isEnableSubstitutionInVariables()
611                                 && (endMatchLen = prefixMatcher.isMatch(chars,
612                                         pos, offset, bufEnd)) != 0) {
613                             // found a nested variable start
614                             nestedVarCount++;
615                             pos += endMatchLen;
616                             continue;
617                         }
618 
619                         endMatchLen = suffixMatcher.isMatch(chars, pos, offset,
620                                 bufEnd);
621                         if (endMatchLen == 0) {
622                             pos++;
623                         } else {
624                             // found variable end marker
625                             if (nestedVarCount == 0) {
626                                 String varName = new String(chars, startPos
627                                         + startMatchLen, pos - startPos
628                                         - startMatchLen);
629                                 if (isEnableSubstitutionInVariables()) {
630                                     final StrBuilder bufName = new StrBuilder(varName);
631                                     substitute(bufName, 0, bufName.length());
632                                     varName = bufName.toString();
633                                 }
634                                 pos += endMatchLen;
635                                 final int endPos = pos;
636 
637                                 // on the first call initialize priorVariables
638                                 if (priorVariables == null) {
639                                     priorVariables = new ArrayList<String>();
640                                     priorVariables.add(new String(chars,
641                                             offset, length));
642                                 }
643 
644                                 // handle cyclic substitution
645                                 checkCyclicSubstitution(varName, priorVariables);
646                                 priorVariables.add(varName);
647 
648                                 // resolve the variable
649                                 final String varValue = resolveVariable(varName, buf,
650                                         startPos, endPos);
651                                 if (varValue != null) {
652                                     // recursive replace
653                                     final int varLen = varValue.length();
654                                     buf.replace(startPos, endPos, varValue);
655                                     altered = true;
656                                     int change = substitute(buf, startPos,
657                                             varLen, priorVariables);
658                                     change = change
659                                             + varLen - (endPos - startPos);
660                                     pos += change;
661                                     bufEnd += change;
662                                     lengthChange += change;
663                                     chars = buf.buffer; // in case buffer was
664                                                         // altered
665                                 }
666 
667                                 // remove variable from the cyclic stack
668                                 priorVariables
669                                         .remove(priorVariables.size() - 1);
670                                 break;
671                             } else {
672                                 nestedVarCount--;
673                                 pos += endMatchLen;
674                             }
675                         }
676                     }
677                 }
678             }
679         }
680         if (top) {
681             return altered ? 1 : 0;
682         }
683         return lengthChange;
684     }
685 
686     /**
687      * Checks if the specified variable is already in the stack (list) of variables.
688      *
689      * @param varName  the variable name to check
690      * @param priorVariables  the list of prior variables
691      */
692     private void checkCyclicSubstitution(final String varName, final List<String> priorVariables) {
693         if (priorVariables.contains(varName) == false) {
694             return;
695         }
696         final StrBuilder buf = new StrBuilder(256);
697         buf.append("Infinite loop in property interpolation of ");
698         buf.append(priorVariables.remove(0));
699         buf.append(": ");
700         buf.appendWithSeparators(priorVariables, "->");
701         throw new IllegalStateException(buf.toString());
702     }
703 
704     /**
705      * Internal method that resolves the value of a variable.
706      * <p>
707      * Most users of this class do not need to call this method. This method is
708      * called automatically by the substitution process.
709      * <p>
710      * Writers of subclasses can override this method if they need to alter
711      * how each substitution occurs. The method is passed the variable's name
712      * and must return the corresponding value. This implementation uses the
713      * {@link #getVariableResolver()} with the variable's name as the key.
714      *
715      * @param variableName  the name of the variable, not null
716      * @param buf  the buffer where the substitution is occurring, not null
717      * @param startPos  the start position of the variable including the prefix, valid
718      * @param endPos  the end position of the variable including the suffix, valid
719      * @return the variable's value or <b>null</b> if the variable is unknown
720      */
721     protected String resolveVariable(final String variableName, final StrBuilder buf, final int startPos, final int endPos) {
722         final StrLookup<?> resolver = getVariableResolver();
723         if (resolver == null) {
724             return null;
725         }
726         return resolver.lookup(variableName);
727     }
728 
729     // Escape
730     //-----------------------------------------------------------------------
731     /**
732      * Returns the escape character.
733      *
734      * @return the character used for escaping variable references
735      */
736     public char getEscapeChar() {
737         return this.escapeChar;
738     }
739 
740     /**
741      * Sets the escape character.
742      * If this character is placed before a variable reference in the source
743      * text, this variable will be ignored.
744      *
745      * @param escapeCharacter  the escape character (0 for disabling escaping)
746      */
747     public void setEscapeChar(final char escapeCharacter) {
748         this.escapeChar = escapeCharacter;
749     }
750 
751     // Prefix
752     //-----------------------------------------------------------------------
753     /**
754      * Gets the variable prefix matcher currently in use.
755      * <p>
756      * The variable prefix is the characer or characters that identify the
757      * start of a variable. This prefix is expressed in terms of a matcher
758      * allowing advanced prefix matches.
759      *
760      * @return the prefix matcher in use
761      */
762     public StrMatcher getVariablePrefixMatcher() {
763         return prefixMatcher;
764     }
765 
766     /**
767      * Sets the variable prefix matcher currently in use.
768      * <p>
769      * The variable prefix is the characer or characters that identify the
770      * start of a variable. This prefix is expressed in terms of a matcher
771      * allowing advanced prefix matches.
772      *
773      * @param prefixMatcher  the prefix matcher to use, null ignored
774      * @return this, to enable chaining
775      * @throws IllegalArgumentException if the prefix matcher is null
776      */
777     public StrSubstitutor setVariablePrefixMatcher(final StrMatcher prefixMatcher) {
778         if (prefixMatcher == null) {
779             throw new IllegalArgumentException("Variable prefix matcher must not be null!");
780         }
781         this.prefixMatcher = prefixMatcher;
782         return this;
783     }
784 
785     /**
786      * Sets the variable prefix to use.
787      * <p>
788      * The variable prefix is the character or characters that identify the
789      * start of a variable. This method allows a single character prefix to
790      * be easily set.
791      *
792      * @param prefix  the prefix character to use
793      * @return this, to enable chaining
794      */
795     public StrSubstitutor setVariablePrefix(final char prefix) {
796         return setVariablePrefixMatcher(StrMatcher.charMatcher(prefix));
797     }
798 
799     /**
800      * Sets the variable prefix to use.
801      * <p>
802      * The variable prefix is the characer or characters that identify the
803      * start of a variable. This method allows a string prefix to be easily set.
804      *
805      * @param prefix  the prefix for variables, not null
806      * @return this, to enable chaining
807      * @throws IllegalArgumentException if the prefix is null
808      */
809     public StrSubstitutor setVariablePrefix(final String prefix) {
810        if (prefix == null) {
811             throw new IllegalArgumentException("Variable prefix must not be null!");
812         }
813         return setVariablePrefixMatcher(StrMatcher.stringMatcher(prefix));
814     }
815 
816     // Suffix
817     //-----------------------------------------------------------------------
818     /**
819      * Gets the variable suffix matcher currently in use.
820      * <p>
821      * The variable suffix is the characer or characters that identify the
822      * end of a variable. This suffix is expressed in terms of a matcher
823      * allowing advanced suffix matches.
824      *
825      * @return the suffix matcher in use
826      */
827     public StrMatcher getVariableSuffixMatcher() {
828         return suffixMatcher;
829     }
830 
831     /**
832      * Sets the variable suffix matcher currently in use.
833      * <p>
834      * The variable suffix is the characer or characters that identify the
835      * end of a variable. This suffix is expressed in terms of a matcher
836      * allowing advanced suffix matches.
837      *
838      * @param suffixMatcher  the suffix matcher to use, null ignored
839      * @return this, to enable chaining
840      * @throws IllegalArgumentException if the suffix matcher is null
841      */
842     public StrSubstitutor setVariableSuffixMatcher(final StrMatcher suffixMatcher) {
843         if (suffixMatcher == null) {
844             throw new IllegalArgumentException("Variable suffix matcher must not be null!");
845         }
846         this.suffixMatcher = suffixMatcher;
847         return this;
848     }
849 
850     /**
851      * Sets the variable suffix to use.
852      * <p>
853      * The variable suffix is the characer or characters that identify the
854      * end of a variable. This method allows a single character suffix to
855      * be easily set.
856      *
857      * @param suffix  the suffix character to use
858      * @return this, to enable chaining
859      */
860     public StrSubstitutor setVariableSuffix(final char suffix) {
861         return setVariableSuffixMatcher(StrMatcher.charMatcher(suffix));
862     }
863 
864     /**
865      * Sets the variable suffix to use.
866      * <p>
867      * The variable suffix is the character or characters that identify the
868      * end of a variable. This method allows a string suffix to be easily set.
869      *
870      * @param suffix  the suffix for variables, not null
871      * @return this, to enable chaining
872      * @throws IllegalArgumentException if the suffix is null
873      */
874     public StrSubstitutor setVariableSuffix(final String suffix) {
875        if (suffix == null) {
876             throw new IllegalArgumentException("Variable suffix must not be null!");
877         }
878         return setVariableSuffixMatcher(StrMatcher.stringMatcher(suffix));
879     }
880 
881     // Resolver
882     //-----------------------------------------------------------------------
883     /**
884      * Gets the VariableResolver that is used to lookup variables.
885      *
886      * @return the VariableResolver
887      */
888     public StrLookup<?> getVariableResolver() {
889         return this.variableResolver;
890     }
891 
892     /**
893      * Sets the VariableResolver that is used to lookup variables.
894      *
895      * @param variableResolver  the VariableResolver
896      */
897     public void setVariableResolver(final StrLookup<?> variableResolver) {
898         this.variableResolver = variableResolver;
899     }
900 
901     // Substitution support in variable names
902     //-----------------------------------------------------------------------
903     /**
904      * Returns a flag whether substitution is done in variable names.
905      *
906      * @return the substitution in variable names flag
907      * @since 3.0
908      */
909     public boolean isEnableSubstitutionInVariables() {
910         return enableSubstitutionInVariables;
911     }
912 
913     /**
914      * Sets a flag whether substitution is done in variable names. If set to
915      * <b>true</b>, the names of variables can contain other variables which are
916      * processed first before the original variable is evaluated, e.g.
917      * <code>${jre-${java.version}}</code>. The default value is <b>false</b>.
918      *
919      * @param enableSubstitutionInVariables the new value of the flag
920      * @since 3.0
921      */
922     public void setEnableSubstitutionInVariables(
923             final boolean enableSubstitutionInVariables) {
924         this.enableSubstitutionInVariables = enableSubstitutionInVariables;
925     }
926 }