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