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