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