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