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.logging.log4j.core.lookup;
18  
19  import java.util.ArrayList;
20  import java.util.Enumeration;
21  import java.util.HashMap;
22  import java.util.Iterator;
23  import java.util.List;
24  import java.util.Map;
25  import java.util.Objects;
26  import java.util.Properties;
27  
28  import org.apache.logging.log4j.core.LogEvent;
29  import org.apache.logging.log4j.core.config.Configuration;
30  import org.apache.logging.log4j.core.config.ConfigurationAware;
31  import org.apache.logging.log4j.status.StatusLogger;
32  import org.apache.logging.log4j.util.Strings;
33  
34  /**
35   * Substitutes variables within a string by values.
36   * <p>
37   * This class takes a piece of text and substitutes all the variables within it.
38   * The default definition of a variable is <code>${variableName}</code>.
39   * The prefix and suffix can be changed via constructors and set methods.
40   * </p>
41   * <p>
42   * Variable values are typically resolved from a map, but could also be resolved
43   * from system properties, or by supplying a custom variable resolver.
44   * </p>
45   * <p>
46   * The simplest example is to use this class to replace Java System properties. For example:
47   * </p>
48   * <pre>
49   * StrSubstitutor.replaceSystemProperties(
50   *      "You are running with java.version = ${java.version} and os.name = ${os.name}.");
51   * </pre>
52   * <p>
53   * Typical usage of this class follows the following pattern: First an instance is created
54   * and initialized with the map that contains the values for the available variables.
55   * If a prefix and/or suffix for variables should be used other than the default ones,
56   * the appropriate settings can be performed. After that the <code>replace()</code>
57   * method can be called passing in the source text for interpolation. In the returned
58   * text all variable references (as long as their values are known) will be resolved.
59   * The following example demonstrates this:
60   * </p>
61   * <pre>
62   * Map valuesMap = HashMap();
63   * valuesMap.put(&quot;animal&quot;, &quot;quick brown fox&quot;);
64   * valuesMap.put(&quot;target&quot;, &quot;lazy dog&quot;);
65   * String templateString = &quot;The ${animal} jumped over the ${target}.&quot;;
66   * StrSubstitutor sub = new StrSubstitutor(valuesMap);
67   * String resolvedString = sub.replace(templateString);
68   * </pre>
69   * <p>yielding:</p>
70   * <pre>
71   *      The quick brown fox jumped over the lazy dog.
72   * </pre>
73   * <p>
74   * Also, this class allows to set a default value for unresolved variables.
75   * The default value for a variable can be appended to the variable name after the variable
76   * default value delimiter. The default value of the variable default value delimiter is ':-',
77   * as in bash and other *nix shells, as those are arguably where the default ${} delimiter set originated.
78   * The variable default value delimiter can be manually set by calling {@link #setValueDelimiterMatcher(StrMatcher)},
79   * {@link #setValueDelimiter(char)} or {@link #setValueDelimiter(String)}.
80   * The following shows an example with variable default value settings:
81   * </p>
82   * <pre>
83   * Map valuesMap = HashMap();
84   * valuesMap.put(&quot;animal&quot;, &quot;quick brown fox&quot;);
85   * valuesMap.put(&quot;target&quot;, &quot;lazy dog&quot;);
86   * String templateString = &quot;The ${animal} jumped over the ${target}. ${undefined.number:-1234567890}.&quot;;
87   * StrSubstitutor sub = new StrSubstitutor(valuesMap);
88   * String resolvedString = sub.replace(templateString);
89   * </pre>
90   * <p>yielding:</p>
91   * <pre>
92   *      The quick brown fox jumped over the lazy dog. 1234567890.
93   * </pre>
94   * <p>
95   * In addition to this usage pattern there are some static convenience methods that
96   * cover the most common use cases. These methods can be used without the need of
97   * manually creating an instance. However if multiple replace operations are to be
98   * performed, creating and reusing an instance of this class will be more efficient.
99   * </p>
100  * <p>
101  * Variable replacement works in a recursive way. Thus, if a variable value contains
102  * a variable then that variable will also be replaced. Cyclic replacements are
103  * detected and will cause an exception to be thrown.
104  * </p>
105  * <p>
106  * Sometimes the interpolation's result must contain a variable prefix. As an example
107  * take the following source text:
108  * </p>
109  * <pre>
110  *   The variable ${${name}} must be used.
111  * </pre>
112  * <p>
113  * Here only the variable's name referred to in the text should be replaced resulting
114  * in the text (assuming that the value of the <code>name</code> variable is <code>x</code>):
115  * </p>
116  * <pre>
117  *   The variable ${x} must be used.
118  * </pre>
119  * <p>
120  * To achieve this effect there are two possibilities: Either set a different prefix
121  * and suffix for variables which do not conflict with the result text you want to
122  * produce. The other possibility is to use the escape character, by default '$'.
123  * If this character is placed before a variable reference, this reference is ignored
124  * and won't be replaced. For example:
125  * </p>
126  * <pre>
127  *   The variable $${${name}} must be used.
128  * </pre>
129  * <p>
130  * In some complex scenarios you might even want to perform substitution in the
131  * names of variables, for instance
132  * </p>
133  * <pre>
134  * ${jre-${java.specification.version}}
135  * </pre>
136  * <p>
137  * <code>StrSubstitutor</code> supports this recursive substitution in variable
138  * names, but it has to be enabled explicitly by setting the
139  * {@link #setEnableSubstitutionInVariables(boolean) enableSubstitutionInVariables}
140  * property to <b>true</b>.
141  * </p>
142  */
143 public class StrSubstitutor implements ConfigurationAware {
144 
145     /**
146      * Constant for the default escape character.
147      */
148     public static final char DEFAULT_ESCAPE = '$';
149 
150     /**
151      * Constant for the default variable prefix.
152      */
153     public static final StrMatcher DEFAULT_PREFIX = StrMatcher.stringMatcher(DEFAULT_ESCAPE + "{");
154 
155     /**
156      * Constant for the default variable suffix.
157      */
158     public static final StrMatcher DEFAULT_SUFFIX = StrMatcher.stringMatcher("}");
159 
160     /**
161      * Constant for the default value delimiter of a variable.
162      */
163     public static final String DEFAULT_VALUE_DELIMITER_STRING = ":-";
164     public static final StrMatcher DEFAULT_VALUE_DELIMITER = StrMatcher.stringMatcher(DEFAULT_VALUE_DELIMITER_STRING);
165 
166     public static final String ESCAPE_DELIMITER_STRING = ":\\-";
167     public static final StrMatcher DEFAULT_VALUE_ESCAPE_DELIMITER = StrMatcher.stringMatcher(ESCAPE_DELIMITER_STRING);
168 
169     private static final int BUF_SIZE = 256;
170 
171     /**
172      * Stores the escape character.
173      */
174     private char escapeChar;
175 
176     /**
177      * Stores the variable prefix.
178      */
179     private StrMatcher prefixMatcher;
180 
181     /**
182      * Stores the variable suffix.
183      */
184     private StrMatcher suffixMatcher;
185 
186     /**
187      * Stores the default variable value delimiter
188      */
189     private String valueDelimiterString;
190     private StrMatcher valueDelimiterMatcher;
191 
192     /**
193      * Escape string to avoid matching the value delimiter matcher;
194      */
195     private StrMatcher valueEscapeDelimiterMatcher;
196 
197     /**
198      * Variable resolution is delegated to an implementer of VariableResolver.
199      */
200     private StrLookup variableResolver;
201 
202     /**
203      * The flag whether substitution in variable names is enabled.
204      */
205     private boolean enableSubstitutionInVariables = true;
206 
207     /**
208      * The currently active Configuration for use by ConfigurationAware StrLookup implementations.
209      */
210     private Configuration configuration;
211 
212     private boolean recursiveEvaluationAllowed;
213 
214     //-----------------------------------------------------------------------
215     /**
216      * Creates a new instance with defaults for variable prefix and suffix
217      * and the escaping character.
218      */
219     public StrSubstitutor() {
220         this(null, DEFAULT_PREFIX, DEFAULT_SUFFIX, DEFAULT_ESCAPE);
221     }
222 
223     /**
224      * Creates a new instance and initializes it. Uses defaults for variable
225      * prefix and suffix and the escaping character.
226      *
227      * @param valueMap  the map with the variables' values, may be null
228      */
229     public StrSubstitutor(final Map<String, String> valueMap) {
230         this(new MapLookup(valueMap), DEFAULT_PREFIX, DEFAULT_SUFFIX, DEFAULT_ESCAPE);
231     }
232 
233     /**
234      * Creates a new instance and initializes it. Uses a default escaping character.
235      *
236      * @param valueMap  the map with the variables' values, may be null
237      * @param prefix  the prefix for variables, not null
238      * @param suffix  the suffix for variables, not null
239      * @throws IllegalArgumentException if the prefix or suffix is null
240      */
241     public StrSubstitutor(final Map<String, String> valueMap, final String prefix, final String suffix) {
242         this(new MapLookup(valueMap), prefix, suffix, DEFAULT_ESCAPE);
243     }
244 
245     /**
246      * Creates a new instance and initializes it.
247      *
248      * @param valueMap  the map with the variables' values, may be null
249      * @param prefix  the prefix for variables, not null
250      * @param suffix  the suffix for variables, not null
251      * @param escape  the escape character
252      * @throws IllegalArgumentException if the prefix or suffix is null
253      */
254     public StrSubstitutor(final Map<String, String> valueMap, final String prefix, final String suffix,
255                           final char escape) {
256         this(new MapLookup(valueMap), prefix, suffix, escape);
257     }
258 
259     /**
260      * Creates a new instance and initializes it.
261      *
262      * @param valueMap  the map with the variables' values, 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      * @param valueDelimiter  the variable default value delimiter, may be null
267      * @throws IllegalArgumentException if the prefix or suffix is null
268      */
269     public StrSubstitutor(final Map<String, String> valueMap, final String prefix, final String suffix,
270                               final char escape, final String valueDelimiter) {
271         this(new MapLookup(valueMap), prefix, suffix, escape, valueDelimiter);
272     }
273 
274     /**
275      * Creates a new instance and initializes it. Uses defaults for variable
276      * prefix and suffix and the escaping character.
277      *
278      * @param properties  the map with the variables' values, may be null
279      */
280     public StrSubstitutor(final Properties properties) {
281         this(toTypeSafeMap(properties));
282     }
283 
284     /**
285      * Creates a new instance and initializes it.
286      *
287      * @param variableResolver  the variable resolver, may be null
288      */
289     public StrSubstitutor(final StrLookup variableResolver) {
290         this(variableResolver, DEFAULT_PREFIX, DEFAULT_SUFFIX, DEFAULT_ESCAPE);
291     }
292 
293     /**
294      * Creates a new instance and initializes it.
295      *
296      * @param variableResolver  the variable resolver, may be null
297      * @param prefix  the prefix for variables, not null
298      * @param suffix  the suffix for variables, not null
299      * @param escape  the escape character
300      * @throws IllegalArgumentException if the prefix or suffix is null
301      */
302     public StrSubstitutor(final StrLookup variableResolver, final String prefix, final String suffix,
303                           final char escape) {
304         this.setVariableResolver(variableResolver);
305         this.setVariablePrefix(prefix);
306         this.setVariableSuffix(suffix);
307         this.setEscapeChar(escape);
308     }
309 
310     /**
311      * Creates a new instance and initializes it.
312      *
313      * @param variableResolver  the variable resolver, may be null
314      * @param prefix  the prefix for variables, not null
315      * @param suffix  the suffix for variables, not null
316      * @param escape  the escape character
317      * @param valueDelimiter  the variable default value delimiter string, may be null
318      * @throws IllegalArgumentException if the prefix or suffix is null
319      */
320     public StrSubstitutor(final StrLookup variableResolver, final String prefix, final String suffix, final char escape, final String valueDelimiter) {
321         this.setVariableResolver(variableResolver);
322         this.setVariablePrefix(prefix);
323         this.setVariableSuffix(suffix);
324         this.setEscapeChar(escape);
325         this.setValueDelimiter(valueDelimiter);
326     }
327 
328     /**
329      * Creates a new instance and initializes it.
330      *
331      * @param variableResolver  the variable resolver, may be null
332      * @param prefixMatcher  the prefix for variables, not null
333      * @param suffixMatcher  the suffix for variables, not null
334      * @param escape  the escape character
335      * @throws IllegalArgumentException if the prefix or suffix is null
336      */
337     public StrSubstitutor(final StrLookup variableResolver, final StrMatcher prefixMatcher,
338                           final StrMatcher suffixMatcher,
339                           final char escape) {
340         this(variableResolver, prefixMatcher, suffixMatcher, escape, DEFAULT_VALUE_DELIMITER,
341                 DEFAULT_VALUE_ESCAPE_DELIMITER);
342         this.valueDelimiterString = DEFAULT_VALUE_DELIMITER_STRING;
343     }
344 
345     /**
346      * Creates a new instance and initializes it.
347      *
348      * @param variableResolver  the variable resolver, may be null
349      * @param prefixMatcher  the prefix for variables, not null
350      * @param suffixMatcher  the suffix for variables, not null
351      * @param escape  the escape character
352      * @param valueDelimiterMatcher  the variable default value delimiter matcher, may be null
353      * @throws IllegalArgumentException if the prefix or suffix is null
354      */
355     public StrSubstitutor(final StrLookup variableResolver, final StrMatcher prefixMatcher,
356             final StrMatcher suffixMatcher, final char escape, final StrMatcher valueDelimiterMatcher) {
357         this.setVariableResolver(variableResolver);
358         this.setVariablePrefixMatcher(prefixMatcher);
359         this.setVariableSuffixMatcher(suffixMatcher);
360         this.setEscapeChar(escape);
361         this.setValueDelimiterMatcher(valueDelimiterMatcher);
362     }
363 
364     /**
365      * Creates a new instance and initializes it.
366      *
367      * @param variableResolver  the variable resolver, may be null
368      * @param prefixMatcher  the prefix for variables, not null
369      * @param suffixMatcher  the suffix for variables, not null
370      * @param escape  the escape character
371      * @param valueDelimiterMatcher  the variable default value delimiter matcher, may be null
372      * @param valueEscapeMatcher the matcher to escape defaulting, may be null.
373      * @throws IllegalArgumentException if the prefix or suffix is null
374      */
375     public StrSubstitutor(final StrLookup variableResolver, final StrMatcher prefixMatcher,
376                           final StrMatcher suffixMatcher, final char escape, final StrMatcher valueDelimiterMatcher,
377                           final StrMatcher valueEscapeMatcher) {
378         this.setVariableResolver(variableResolver);
379         this.setVariablePrefixMatcher(prefixMatcher);
380         this.setVariableSuffixMatcher(suffixMatcher);
381         this.setEscapeChar(escape);
382         this.setValueDelimiterMatcher(valueDelimiterMatcher);
383         valueEscapeDelimiterMatcher = valueEscapeMatcher;
384     }
385 
386     StrSubstitutor(final StrSubstitutor other) {
387         Objects.requireNonNull(other, "other");
388         this.setVariableResolver(other.getVariableResolver());
389         this.setVariablePrefixMatcher(other.getVariablePrefixMatcher());
390         this.setVariableSuffixMatcher(other.getVariableSuffixMatcher());
391         this.setEscapeChar(other.getEscapeChar());
392         this.setValueDelimiterMatcher(other.valueDelimiterMatcher);
393         this.valueEscapeDelimiterMatcher = other.valueEscapeDelimiterMatcher;
394         this.configuration = other.configuration;
395         this.recursiveEvaluationAllowed = other.isRecursiveEvaluationAllowed();
396         this.enableSubstitutionInVariables = other.isEnableSubstitutionInVariables();
397         this.valueDelimiterString = other.valueDelimiterString;
398     }
399 
400     //-----------------------------------------------------------------------
401     /**
402      * Replaces all the occurrences of variables in the given source object with
403      * their matching values from the map.
404      *
405      * @param source  the source text containing the variables to substitute, null returns null
406      * @param valueMap  the map with the values, may be null
407      * @return the result of the replace operation
408      */
409     public static String replace(final Object source, final Map<String, String> valueMap) {
410         return new StrSubstitutor(valueMap).replace(source);
411     }
412 
413     /**
414      * Replaces all the occurrences of variables in the given source object with
415      * their matching values from the map. This method allows to specify a
416      * custom variable prefix and suffix
417      *
418      * @param source  the source text containing the variables to substitute, null returns null
419      * @param valueMap  the map with the values, may be null
420      * @param prefix  the prefix of variables, not null
421      * @param suffix  the suffix of variables, not null
422      * @return the result of the replace operation
423      * @throws IllegalArgumentException if the prefix or suffix is null
424      */
425     public static String replace(final Object source, final Map<String, String> valueMap, final String prefix,
426                                  final String suffix) {
427         return new StrSubstitutor(valueMap, prefix, suffix).replace(source);
428     }
429 
430     /**
431      * Replaces all the occurrences of variables in the given source object with their matching
432      * values from the properties.
433      *
434      * @param source the source text containing the variables to substitute, null returns null
435      * @param valueProperties the properties with values, may be null
436      * @return the result of the replace operation
437      */
438     public static String replace(final Object source, final Properties valueProperties) {
439         if (valueProperties == null) {
440             return source.toString();
441         }
442         final Map<String, String> valueMap = new HashMap<>();
443         final Enumeration<?> propNames = valueProperties.propertyNames();
444         while (propNames.hasMoreElements()) {
445             final String propName = (String) propNames.nextElement();
446             final String propValue = valueProperties.getProperty(propName);
447             valueMap.put(propName, propValue);
448         }
449         return StrSubstitutor.replace(source, valueMap);
450     }
451 
452     private static Map<String, String> toTypeSafeMap(final Properties properties) {
453         final Map<String, String> map = new HashMap<>(properties.size());
454         for (final String name : properties.stringPropertyNames()) {
455             map.put(name, properties.getProperty(name));
456         }
457         return map;
458     }
459 
460     private static String handleFailedReplacement(String input, Throwable throwable) {
461         StatusLogger.getLogger().error("Replacement failed on {}", input, throwable);
462         return input;
463     }
464 
465     //-----------------------------------------------------------------------
466     /**
467      * Replaces all the occurrences of variables with their matching values
468      * from the resolver using the given source string as a template.
469      *
470      * @param source  the string to replace in, null returns null
471      * @return the result of the replace operation
472      */
473     public String replace(final String source) {
474         return replace(null, source);
475     }
476     //-----------------------------------------------------------------------
477     /**
478      * Replaces all the occurrences of variables with their matching values
479      * from the resolver using the given source string as a template.
480      *
481      * @param event The current LogEvent if there is one.
482      * @param source  the string to replace in, null returns null
483      * @return the result of the replace operation
484      */
485     public String replace(final LogEvent event, final String source) {
486         if (source == null) {
487             return null;
488         }
489         final StringBuilder buf = new StringBuilder(source);
490         try {
491             if (!substitute(event, buf, 0, source.length())) {
492                 return source;
493             }
494         } catch (Throwable t) {
495             return handleFailedReplacement(source, t);
496         }
497         return buf.toString();
498     }
499 
500     /**
501      * Replaces all the occurrences of variables with their matching values
502      * from the resolver using the given source string as a template.
503      * <p>
504      * Only the specified portion of the string will be processed.
505      * The rest of the string is not processed, and is not returned.
506      * </p>
507      *
508      * @param source  the string to replace in, null returns null
509      * @param offset  the start offset within the array, must be valid
510      * @param length  the length within the array to be processed, must be valid
511      * @return the result of the replace operation
512      */
513     public String replace(final String source, final int offset, final int length) {
514         return replace(null, source, offset, length);
515     }
516 
517     /**
518      * Replaces all the occurrences of variables with their matching values
519      * from the resolver using the given source string as a template.
520      * <p>
521      * Only the specified portion of the string will be processed.
522      * The rest of the string is not processed, and is not returned.
523      * </p>
524      *
525      * @param event the current LogEvent, if one exists.
526      * @param source  the string to replace in, null returns null
527      * @param offset  the start offset within the array, must be valid
528      * @param length  the length within the array to be processed, must be valid
529      * @return the result of the replace operation
530      */
531     public String replace(final LogEvent event, final String source, final int offset, final int length) {
532         if (source == null) {
533             return null;
534         }
535         final StringBuilder buf = new StringBuilder(length).append(source, offset, length);
536         try {
537             if (!substitute(event, buf, 0, length)) {
538                 return source.substring(offset, offset + length);
539             }
540         } catch (Throwable t) {
541             return handleFailedReplacement(source, t);
542         }
543         return buf.toString();
544     }
545 
546     //-----------------------------------------------------------------------
547     /**
548      * Replaces all the occurrences of variables with their matching values
549      * from the resolver using the given source array as a template.
550      * The array is not altered by this method.
551      *
552      * @param source  the character array to replace in, not altered, null returns null
553      * @return the result of the replace operation
554      */
555     public String replace(final char[] source) {
556         return replace(null, source);
557     }
558 
559     //-----------------------------------------------------------------------
560     /**
561      * Replaces all the occurrences of variables with their matching values
562      * from the resolver using the given source array as a template.
563      * The array is not altered by this method.
564      *
565      * @param event the current LogEvent, if one exists.
566      * @param source  the character array to replace in, not altered, null returns null
567      * @return the result of the replace operation
568      */
569     public String replace(final LogEvent event, final char[] source) {
570         if (source == null) {
571             return null;
572         }
573         final StringBuilder buf = new StringBuilder(source.length).append(source);
574         try {
575             substitute(event, buf, 0, source.length);
576         } catch (Throwable t) {
577             return handleFailedReplacement(new String(source), t);
578         }
579         return buf.toString();
580     }
581 
582     /**
583      * Replaces all the occurrences of variables with their matching values
584      * from the resolver using the given source array as a template.
585      * The array is not altered by this method.
586      * <p>
587      * Only the specified portion of the array will be processed.
588      * The rest of the array is not processed, and is not returned.
589      * </p>
590      *
591      * @param source  the character array to replace in, not altered, null returns null
592      * @param offset  the start offset within the array, must be valid
593      * @param length  the length within the array to be processed, must be valid
594      * @return the result of the replace operation
595      */
596     public String replace(final char[] source, final int offset, final int length) {
597         return replace(null, source, offset, length);
598     }
599 
600     /**
601      * Replaces all the occurrences of variables with their matching values
602      * from the resolver using the given source array as a template.
603      * The array is not altered by this method.
604      * <p>
605      * Only the specified portion of the array will be processed.
606      * The rest of the array is not processed, and is not returned.
607      * </p>
608      *
609      * @param event the current LogEvent, if one exists.
610      * @param source  the character array to replace in, not altered, null returns null
611      * @param offset  the start offset within the array, must be valid
612      * @param length  the length within the array to be processed, must be valid
613      * @return the result of the replace operation
614      */
615     public String replace(final LogEvent event, final char[] source, final int offset, final int length) {
616         if (source == null) {
617             return null;
618         }
619         final StringBuilder buf = new StringBuilder(length).append(source, offset, length);
620         try {
621             substitute(event, buf, 0, length);
622         } catch (Throwable t) {
623             return handleFailedReplacement(new String(source, offset, length), t);
624         }
625         return buf.toString();
626     }
627 
628     //-----------------------------------------------------------------------
629     /**
630      * Replaces all the occurrences of variables with their matching values
631      * from the resolver using the given source buffer as a template.
632      * The buffer is not altered by this method.
633      *
634      * @param source  the buffer to use as a template, not changed, null returns null
635      * @return the result of the replace operation
636      */
637     public String replace(final StringBuffer source) {
638         return replace(null, source);
639     }
640 
641     //-----------------------------------------------------------------------
642     /**
643      * Replaces all the occurrences of variables with their matching values
644      * from the resolver using the given source buffer as a template.
645      * The buffer is not altered by this method.
646      *
647      * @param event the current LogEvent, if one exists.
648      * @param source  the buffer to use as a template, not changed, null returns null
649      * @return the result of the replace operation
650      */
651     public String replace(final LogEvent event, final StringBuffer source) {
652         if (source == null) {
653             return null;
654         }
655         final StringBuilder buf = new StringBuilder(source.length()).append(source);
656         try {
657             substitute(event, buf, 0, buf.length());
658         } catch (Throwable t) {
659             return handleFailedReplacement(source.toString(), t);
660         }
661         return buf.toString();
662     }
663 
664     /**
665      * Replaces all the occurrences of variables with their matching values
666      * from the resolver using the given source buffer as a template.
667      * The buffer is not altered by this method.
668      * <p>
669      * Only the specified portion of the buffer will be processed.
670      * The rest of the buffer is not processed, and is not returned.
671      * </p>
672      *
673      * @param source  the buffer to use as a template, not changed, null returns null
674      * @param offset  the start offset within the array, must be valid
675      * @param length  the length within the array to be processed, must be valid
676      * @return the result of the replace operation
677      */
678     public String replace(final StringBuffer source, final int offset, final int length) {
679         return replace(null, source, offset, length);
680     }
681 
682     /**
683      * Replaces all the occurrences of variables with their matching values
684      * from the resolver using the given source buffer as a template.
685      * The buffer is not altered by this method.
686      * <p>
687      * Only the specified portion of the buffer will be processed.
688      * The rest of the buffer is not processed, and is not returned.
689      * </p>
690      *
691      * @param event the current LogEvent, if one exists.
692      * @param source  the buffer to use as a template, not changed, null returns null
693      * @param offset  the start offset within the array, must be valid
694      * @param length  the length within the array to be processed, must be valid
695      * @return the result of the replace operation
696      */
697     public String replace(final LogEvent event, final StringBuffer source, final int offset, final int length) {
698         if (source == null) {
699             return null;
700         }
701         final StringBuilder buf = new StringBuilder(length).append(source, offset, length);
702         try {
703             substitute(event, buf, 0, length);
704         } catch (Throwable t) {
705             return handleFailedReplacement(source.substring(offset, offset + length), t);
706         }
707         return buf.toString();
708     }
709 
710     //-----------------------------------------------------------------------
711     /**
712      * Replaces all the occurrences of variables with their matching values
713      * from the resolver using the given source builder as a template.
714      * The builder is not altered by this method.
715      *
716      * @param source  the builder to use as a template, not changed, null returns null
717      * @return the result of the replace operation
718      */
719     public String replace(final StringBuilder source) {
720         return replace(null, source);
721     }
722 
723     //-----------------------------------------------------------------------
724     /**
725      * Replaces all the occurrences of variables with their matching values
726      * from the resolver using the given source builder as a template.
727      * The builder is not altered by this method.
728      *
729      * @param event The LogEvent.
730      * @param source  the builder to use as a template, not changed, null returns null.
731      * @return the result of the replace operation.
732      */
733     public String replace(final LogEvent event, final StringBuilder source) {
734         if (source == null) {
735             return null;
736         }
737         final StringBuilder buf = new StringBuilder(source.length()).append(source);
738         try {
739             substitute(event, buf, 0, buf.length());
740         } catch (Throwable t) {
741             return handleFailedReplacement(source.toString(), t);
742         }
743         return buf.toString();
744     }
745     /**
746      * Replaces all the occurrences of variables with their matching values
747      * from the resolver using the given source builder as a template.
748      * The builder is not altered by this method.
749      * <p>
750      * Only the specified portion of the builder will be processed.
751      * The rest of the builder is not processed, and is not returned.
752      * </p>
753      *
754      * @param source  the builder to use as a template, not changed, null returns null
755      * @param offset  the start offset within the array, must be valid
756      * @param length  the length within the array to be processed, must be valid
757      * @return the result of the replace operation
758      */
759     public String replace(final StringBuilder source, final int offset, final int length) {
760         return replace(null, source, offset, length);
761     }
762 
763     /**
764      * Replaces all the occurrences of variables with their matching values
765      * from the resolver using the given source builder as a template.
766      * The builder is not altered by this method.
767      * <p>
768      * Only the specified portion of the builder will be processed.
769      * The rest of the builder is not processed, and is not returned.
770      * </p>
771      *
772      * @param event the current LogEvent, if one exists.
773      * @param source  the builder to use as a template, not changed, null returns null
774      * @param offset  the start offset within the array, must be valid
775      * @param length  the length within the array to be processed, must be valid
776      * @return the result of the replace operation
777      */
778     public String replace(final LogEvent event, final StringBuilder source, final int offset, final int length) {
779         if (source == null) {
780             return null;
781         }
782         final StringBuilder buf = new StringBuilder(length).append(source, offset, length);
783         try {
784             substitute(event, buf, 0, length);
785         } catch (Throwable t) {
786             return handleFailedReplacement(source.substring(offset, offset + length), t);
787         }
788         return buf.toString();
789     }
790 
791     //-----------------------------------------------------------------------
792     /**
793      * Replaces all the occurrences of variables in the given source object with
794      * their matching values from the resolver. The input source object is
795      * converted to a string using <code>toString</code> and is not altered.
796      *
797      * @param source  the source to replace in, null returns null
798      * @return the result of the replace operation
799      */
800     public String replace(final Object source) {
801         return replace(null, source);
802     }
803     //-----------------------------------------------------------------------
804     /**
805      * Replaces all the occurrences of variables in the given source object with
806      * their matching values from the resolver. The input source object is
807      * converted to a string using <code>toString</code> and is not altered.
808      *
809      * @param event the current LogEvent, if one exists.
810      * @param source  the source to replace in, null returns null
811      * @return the result of the replace operation
812      */
813     public String replace(final LogEvent event, final Object source) {
814         if (source == null) {
815             return null;
816         }
817         String stringValue = String.valueOf(source);
818         final StringBuilder buf = new StringBuilder(stringValue.length()).append(stringValue);
819         try {
820             substitute(event, buf, 0, buf.length());
821         } catch (Throwable t) {
822             return handleFailedReplacement(stringValue, t);
823         }
824         return buf.toString();
825     }
826 
827     //-----------------------------------------------------------------------
828     /**
829      * Replaces all the occurrences of variables within the given source buffer
830      * with their matching values from the resolver.
831      * The buffer is updated with the result.
832      *
833      * @param source  the buffer to replace in, updated, null returns zero
834      * @return true if altered
835      */
836     public boolean replaceIn(final StringBuffer source) {
837         if (source == null) {
838             return false;
839         }
840         return replaceIn(source, 0, source.length());
841     }
842 
843     /**
844      * Replaces all the occurrences of variables within the given source buffer
845      * with their matching values from the resolver.
846      * The buffer is updated with the result.
847      * <p>
848      * Only the specified portion of the buffer will be processed.
849      * The rest of the buffer is not processed, but it is not deleted.
850      * </p>
851      *
852      * @param source  the buffer to replace in, updated, null returns zero
853      * @param offset  the start offset within the array, must be valid
854      * @param length  the length within the buffer to be processed, must be valid
855      * @return true if altered
856      */
857     public boolean replaceIn(final StringBuffer source, final int offset, final int length) {
858         return replaceIn(null, source, offset, length);
859     }
860 
861     /**
862      * Replaces all the occurrences of variables within the given source buffer
863      * with their matching values from the resolver.
864      * The buffer is updated with the result.
865      * <p>
866      * Only the specified portion of the buffer will be processed.
867      * The rest of the buffer is not processed, but it is not deleted.
868      * </p>
869      *
870      * @param event the current LogEvent, if one exists.
871      * @param source  the buffer to replace in, updated, null returns zero
872      * @param offset  the start offset within the array, must be valid
873      * @param length  the length within the buffer to be processed, must be valid
874      * @return true if altered
875      */
876     public boolean replaceIn(final LogEvent event, final StringBuffer source, final int offset, final int length) {
877         if (source == null) {
878             return false;
879         }
880         final StringBuilder buf = new StringBuilder(length).append(source, offset, length);
881         try {
882             if (!substitute(event, buf, 0, length)) {
883                 return false;
884             }
885         } catch (Throwable t) {
886             StatusLogger.getLogger().error("Replacement failed on {}", source, t);
887             return false;
888         }
889         source.replace(offset, offset + length, buf.toString());
890         return true;
891     }
892 
893     //-----------------------------------------------------------------------
894     /**
895      * Replaces all the occurrences of variables within the given source
896      * builder with their matching values from the resolver.
897      *
898      * @param source  the builder to replace in, updated, null returns zero
899      * @return true if altered
900      */
901     public boolean replaceIn(final StringBuilder source) {
902         return replaceIn(null, source);
903     }
904 
905     //-----------------------------------------------------------------------
906     /**
907      * Replaces all the occurrences of variables within the given source
908      * builder with their matching values from the resolver.
909      *
910      * @param event the current LogEvent, if one exists.
911      * @param source  the builder to replace in, updated, null returns zero
912      * @return true if altered
913      */
914     public boolean replaceIn(final LogEvent event, final StringBuilder source) {
915         if (source == null) {
916             return false;
917         }
918         return substitute(event, source, 0, source.length());
919     }
920     /**
921      * Replaces all the occurrences of variables within the given source
922      * builder with their matching values from the resolver.
923      * <p>
924      * Only the specified portion of the builder will be processed.
925      * The rest of the builder is not processed, but it is not deleted.
926      * </p>
927      *
928      * @param source  the builder to replace in, null returns zero
929      * @param offset  the start offset within the array, must be valid
930      * @param length  the length within the builder to be processed, must be valid
931      * @return true if altered
932      */
933     public boolean replaceIn(final StringBuilder source, final int offset, final int length) {
934         return replaceIn(null, source, offset, length);
935     }
936 
937     /**
938      * Replaces all the occurrences of variables within the given source
939      * builder with their matching values from the resolver.
940      * <p>
941      * Only the specified portion of the builder will be processed.
942      * The rest of the builder is not processed, but it is not deleted.
943      * </p>
944      *
945      * @param event   the current LogEvent, if one is present.
946      * @param source  the builder to replace in, null returns zero
947      * @param offset  the start offset within the array, must be valid
948      * @param length  the length within the builder to be processed, must be valid
949      * @return true if altered
950      */
951     public boolean replaceIn(final LogEvent event, final StringBuilder source, final int offset, final int length) {
952         if (source == null) {
953             return false;
954         }
955         return substitute(event, source, offset, length);
956     }
957 
958     //-----------------------------------------------------------------------
959     /**
960      * Internal method that substitutes the variables.
961      * <p>
962      * Most users of this class do not need to call this method. This method will
963      * be called automatically by another (public) method.
964      * </p>
965      * <p>
966      * Writers of subclasses can override this method if they need access to
967      * the substitution process at the start or end.
968      * </p>
969      *
970      * @param event The current LogEvent, if there is one.
971      * @param buf  the string builder to substitute into, not null
972      * @param offset  the start offset within the builder, must be valid
973      * @param length  the length within the builder to be processed, must be valid
974      * @return true if altered
975      */
976     protected boolean substitute(final LogEvent event, final StringBuilder buf, final int offset, final int length) {
977         return substitute(event, buf, offset, length, null) > 0;
978     }
979 
980     /**
981      * Recursive handler for multiple levels of interpolation. This is the main
982      * interpolation method, which resolves the values of all variable references
983      * contained in the passed in text.
984      *
985      * @param event The current LogEvent, if there is one.
986      * @param buf  the string builder to substitute into, not null
987      * @param offset  the start offset within the builder, must be valid
988      * @param length  the length within the builder to be processed, must be valid
989      * @param priorVariables  the stack keeping track of the replaced variables, may be null
990      * @return the length change that occurs, unless priorVariables is null when the int
991      *  represents a boolean flag as to whether any change occurred.
992      */
993     private int substitute(final LogEvent event, final StringBuilder buf, final int offset, final int length,
994                            List<String> priorVariables) {
995         final StrMatcher prefixMatcher = getVariablePrefixMatcher();
996         final StrMatcher suffixMatcher = getVariableSuffixMatcher();
997         final char escape = getEscapeChar();
998         final StrMatcher valueDelimiterMatcher = getValueDelimiterMatcher();
999         final boolean substitutionInVariablesEnabled = isEnableSubstitutionInVariables();
1000 
1001         final boolean top = priorVariables == null;
1002         boolean altered = false;
1003         int lengthChange = 0;
1004         char[] chars = getChars(buf);
1005         int bufEnd = offset + length;
1006         int pos = offset;
1007         while (pos < bufEnd) {
1008             final int startMatchLen = prefixMatcher.isMatch(chars, pos, offset, bufEnd);
1009             if (startMatchLen == 0) {
1010                 pos++;
1011             } else // found variable start marker
1012             if (pos > offset && chars[pos - 1] == escape) {
1013                 // escaped
1014                 buf.deleteCharAt(pos - 1);
1015                 chars = getChars(buf);
1016                 lengthChange--;
1017                 altered = true;
1018                 bufEnd--;
1019             } else {
1020                 // find suffix
1021                 final int startPos = pos;
1022                 pos += startMatchLen;
1023                 int endMatchLen = 0;
1024                 int nestedVarCount = 0;
1025                 while (pos < bufEnd) {
1026                     if (substitutionInVariablesEnabled
1027                             && (endMatchLen = prefixMatcher.isMatch(chars, pos, offset, bufEnd)) != 0) {
1028                         // found a nested variable start
1029                         nestedVarCount++;
1030                         pos += endMatchLen;
1031                         continue;
1032                     }
1033 
1034                     endMatchLen = suffixMatcher.isMatch(chars, pos, offset, bufEnd);
1035                     if (endMatchLen == 0) {
1036                         pos++;
1037                     } else {
1038                         // found variable end marker
1039                         if (nestedVarCount == 0) {
1040                             String varNameExpr = new String(chars, startPos + startMatchLen, pos - startPos - startMatchLen);
1041                             if (substitutionInVariablesEnabled) {
1042                                 // initialize priorVariables if they're not already set
1043                                 if (priorVariables == null) {
1044                                     priorVariables = new ArrayList<>();
1045                                 }
1046                                 final StringBuilder bufName = new StringBuilder(varNameExpr);
1047                                 substitute(event, bufName, 0, bufName.length(), priorVariables);
1048                                 varNameExpr = bufName.toString();
1049                             }
1050                             pos += endMatchLen;
1051                             final int endPos = pos;
1052 
1053                             String varName = varNameExpr;
1054                             String varDefaultValue = null;
1055 
1056                             if (valueDelimiterMatcher != null) {
1057                                 final char [] varNameExprChars = varNameExpr.toCharArray();
1058                                 int valueDelimiterMatchLen = 0;
1059                                 for (int i = 0; i < varNameExprChars.length; i++) {
1060                                     // if there's any nested variable when nested variable substitution disabled, then stop resolving name and default value.
1061                                     if (!substitutionInVariablesEnabled
1062                                             && prefixMatcher.isMatch(varNameExprChars, i, i, varNameExprChars.length) != 0) {
1063                                         break;
1064                                     }
1065                                     if (valueEscapeDelimiterMatcher != null) {
1066                                         int matchLen = valueEscapeDelimiterMatcher.isMatch(varNameExprChars, i);
1067                                         if (matchLen != 0) {
1068                                             String varNamePrefix = varNameExpr.substring(0, i) + Interpolator.PREFIX_SEPARATOR;
1069                                             varName = varNamePrefix + varNameExpr.substring(i + matchLen - 1);
1070                                             for (int j = i + matchLen; j < varNameExprChars.length; ++j){
1071                                                 if ((valueDelimiterMatchLen = valueDelimiterMatcher.isMatch(varNameExprChars, j)) != 0) {
1072                                                     varName = varNamePrefix + varNameExpr.substring(i + matchLen, j);
1073                                                     varDefaultValue = varNameExpr.substring(j + valueDelimiterMatchLen);
1074                                                     break;
1075                                                 }
1076                                             }
1077                                             break;
1078                                         } else if ((valueDelimiterMatchLen = valueDelimiterMatcher.isMatch(varNameExprChars, i)) != 0) {
1079                                             varName = varNameExpr.substring(0, i);
1080                                             varDefaultValue = varNameExpr.substring(i + valueDelimiterMatchLen);
1081                                             break;
1082                                         }
1083                                     } else if ((valueDelimiterMatchLen = valueDelimiterMatcher.isMatch(varNameExprChars, i)) != 0) {
1084                                         varName = varNameExpr.substring(0, i);
1085                                         varDefaultValue = varNameExpr.substring(i + valueDelimiterMatchLen);
1086                                         break;
1087                                     }
1088                                 }
1089                             }
1090 
1091                             // on the first call initialize priorVariables
1092                             if (priorVariables == null) {
1093                                 priorVariables = new ArrayList<>();
1094                                 priorVariables.add(new String(chars, offset, length + lengthChange));
1095                             }
1096 
1097                             // handle cyclic substitution
1098                             boolean isCyclic = isCyclicSubstitution(varName, priorVariables);
1099 
1100                             // resolve the variable
1101                             String varValue = isCyclic ? null : resolveVariable(event, varName, buf, startPos, endPos);
1102                             if (varValue == null) {
1103                                 varValue = varDefaultValue;
1104                             }
1105                             if (varValue != null) {
1106                                 // recursive replace
1107                                 final int varLen = varValue.length();
1108                                 buf.replace(startPos, endPos, varValue);
1109                                 altered = true;
1110                                 int change = isRecursiveEvaluationAllowed()
1111                                         ? substitute(event, buf, startPos, varLen, priorVariables)
1112                                         : 0;
1113                                 change = change + (varLen - (endPos - startPos));
1114                                 pos += change;
1115                                 bufEnd += change;
1116                                 lengthChange += change;
1117                                 chars = getChars(buf); // in case buffer was altered
1118                             }
1119 
1120                             // remove variable from the cyclic stack
1121                             if (!isCyclic) {
1122                                 priorVariables.remove(priorVariables.size() - 1);
1123                             }
1124                             break;
1125                         }
1126                         nestedVarCount--;
1127                         pos += endMatchLen;
1128                     }
1129                 }
1130             }
1131         }
1132         if (top) {
1133             return altered ? 1 : 0;
1134         }
1135         return lengthChange;
1136     }
1137 
1138     /**
1139      * Checks if the specified variable is already in the stack (list) of variables, adding the value
1140      * if it's not already present.
1141      *
1142      * @param varName  the variable name to check
1143      * @param priorVariables  the list of prior variables
1144      * @return true if this is a cyclic substitution
1145      */
1146     private boolean isCyclicSubstitution(final String varName, final List<String> priorVariables) {
1147         if (!priorVariables.contains(varName)) {
1148             priorVariables.add(varName);
1149             return false;
1150         }
1151         final StringBuilder buf = new StringBuilder(BUF_SIZE);
1152         buf.append("Infinite loop in property interpolation of ");
1153         appendWithSeparators(buf, priorVariables, "->");
1154         StatusLogger.getLogger().warn(buf);
1155         return true;
1156     }
1157 
1158     /**
1159      * Internal method that resolves the value of a variable.
1160      * <p>
1161      * Most users of this class do not need to call this method. This method is
1162      * called automatically by the substitution process.
1163      * </p>
1164      * <p>
1165      * Writers of subclasses can override this method if they need to alter
1166      * how each substitution occurs. The method is passed the variable's name
1167      * and must return the corresponding value. This implementation uses the
1168      * {@link #getVariableResolver()} with the variable's name as the key.
1169      * </p>
1170      *
1171      * @param event The LogEvent, if there is one.
1172      * @param variableName  the name of the variable, not null
1173      * @param buf  the buffer where the substitution is occurring, not null
1174      * @param startPos  the start position of the variable including the prefix, valid
1175      * @param endPos  the end position of the variable including the suffix, valid
1176      * @return the variable's value or <b>null</b> if the variable is unknown
1177      */
1178     protected String resolveVariable(final LogEvent event, final String variableName, final StringBuilder buf,
1179                                      final int startPos, final int endPos) {
1180         final StrLookup resolver = getVariableResolver();
1181         if (resolver == null) {
1182             return null;
1183         }
1184         try {
1185             return resolver.lookup(event, variableName);
1186         } catch (Throwable t) {
1187             StatusLogger.getLogger().error("Resolver failed to lookup {}", variableName, t);
1188             return null;
1189         }
1190     }
1191 
1192     // Escape
1193     //-----------------------------------------------------------------------
1194     /**
1195      * Returns the escape character.
1196      *
1197      * @return the character used for escaping variable references
1198      */
1199     public char getEscapeChar() {
1200         return this.escapeChar;
1201     }
1202 
1203     /**
1204      * Sets the escape character.
1205      * If this character is placed before a variable reference in the source
1206      * text, this variable will be ignored.
1207      *
1208      * @param escapeCharacter  the escape character (0 for disabling escaping)
1209      */
1210     public void setEscapeChar(final char escapeCharacter) {
1211         this.escapeChar = escapeCharacter;
1212     }
1213 
1214     // Prefix
1215     //-----------------------------------------------------------------------
1216     /**
1217      * Gets the variable prefix matcher currently in use.
1218      * <p>
1219      * The variable prefix is the character or characters that identify the
1220      * start of a variable. This prefix is expressed in terms of a matcher
1221      * allowing advanced prefix matches.
1222      * </p>
1223      *
1224      * @return the prefix matcher in use
1225      */
1226     public StrMatcher getVariablePrefixMatcher() {
1227         return prefixMatcher;
1228     }
1229 
1230     /**
1231      * Sets the variable prefix matcher currently in use.
1232      * <p>
1233      * The variable prefix is the character or characters that identify the
1234      * start of a variable. This prefix is expressed in terms of a matcher
1235      * allowing advanced prefix matches.
1236      * </p>
1237      *
1238      * @param prefixMatcher  the prefix matcher to use, null ignored
1239      * @return this, to enable chaining
1240      * @throws IllegalArgumentException if the prefix matcher is null
1241      */
1242     public StrSubstitutor setVariablePrefixMatcher(final StrMatcher prefixMatcher) {
1243         if (prefixMatcher == null) {
1244             throw new IllegalArgumentException("Variable prefix matcher must not be null!");
1245         }
1246         this.prefixMatcher = prefixMatcher;
1247         return this;
1248     }
1249 
1250     /**
1251      * Sets the variable prefix to use.
1252      * <p>
1253      * The variable prefix is the character or characters that identify the
1254      * start of a variable. This method allows a single character prefix to
1255      * be easily set.
1256      * </p>
1257      *
1258      * @param prefix  the prefix character to use
1259      * @return this, to enable chaining
1260      */
1261     public StrSubstitutor setVariablePrefix(final char prefix) {
1262         return setVariablePrefixMatcher(StrMatcher.charMatcher(prefix));
1263     }
1264 
1265     /**
1266      * Sets the variable prefix to use.
1267      * <p>
1268      * The variable prefix is the character or characters that identify the
1269      * start of a variable. This method allows a string prefix to be easily set.
1270      * </p>
1271      *
1272      * @param prefix  the prefix for variables, not null
1273      * @return this, to enable chaining
1274      * @throws IllegalArgumentException if the prefix is null
1275      */
1276     public StrSubstitutor setVariablePrefix(final String prefix) {
1277        if (prefix == null) {
1278             throw new IllegalArgumentException("Variable prefix must not be null!");
1279         }
1280         return setVariablePrefixMatcher(StrMatcher.stringMatcher(prefix));
1281     }
1282 
1283     // Suffix
1284     //-----------------------------------------------------------------------
1285     /**
1286      * Gets the variable suffix matcher currently in use.
1287      * <p>
1288      * The variable suffix is the character or characters that identify the
1289      * end of a variable. This suffix is expressed in terms of a matcher
1290      * allowing advanced suffix matches.
1291      * </p>
1292      *
1293      * @return the suffix matcher in use
1294      */
1295     public StrMatcher getVariableSuffixMatcher() {
1296         return suffixMatcher;
1297     }
1298 
1299     /**
1300      * Sets the variable suffix matcher currently in use.
1301      * <p>
1302      * The variable suffix is the character or characters that identify the
1303      * end of a variable. This suffix is expressed in terms of a matcher
1304      * allowing advanced suffix matches.
1305      * </p>
1306      *
1307      * @param suffixMatcher  the suffix matcher to use, null ignored
1308      * @return this, to enable chaining
1309      * @throws IllegalArgumentException if the suffix matcher is null
1310      */
1311     public StrSubstitutor setVariableSuffixMatcher(final StrMatcher suffixMatcher) {
1312         if (suffixMatcher == null) {
1313             throw new IllegalArgumentException("Variable suffix matcher must not be null!");
1314         }
1315         this.suffixMatcher = suffixMatcher;
1316         return this;
1317     }
1318 
1319     /**
1320      * Sets the variable suffix to use.
1321      * <p>
1322      * The variable suffix is the character or characters that identify the
1323      * end of a variable. This method allows a single character suffix to
1324      * be easily set.
1325      * </p>
1326      *
1327      * @param suffix  the suffix character to use
1328      * @return this, to enable chaining
1329      */
1330     public StrSubstitutor setVariableSuffix(final char suffix) {
1331         return setVariableSuffixMatcher(StrMatcher.charMatcher(suffix));
1332     }
1333 
1334     /**
1335      * Sets the variable suffix to use.
1336      * <p>
1337      * The variable suffix is the character or characters that identify the
1338      * end of a variable. This method allows a string suffix to be easily set.
1339      * </p>
1340      *
1341      * @param suffix  the suffix for variables, not null
1342      * @return this, to enable chaining
1343      * @throws IllegalArgumentException if the suffix is null
1344      */
1345     public StrSubstitutor setVariableSuffix(final String suffix) {
1346        if (suffix == null) {
1347             throw new IllegalArgumentException("Variable suffix must not be null!");
1348         }
1349         return setVariableSuffixMatcher(StrMatcher.stringMatcher(suffix));
1350     }
1351 
1352     // Variable Default Value Delimiter
1353     //-----------------------------------------------------------------------
1354     /**
1355      * Gets the variable default value delimiter matcher currently in use.
1356      * <p>
1357      * The variable default value delimiter is the character or characters that delimit the
1358      * variable name and the variable default value. This delimiter is expressed in terms of a matcher
1359      * allowing advanced variable default value delimiter matches.
1360      * </p>
1361      * <p>
1362      * If it returns null, then the variable default value resolution is disabled.
1363      * </p>
1364      *
1365      * @return the variable default value delimiter matcher in use, may be null
1366      */
1367     public StrMatcher getValueDelimiterMatcher() {
1368         return valueDelimiterMatcher;
1369     }
1370 
1371     /**
1372      * Sets the variable default value delimiter matcher to use.
1373      * <p>
1374      * The variable default value delimiter is the character or characters that delimit the
1375      * variable name and the variable default value. This delimiter is expressed in terms of a matcher
1376      * allowing advanced variable default value delimiter matches.
1377      * </p>
1378      * <p>
1379      * If the <code>valueDelimiterMatcher</code> is null, then the variable default value resolution
1380      * becomes disabled.
1381      * </p>
1382      *
1383      * @param valueDelimiterMatcher  variable default value delimiter matcher to use, may be null
1384      * @return this, to enable chaining
1385      */
1386     public StrSubstitutor setValueDelimiterMatcher(final StrMatcher valueDelimiterMatcher) {
1387         this.valueDelimiterMatcher = valueDelimiterMatcher;
1388         return this;
1389     }
1390 
1391     /**
1392      * Sets the variable default value delimiter to use.
1393      * <p>
1394      * The variable default value delimiter is the character or characters that delimit the
1395      * variable name and the variable default value. This method allows a single character
1396      * variable default value delimiter to be easily set.
1397      * </p>
1398      *
1399      * @param valueDelimiter  the variable default value delimiter character to use
1400      * @return this, to enable chaining
1401      */
1402     public StrSubstitutor setValueDelimiter(final char valueDelimiter) {
1403         return setValueDelimiterMatcher(StrMatcher.charMatcher(valueDelimiter));
1404     }
1405 
1406     /**
1407      * Sets the variable default value delimiter to use.
1408      * <p>
1409      * The variable default value delimiter is the character or characters that delimit the
1410      * variable name and the variable default value. This method allows a string
1411      * variable default value delimiter to be easily set.
1412      * </p>
1413      * <p>
1414      * If the <code>valueDelimiter</code> is null or empty string, then the variable default
1415      * value resolution becomes disabled.
1416      * </p>
1417      *
1418      * @param valueDelimiter  the variable default value delimiter string to use, may be null or empty
1419      * @return this, to enable chaining
1420      */
1421     public StrSubstitutor setValueDelimiter(final String valueDelimiter) {
1422         if (Strings.isEmpty(valueDelimiter)) {
1423             setValueDelimiterMatcher(null);
1424             return this;
1425         }
1426         String escapeValue = valueDelimiter.substring(0, valueDelimiter.length() - 1) + "\\"
1427                 + valueDelimiter.substring(valueDelimiter.length() - 1);
1428         valueEscapeDelimiterMatcher = StrMatcher.stringMatcher(escapeValue);
1429         return setValueDelimiterMatcher(StrMatcher.stringMatcher(valueDelimiter));
1430     }
1431 
1432     // Resolver
1433     //-----------------------------------------------------------------------
1434     /**
1435      * Gets the VariableResolver that is used to lookup variables.
1436      *
1437      * @return the VariableResolver
1438      */
1439     public StrLookup getVariableResolver() {
1440         return this.variableResolver;
1441     }
1442 
1443     /**
1444      * Sets the VariableResolver that is used to lookup variables.
1445      *
1446      * @param variableResolver  the VariableResolver
1447      */
1448     public void setVariableResolver(final StrLookup variableResolver) {
1449         if (variableResolver instanceof ConfigurationAware && this.configuration != null) {
1450             ((ConfigurationAware) variableResolver).setConfiguration(this.configuration);
1451         }
1452         this.variableResolver = variableResolver;
1453     }
1454 
1455     // Substitution support in variable names
1456     //-----------------------------------------------------------------------
1457     /**
1458      * Returns a flag whether substitution is done in variable names.
1459      *
1460      * @return the substitution in variable names flag
1461      */
1462     public boolean isEnableSubstitutionInVariables() {
1463         return enableSubstitutionInVariables;
1464     }
1465 
1466     /**
1467      * Sets a flag whether substitution is done in variable names. If set to
1468      * <b>true</b>, the names of variables can contain other variables which are
1469      * processed first before the original variable is evaluated, e.g.
1470      * <code>${jre-${java.version}}</code>. The default value is <b>true</b>.
1471      *
1472      * @param enableSubstitutionInVariables the new value of the flag
1473      */
1474     public void setEnableSubstitutionInVariables(final boolean enableSubstitutionInVariables) {
1475         this.enableSubstitutionInVariables = enableSubstitutionInVariables;
1476     }
1477 
1478     boolean isRecursiveEvaluationAllowed() {
1479         return recursiveEvaluationAllowed;
1480     }
1481 
1482     void setRecursiveEvaluationAllowed(final boolean recursiveEvaluationAllowed) {
1483         this.recursiveEvaluationAllowed = recursiveEvaluationAllowed;
1484     }
1485 
1486     private char[] getChars(final StringBuilder sb) {
1487         final char[] chars = new char[sb.length()];
1488         sb.getChars(0, sb.length(), chars, 0);
1489         return chars;
1490     }
1491 
1492     /**
1493      * Appends a iterable placing separators between each value, but
1494      * not before the first or after the last.
1495      * Appending a null iterable will have no effect..
1496      *
1497      * @param sb StringBuilder that contains the String being constructed.
1498      * @param iterable  the iterable to append
1499      * @param separator  the separator to use, null means no separator
1500      */
1501     public void appendWithSeparators(final StringBuilder sb, final Iterable<?> iterable, String separator) {
1502         if (iterable != null) {
1503             separator = separator == null ? Strings.EMPTY : separator;
1504             final Iterator<?> it = iterable.iterator();
1505             while (it.hasNext()) {
1506                 sb.append(it.next());
1507                 if (it.hasNext()) {
1508                     sb.append(separator);
1509                 }
1510             }
1511         }
1512     }
1513 
1514     @Override
1515     public String toString() {
1516         return "StrSubstitutor(" + variableResolver.toString() + ')';
1517     }
1518 
1519     @Override
1520     public void setConfiguration(final Configuration configuration) {
1521         this.configuration = configuration;
1522         if (this.variableResolver instanceof ConfigurationAware) {
1523             ((ConfigurationAware) this.variableResolver).setConfiguration(this.configuration);
1524         }
1525     }
1526 }