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.commons.text;
018
019import java.util.ArrayList;
020import java.util.List;
021import java.util.Map;
022import java.util.Properties;
023import java.util.function.Function;
024import java.util.stream.Collectors;
025
026import org.apache.commons.lang3.Validate;
027
028/**
029 * Substitutes variables within a string by values.
030 * <p>
031 * This class takes a piece of text and substitutes all the variables within it.
032 * The default definition of a variable is {@code ${variableName}}.
033 * The prefix and suffix can be changed via constructors and set methods.
034 * <p>
035 * Variable values are typically resolved from a map, but could also be resolved
036 * from system properties, or by supplying a custom variable resolver.
037 * <p>
038 * The simplest example is to use this class to replace Java System properties. For example:
039 * <pre>
040 * StrSubstitutor.replaceSystemProperties(
041 *      "You are running with java.version = ${java.version} and os.name = ${os.name}.");
042 * </pre>
043 * <p>
044 * Typical usage of this class follows the following pattern: First an instance is created
045 * and initialized with the map that contains the values for the available variables.
046 * If a prefix and/or suffix for variables should be used other than the default ones,
047 * the appropriate settings can be performed. After that the {@code replace()}
048 * method can be called passing in the source text for interpolation. In the returned
049 * text all variable references (as long as their values are known) will be resolved.
050 * The following example demonstrates this:
051 * <pre>
052 * Map&lt;String, String&gt; valuesMap = new HashMap&lt;&gt;();
053 * valuesMap.put(&quot;animal&quot;, &quot;quick brown fox&quot;);
054 * valuesMap.put(&quot;target&quot;, &quot;lazy dog&quot;);
055 * String templateString = &quot;The ${animal} jumped over the ${target}.&quot;;
056 * StrSubstitutor sub = new StrSubstitutor(valuesMap);
057 * String resolvedString = sub.replace(templateString);
058 * </pre>
059 * yielding:
060 * <pre>
061 *      The quick brown fox jumped over the lazy dog.
062 * </pre>
063 * <p>
064 * Also, this class allows to set a default value for unresolved variables.
065 * The default value for a variable can be appended to the variable name after the variable
066 * default value delimiter. The default value of the variable default value delimiter is ':-',
067 * as in bash and other *nix shells, as those are arguably where the default ${} delimiter set originated.
068 * The variable default value delimiter can be manually set by calling {@link #setValueDelimiterMatcher(StrMatcher)},
069 * {@link #setValueDelimiter(char)} or {@link #setValueDelimiter(String)}.
070 * The following shows an example with variable default value settings:
071 * <pre>
072 * Map&lt;String, String&gt; valuesMap = new HashMap&lt;&gt;();
073 * valuesMap.put(&quot;animal&quot;, &quot;quick brown fox&quot;);
074 * valuesMap.put(&quot;target&quot;, &quot;lazy dog&quot;);
075 * String templateString = &quot;The ${animal} jumped over the ${target}. ${undefined.number:-1234567890}.&quot;;
076 * StrSubstitutor sub = new StrSubstitutor(valuesMap);
077 * String resolvedString = sub.replace(templateString);
078 * </pre>
079 * yielding:
080 * <pre>
081 *      The quick brown fox jumped over the lazy dog. 1234567890.
082 * </pre>
083 * <p>
084 * In addition to this usage pattern there are some static convenience methods that
085 * cover the most common use cases. These methods can be used without the need of
086 * manually creating an instance. However if multiple replace operations are to be
087 * performed, creating and reusing an instance of this class will be more efficient.
088 * <p>
089 * Variable replacement works in a recursive way. Thus, if a variable value contains
090 * a variable then that variable will also be replaced. Cyclic replacements are
091 * detected and will cause an exception to be thrown.
092 * <p>
093 * Sometimes the interpolation's result must contain a variable prefix. As an example
094 * take the following source text:
095 * <pre>
096 *   The variable ${${name}} must be used.
097 * </pre>
098 * Here only the variable's name referred to in the text should be replaced resulting
099 * in the text (assuming that the value of the {@code name} variable is {@code x}):
100 * <pre>
101 *   The variable ${x} must be used.
102 * </pre>
103 * To achieve this effect there are two possibilities: Either set a different prefix
104 * and suffix for variables which do not conflict with the result text you want to
105 * produce. The other possibility is to use the escape character, by default '$'.
106 * If this character is placed before a variable reference, this reference is ignored
107 * and won't be replaced. For example:
108 * <pre>
109 *   The variable $${${name}} must be used.
110 * </pre>
111 * <p>
112 * In some complex scenarios you might even want to perform substitution in the
113 * names of variables, for instance
114 * <pre>
115 * ${jre-${java.specification.version}}
116 * </pre>
117 * {@code StrSubstitutor} supports this recursive substitution in variable
118 * names, but it has to be enabled explicitly by setting the
119 * {@link #setEnableSubstitutionInVariables(boolean) enableSubstitutionInVariables}
120 * property to <b>true</b>.
121 * <p>This class is <b>not</b> thread safe.</p>
122 *
123 * @since 1.0
124 * @deprecated Deprecated as of 1.3, use {@link StringSubstitutor} instead. This class will be removed in 2.0.
125 */
126@Deprecated
127public class StrSubstitutor {
128
129    /**
130     * Constant for the default escape character.
131     */
132    public static final char DEFAULT_ESCAPE = '$';
133
134    /**
135     * Constant for the default variable prefix.
136     */
137    public static final StrMatcher DEFAULT_PREFIX = StrMatcher.stringMatcher("${");
138
139    /**
140     * Constant for the default variable suffix.
141     */
142    public static final StrMatcher DEFAULT_SUFFIX = StrMatcher.stringMatcher("}");
143
144    /**
145     * Constant for the default value delimiter of a variable.
146     */
147    public static final StrMatcher DEFAULT_VALUE_DELIMITER = StrMatcher.stringMatcher(":-");
148
149    /**
150     * Replaces all the occurrences of variables in the given source object with
151     * their matching values from the map.
152     *
153     * @param <V> the type of the values in the map
154     * @param source  the source text containing the variables to substitute, null returns null
155     * @param valueMap  the map with the values, may be null
156     * @return The result of the replace operation
157     */
158    public static <V> String replace(final Object source, final Map<String, V> valueMap) {
159        return new StrSubstitutor(valueMap).replace(source);
160    }
161
162    /**
163     * Replaces all the occurrences of variables in the given source object with
164     * their matching values from the map. This method allows to specify a
165     * custom variable prefix and suffix
166     *
167     * @param <V> the type of the values in the map
168     * @param source  the source text containing the variables to substitute, null returns null
169     * @param valueMap  the map with the values, may be null
170     * @param prefix  the prefix of variables, not null
171     * @param suffix  the suffix of variables, not null
172     * @return The result of the replace operation
173     * @throws IllegalArgumentException if the prefix or suffix is null
174     */
175    public static <V> String replace(final Object source,
176                                     final Map<String, V> valueMap,
177                                     final String prefix,
178                                     final String suffix) {
179        return new StrSubstitutor(valueMap, prefix, suffix).replace(source);
180    }
181
182    /**
183     * Replaces all the occurrences of variables in the given source object with their matching
184     * values from the properties.
185     *
186     * @param source the source text containing the variables to substitute, null returns null
187     * @param valueProperties the properties with values, may be null
188     * @return The result of the replace operation
189     */
190    public static String replace(final Object source, final Properties valueProperties) {
191        if (valueProperties == null) {
192            return source.toString();
193        }
194        return StrSubstitutor.replace(source,
195                valueProperties.stringPropertyNames().stream().collect(Collectors.toMap(Function.identity(), valueProperties::getProperty)));
196    }
197
198    /**
199     * Replaces all the occurrences of variables in the given source object with
200     * their matching values from the system properties.
201     *
202     * @param source  the source text containing the variables to substitute, null returns null
203     * @return The result of the replace operation
204     */
205    public static String replaceSystemProperties(final Object source) {
206        return new StrSubstitutor(StrLookup.systemPropertiesLookup()).replace(source);
207    }
208
209    /**
210     * Stores the escape character.
211     */
212    private char escapeChar;
213
214    /**
215     * Stores the variable prefix.
216     */
217    private StrMatcher prefixMatcher;
218
219    /**
220     * Stores the variable suffix.
221     */
222    private StrMatcher suffixMatcher;
223
224    /**
225     * Stores the default variable value delimiter.
226     */
227    private StrMatcher valueDelimiterMatcher;
228
229    /**
230     * Variable resolution is delegated to an implementor of VariableResolver.
231     */
232    private StrLookup<?> variableResolver;
233
234    /**
235     * The flag whether substitution in variable names is enabled.
236     */
237    private boolean enableSubstitutionInVariables;
238
239    /**
240     * Whether escapes should be preserved.  Default is false;
241     */
242    private boolean preserveEscapes;
243
244    /**
245     * The flag whether substitution in variable values is disabled.
246     */
247    private boolean disableSubstitutionInValues;
248
249    /**
250     * Constructs a new instance with defaults for variable prefix and suffix
251     * and the escaping character.
252     */
253    public StrSubstitutor() {
254        this((StrLookup<?>) null, DEFAULT_PREFIX, DEFAULT_SUFFIX, DEFAULT_ESCAPE);
255    }
256
257    /**
258     * Constructs a new instance and initializes it. Uses defaults for variable
259     * prefix and suffix and the escaping character.
260     *
261     * @param <V> the type of the values in the map
262     * @param valueMap  the map with the variables' values, may be null
263     */
264    public <V> StrSubstitutor(final Map<String, V> valueMap) {
265        this(StrLookup.mapLookup(valueMap), DEFAULT_PREFIX, DEFAULT_SUFFIX, DEFAULT_ESCAPE);
266    }
267
268    /**
269     * Constructs a new instance and initializes it. Uses a default escaping character.
270     *
271     * @param <V> the type of the values in the map
272     * @param valueMap  the map with the variables' values, may be null
273     * @param prefix  the prefix for variables, not null
274     * @param suffix  the suffix for variables, not null
275     * @throws IllegalArgumentException if the prefix or suffix is null
276     */
277    public <V> StrSubstitutor(final Map<String, V> valueMap, final String prefix, final String suffix) {
278        this(StrLookup.mapLookup(valueMap), prefix, suffix, DEFAULT_ESCAPE);
279    }
280
281    /**
282     * Constructs a new instance and initializes it.
283     *
284     * @param <V> the type of the values in the map
285     * @param valueMap  the map with the variables' values, may be null
286     * @param prefix  the prefix for variables, not null
287     * @param suffix  the suffix for variables, not null
288     * @param escape  the escape character
289     * @throws IllegalArgumentException if the prefix or suffix is null
290     */
291    public <V> StrSubstitutor(final Map<String, V> valueMap, final String prefix, final String suffix,
292                              final char escape) {
293        this(StrLookup.mapLookup(valueMap), prefix, suffix, escape);
294    }
295
296    /**
297     * Constructs a new instance and initializes it.
298     *
299     * @param <V> the type of the values in the map
300     * @param valueMap  the map with the variables' values, may be null
301     * @param prefix  the prefix for variables, not null
302     * @param suffix  the suffix for variables, not null
303     * @param escape  the escape character
304     * @param valueDelimiter  the variable default value delimiter, may be null
305     * @throws IllegalArgumentException if the prefix or suffix is null
306     */
307    public <V> StrSubstitutor(final Map<String, V> valueMap, final String prefix, final String suffix,
308                              final char escape, final String valueDelimiter) {
309        this(StrLookup.mapLookup(valueMap), prefix, suffix, escape, valueDelimiter);
310    }
311
312    /**
313     * Constructs a new instance and initializes it.
314     *
315     * @param variableResolver  the variable resolver, may be null
316     */
317    public StrSubstitutor(final StrLookup<?> variableResolver) {
318        this(variableResolver, DEFAULT_PREFIX, DEFAULT_SUFFIX, DEFAULT_ESCAPE);
319    }
320
321    /**
322     * Constructs a new instance and initializes it.
323     *
324     * @param variableResolver  the variable resolver, may be null
325     * @param prefix  the prefix for variables, not null
326     * @param suffix  the suffix for variables, not null
327     * @param escape  the escape character
328     * @throws IllegalArgumentException if the prefix or suffix is null
329     */
330    public StrSubstitutor(final StrLookup<?> variableResolver, final String prefix, final String suffix,
331                          final char escape) {
332        this.setVariableResolver(variableResolver);
333        this.setVariablePrefix(prefix);
334        this.setVariableSuffix(suffix);
335        this.setEscapeChar(escape);
336        this.setValueDelimiterMatcher(DEFAULT_VALUE_DELIMITER);
337    }
338
339    /**
340     * Constructs a new instance and initializes it.
341     *
342     * @param variableResolver  the variable resolver, may be null
343     * @param prefix  the prefix for variables, not null
344     * @param suffix  the suffix for variables, not null
345     * @param escape  the escape character
346     * @param valueDelimiter  the variable default value delimiter string, may be null
347     * @throws IllegalArgumentException if the prefix or suffix is null
348     */
349    public StrSubstitutor(final StrLookup<?> variableResolver, final String prefix, final String suffix,
350                          final char escape, final String valueDelimiter) {
351        this.setVariableResolver(variableResolver);
352        this.setVariablePrefix(prefix);
353        this.setVariableSuffix(suffix);
354        this.setEscapeChar(escape);
355        this.setValueDelimiter(valueDelimiter);
356    }
357
358    /**
359     * Constructs a new instance and initializes it.
360     *
361     * @param variableResolver  the variable resolver, may be null
362     * @param prefixMatcher  the prefix for variables, not null
363     * @param suffixMatcher  the suffix for variables, not null
364     * @param escape  the escape character
365     * @throws IllegalArgumentException if the prefix or suffix is null
366     */
367    public StrSubstitutor(
368            final StrLookup<?> variableResolver, final StrMatcher prefixMatcher, final StrMatcher suffixMatcher,
369            final char escape) {
370        this(variableResolver, prefixMatcher, suffixMatcher, escape, DEFAULT_VALUE_DELIMITER);
371    }
372
373    /**
374     * Constructs a new instance and initializes it.
375     *
376     * @param variableResolver  the variable resolver, may be null
377     * @param prefixMatcher  the prefix for variables, not null
378     * @param suffixMatcher  the suffix for variables, not null
379     * @param escape  the escape character
380     * @param valueDelimiterMatcher  the variable default value delimiter matcher, may be null
381     * @throws IllegalArgumentException if the prefix or suffix is null
382     */
383    public StrSubstitutor(
384            final StrLookup<?> variableResolver, final StrMatcher prefixMatcher, final StrMatcher suffixMatcher,
385            final char escape, final StrMatcher valueDelimiterMatcher) {
386        this.setVariableResolver(variableResolver);
387        this.setVariablePrefixMatcher(prefixMatcher);
388        this.setVariableSuffixMatcher(suffixMatcher);
389        this.setEscapeChar(escape);
390        this.setValueDelimiterMatcher(valueDelimiterMatcher);
391    }
392
393    /**
394     * Checks if the specified variable is already in the stack (list) of variables.
395     *
396     * @param varName  the variable name to check
397     * @param priorVariables  the list of prior variables
398     */
399    private void checkCyclicSubstitution(final String varName, final List<String> priorVariables) {
400        if (!priorVariables.contains(varName)) {
401            return;
402        }
403        final StrBuilder buf = new StrBuilder(256);
404        buf.append("Infinite loop in property interpolation of ");
405        buf.append(priorVariables.remove(0));
406        buf.append(": ");
407        buf.appendWithSeparators(priorVariables, "->");
408        throw new IllegalStateException(buf.toString());
409    }
410
411    /**
412     * Returns the escape character.
413     *
414     * @return The character used for escaping variable references
415     */
416    public char getEscapeChar() {
417        return this.escapeChar;
418    }
419
420    /**
421     * Gets the variable default value delimiter matcher currently in use.
422     * <p>
423     * The variable default value delimiter is the character or characters that delimit the
424     * variable name and the variable default value. This delimiter is expressed in terms of a matcher
425     * allowing advanced variable default value delimiter matches.
426     * </p>
427     * <p>
428     * If it returns null, then the variable default value resolution is disabled.
429     * </p>
430     *
431     * @return The variable default value delimiter matcher in use, may be null
432     */
433    public StrMatcher getValueDelimiterMatcher() {
434        return valueDelimiterMatcher;
435    }
436
437    /**
438     * Gets the variable prefix matcher currently in use.
439     * <p>
440     * The variable prefix is the character or characters that identify the
441     * start of a variable. This prefix is expressed in terms of a matcher
442     * allowing advanced prefix matches.
443     * </p>
444     *
445     * @return The prefix matcher in use
446     */
447    public StrMatcher getVariablePrefixMatcher() {
448        return prefixMatcher;
449    }
450
451    /**
452     * Gets the VariableResolver that is used to lookup variables.
453     *
454     * @return The VariableResolver
455     */
456    public StrLookup<?> getVariableResolver() {
457        return this.variableResolver;
458    }
459
460    /**
461     * Gets the variable suffix matcher currently in use.
462     * <p>
463     * The variable suffix is the character or characters that identify the
464     * end of a variable. This suffix is expressed in terms of a matcher
465     * allowing advanced suffix matches.
466     * </p>
467     *
468     * @return The suffix matcher in use
469     */
470    public StrMatcher getVariableSuffixMatcher() {
471        return suffixMatcher;
472    }
473
474    /**
475     * Returns a flag whether substitution is disabled in variable values.If set to
476     * <b>true</b>, the values of variables can contain other variables will not be
477     * processed and substituted original variable is evaluated, e.g.
478     * <pre>
479     * Map&lt;String, String&gt; valuesMap = new HashMap&lt;&gt;();
480     * valuesMap.put(&quot;name&quot;, &quot;Douglas ${surname}&quot;);
481     * valuesMap.put(&quot;surname&quot;, &quot;Crockford&quot;);
482     * String templateString = &quot;Hi ${name}&quot;;
483     * StrSubstitutor sub = new StrSubstitutor(valuesMap);
484     * String resolvedString = sub.replace(templateString);
485     * </pre>
486     * yielding:
487     * <pre>
488     *      Hi Douglas ${surname}
489     * </pre>
490     *
491     * @return The substitution in variable values flag
492     *
493     * @since 1.2
494     */
495    public boolean isDisableSubstitutionInValues() {
496        return disableSubstitutionInValues;
497    }
498
499    /**
500     * Returns a flag whether substitution is done in variable names.
501     *
502     * @return The substitution in variable names flag
503     */
504    public boolean isEnableSubstitutionInVariables() {
505        return enableSubstitutionInVariables;
506    }
507
508    /**
509     * Returns the flag controlling whether escapes are preserved during
510     * substitution.
511     *
512     * @return The preserve escape flag
513     */
514    public boolean isPreserveEscapes() {
515        return preserveEscapes;
516    }
517
518    /**
519     * Replaces all the occurrences of variables with their matching values
520     * from the resolver using the given source array as a template.
521     * The array is not altered by this method.
522     *
523     * @param source  the character array to replace in, not altered, null returns null
524     * @return The result of the replace operation
525     */
526    public String replace(final char[] source) {
527        if (source == null) {
528            return null;
529        }
530        final StrBuilder buf = new StrBuilder(source.length).append(source);
531        substitute(buf, 0, source.length);
532        return buf.toString();
533    }
534
535    /**
536     * Replaces all the occurrences of variables with their matching values
537     * from the resolver using the given source array as a template.
538     * The array is not altered by this method.
539     * <p>
540     * Only the specified portion of the array will be processed.
541     * The rest of the array is not processed, and is not returned.
542     * </p>
543     *
544     * @param source  the character array to replace in, not altered, null returns null
545     * @param offset  the start offset within the array, must be valid
546     * @param length  the length within the array to be processed, must be valid
547     * @return The result of the replace operation
548     */
549    public String replace(final char[] source, final int offset, final int length) {
550        if (source == null) {
551            return null;
552        }
553        final StrBuilder buf = new StrBuilder(length).append(source, offset, length);
554        substitute(buf, 0, length);
555        return buf.toString();
556    }
557
558    /**
559     * Replaces all the occurrences of variables with their matching values
560     * from the resolver using the given source as a template.
561     * The source is not altered by this method.
562     *
563     * @param source  the buffer to use as a template, not changed, null returns null
564     * @return The result of the replace operation
565     */
566    public String replace(final CharSequence source) {
567        if (source == null) {
568            return null;
569        }
570        return replace(source, 0, source.length());
571    }
572
573    /**
574     * Replaces all the occurrences of variables with their matching values
575     * from the resolver using the given source as a template.
576     * The source is not altered by this method.
577     * <p>
578     * Only the specified portion of the buffer will be processed.
579     * The rest of the buffer is not processed, and is not returned.
580     * </p>
581     *
582     * @param source  the buffer to use as a template, not changed, null returns null
583     * @param offset  the start offset within the array, must be valid
584     * @param length  the length within the array to be processed, must be valid
585     * @return The result of the replace operation
586     */
587    public String replace(final CharSequence source, final int offset, final int length) {
588        if (source == null) {
589            return null;
590        }
591        final StrBuilder buf = new StrBuilder(length).append(source, offset, length);
592        substitute(buf, 0, length);
593        return buf.toString();
594    }
595
596    /**
597     * Replaces all the occurrences of variables in the given source object with
598     * their matching values from the resolver. The input source object is
599     * converted to a string using {@code toString} and is not altered.
600     *
601     * @param source  the source to replace in, null returns null
602     * @return The result of the replace operation
603     */
604    public String replace(final Object source) {
605        if (source == null) {
606            return null;
607        }
608        final StrBuilder buf = new StrBuilder().append(source);
609        substitute(buf, 0, buf.length());
610        return buf.toString();
611    }
612
613    /**
614     * Replaces all the occurrences of variables with their matching values
615     * from the resolver using the given source builder as a template.
616     * The builder is not altered by this method.
617     *
618     * @param source  the builder to use as a template, not changed, null returns null
619     * @return The result of the replace operation
620     */
621    public String replace(final StrBuilder source) {
622        if (source == null) {
623            return null;
624        }
625        final StrBuilder buf = new StrBuilder(source.length()).append(source);
626        substitute(buf, 0, buf.length());
627        return buf.toString();
628    }
629
630    /**
631     * Replaces all the occurrences of variables with their matching values
632     * from the resolver using the given source builder as a template.
633     * The builder is not altered by this method.
634     * <p>
635     * Only the specified portion of the builder will be processed.
636     * The rest of the builder is not processed, and is not returned.
637     * </p>
638     *
639     * @param source  the builder to use as a template, not changed, null returns null
640     * @param offset  the start offset within the array, must be valid
641     * @param length  the length within the array to be processed, must be valid
642     * @return The result of the replace operation
643     */
644    public String replace(final StrBuilder source, final int offset, final int length) {
645        if (source == null) {
646            return null;
647        }
648        final StrBuilder buf = new StrBuilder(length).append(source, offset, length);
649        substitute(buf, 0, length);
650        return buf.toString();
651    }
652
653    /**
654     * Replaces all the occurrences of variables with their matching values
655     * from the resolver using the given source string as a template.
656     *
657     * @param source  the string to replace in, null returns null
658     * @return The result of the replace operation
659     */
660    public String replace(final String source) {
661        if (source == null) {
662            return null;
663        }
664        final StrBuilder buf = new StrBuilder(source);
665        if (!substitute(buf, 0, source.length())) {
666            return source;
667        }
668        return buf.toString();
669    }
670
671    /**
672     * Replaces all the occurrences of variables with their matching values
673     * from the resolver using the given source string as a template.
674     * <p>
675     * Only the specified portion of the string will be processed.
676     * The rest of the string is not processed, and is not returned.
677     * </p>
678     *
679     * @param source  the string to replace in, null returns null
680     * @param offset  the start offset within the array, must be valid
681     * @param length  the length within the array to be processed, must be valid
682     * @return The result of the replace operation
683     */
684    public String replace(final String source, final int offset, final int length) {
685        if (source == null) {
686            return null;
687        }
688        final StrBuilder buf = new StrBuilder(length).append(source, offset, length);
689        if (!substitute(buf, 0, length)) {
690            return source.substring(offset, offset + length);
691        }
692        return buf.toString();
693    }
694
695    /**
696     * Replaces all the occurrences of variables with their matching values
697     * from the resolver using the given source buffer as a template.
698     * The buffer is not altered by this method.
699     *
700     * @param source  the buffer to use as a template, not changed, null returns null
701     * @return The result of the replace operation
702     */
703    public String replace(final StringBuffer source) {
704        if (source == null) {
705            return null;
706        }
707        final StrBuilder buf = new StrBuilder(source.length()).append(source);
708        substitute(buf, 0, buf.length());
709        return buf.toString();
710    }
711
712    /**
713     * Replaces all the occurrences of variables with their matching values
714     * from the resolver using the given source buffer as a template.
715     * The buffer is not altered by this method.
716     * <p>
717     * Only the specified portion of the buffer will be processed.
718     * The rest of the buffer is not processed, and is not returned.
719     * </p>
720     *
721     * @param source  the buffer to use as a template, not changed, null returns null
722     * @param offset  the start offset within the array, must be valid
723     * @param length  the length within the array to be processed, must be valid
724     * @return The result of the replace operation
725     */
726    public String replace(final StringBuffer source, final int offset, final int length) {
727        if (source == null) {
728            return null;
729        }
730        final StrBuilder buf = new StrBuilder(length).append(source, offset, length);
731        substitute(buf, 0, length);
732        return buf.toString();
733    }
734
735    /**
736     * Replaces all the occurrences of variables within the given source
737     * builder with their matching values from the resolver.
738     *
739     * @param source  the builder to replace in, updated, null returns zero
740     * @return true if altered
741     */
742    public boolean replaceIn(final StrBuilder source) {
743        if (source == null) {
744            return false;
745        }
746        return substitute(source, 0, source.length());
747    }
748
749    /**
750     * Replaces all the occurrences of variables within the given source
751     * builder with their matching values from the resolver.
752     * <p>
753     * Only the specified portion of the builder will be processed.
754     * The rest of the builder is not processed, but it is not deleted.
755     * </p>
756     *
757     * @param source  the builder to replace in, null returns zero
758     * @param offset  the start offset within the array, must be valid
759     * @param length  the length within the builder to be processed, must be valid
760     * @return true if altered
761     */
762    public boolean replaceIn(final StrBuilder source, final int offset, final int length) {
763        if (source == null) {
764            return false;
765        }
766        return substitute(source, offset, length);
767    }
768
769    /**
770     * Replaces all the occurrences of variables within the given source buffer
771     * with their matching values from the resolver.
772     * The buffer is updated with the result.
773     *
774     * @param source  the buffer to replace in, updated, null returns zero
775     * @return true if altered
776     */
777    public boolean replaceIn(final StringBuffer source) {
778        if (source == null) {
779            return false;
780        }
781        return replaceIn(source, 0, source.length());
782    }
783
784    /**
785     * Replaces all the occurrences of variables within the given source buffer
786     * with their matching values from the resolver.
787     * The buffer is updated with the result.
788     * <p>
789     * Only the specified portion of the buffer will be processed.
790     * The rest of the buffer is not processed, but it is not deleted.
791     * </p>
792     *
793     * @param source  the buffer to replace in, updated, null returns zero
794     * @param offset  the start offset within the array, must be valid
795     * @param length  the length within the buffer to be processed, must be valid
796     * @return true if altered
797     */
798    public boolean replaceIn(final StringBuffer source, final int offset, final int length) {
799        if (source == null) {
800            return false;
801        }
802        final StrBuilder buf = new StrBuilder(length).append(source, offset, length);
803        if (!substitute(buf, 0, length)) {
804            return false;
805        }
806        source.replace(offset, offset + length, buf.toString());
807        return true;
808    }
809
810    /**
811     * Replaces all the occurrences of variables within the given source buffer
812     * with their matching values from the resolver.
813     * The buffer is updated with the result.
814     *
815     * @param source  the buffer to replace in, updated, null returns zero
816     * @return true if altered
817     */
818    public boolean replaceIn(final StringBuilder source) {
819        if (source == null) {
820            return false;
821        }
822        return replaceIn(source, 0, source.length());
823    }
824
825    /**
826     * Replaces all the occurrences of variables within the given source builder
827     * with their matching values from the resolver.
828     * The builder is updated with the result.
829     * <p>
830     * Only the specified portion of the buffer will be processed.
831     * The rest of the buffer is not processed, but it is not deleted.
832     * </p>
833     *
834     * @param source  the buffer to replace in, updated, null returns zero
835     * @param offset  the start offset within the array, must be valid
836     * @param length  the length within the buffer to be processed, must be valid
837     * @return true if altered
838     */
839    public boolean replaceIn(final StringBuilder source, final int offset, final int length) {
840        if (source == null) {
841            return false;
842        }
843        final StrBuilder buf = new StrBuilder(length).append(source, offset, length);
844        if (!substitute(buf, 0, length)) {
845            return false;
846        }
847        source.replace(offset, offset + length, buf.toString());
848        return true;
849    }
850
851    /**
852     * Internal method that resolves the value of a variable.
853     * <p>
854     * Most users of this class do not need to call this method. This method is
855     * called automatically by the substitution process.
856     * </p>
857     * <p>
858     * Writers of subclasses can override this method if they need to alter
859     * how each substitution occurs. The method is passed the variable's name
860     * and must return the corresponding value. This implementation uses the
861     * {@link #getVariableResolver()} with the variable's name as the key.
862     * </p>
863     *
864     * @param variableName  the name of the variable, not null
865     * @param buf  the buffer where the substitution is occurring, not null
866     * @param startPos  the start position of the variable including the prefix, valid
867     * @param endPos  the end position of the variable including the suffix, valid
868     * @return The variable's value or <b>null</b> if the variable is unknown
869     */
870    protected String resolveVariable(final String variableName,
871                                     final StrBuilder buf,
872                                     final int startPos,
873                                     final int endPos) {
874        final StrLookup<?> resolver = getVariableResolver();
875        if (resolver == null) {
876            return null;
877        }
878        return resolver.lookup(variableName);
879    }
880
881    /**
882     * Sets a flag whether substitution is done in variable values (recursive).
883     *
884     * @param disableSubstitutionInValues true if substitution in variable value are disabled
885     *
886     * @since 1.2
887     */
888    public void setDisableSubstitutionInValues(final boolean disableSubstitutionInValues) {
889        this.disableSubstitutionInValues = disableSubstitutionInValues;
890    }
891
892    /**
893     * Sets a flag whether substitution is done in variable names. If set to
894     * <b>true</b>, the names of variables can contain other variables which are
895     * processed first before the original variable is evaluated, e.g.
896     * {@code ${jre-${java.version}}}. The default value is <b>false</b>.
897     *
898     * @param enableSubstitutionInVariables the new value of the flag
899     */
900    public void setEnableSubstitutionInVariables(
901            final boolean enableSubstitutionInVariables) {
902        this.enableSubstitutionInVariables = enableSubstitutionInVariables;
903    }
904
905    /**
906     * Sets the escape character.
907     * If this character is placed before a variable reference in the source
908     * text, this variable will be ignored.
909     *
910     * @param escapeCharacter  the escape character (0 for disabling escaping)
911     */
912    public void setEscapeChar(final char escapeCharacter) {
913        this.escapeChar = escapeCharacter;
914    }
915
916    /**
917     * Sets a flag controlling whether escapes are preserved during
918     * substitution.  If set to <b>true</b>, the escape character is retained
919     * during substitution (e.g. {@code $${this-is-escaped}} remains
920     * {@code $${this-is-escaped}}).  If set to <b>false</b>, the escape
921     * character is removed during substitution (e.g.
922     * {@code $${this-is-escaped}} becomes
923     * {@code ${this-is-escaped}}).  The default value is <b>false</b>
924     *
925     * @param preserveEscapes true if escapes are to be preserved
926     */
927    public void setPreserveEscapes(final boolean preserveEscapes) {
928        this.preserveEscapes = preserveEscapes;
929    }
930
931    /**
932     * Sets the variable default value delimiter to use.
933     * <p>
934     * The variable default value delimiter is the character or characters that delimit the
935     * variable name and the variable default value. This method allows a single character
936     * variable default value delimiter to be easily set.
937     * </p>
938     *
939     * @param valueDelimiter  the variable default value delimiter character to use
940     * @return this, to enable chaining
941     */
942    public StrSubstitutor setValueDelimiter(final char valueDelimiter) {
943        return setValueDelimiterMatcher(StrMatcher.charMatcher(valueDelimiter));
944    }
945
946    /**
947     * Sets the variable default value delimiter to use.
948     * <p>
949     * The variable default value delimiter is the character or characters that delimit the
950     * variable name and the variable default value. This method allows a string
951     * variable default value delimiter to be easily set.
952     * </p>
953     * <p>
954     * If the {@code valueDelimiter} is null or empty string, then the variable default
955     * value resolution becomes disabled.
956     * </p>
957     *
958     * @param valueDelimiter  the variable default value delimiter string to use, may be null or empty
959     * @return this, to enable chaining
960     */
961    public StrSubstitutor setValueDelimiter(final String valueDelimiter) {
962        if (valueDelimiter == null || valueDelimiter.isEmpty()) {
963            setValueDelimiterMatcher(null);
964            return this;
965        }
966        return setValueDelimiterMatcher(StrMatcher.stringMatcher(valueDelimiter));
967    }
968
969    /**
970     * Sets the variable default value delimiter matcher to use.
971     * <p>
972     * The variable default value delimiter is the character or characters that delimit the
973     * variable name and the variable default value. This delimiter is expressed in terms of a matcher
974     * allowing advanced variable default value delimiter matches.
975     * </p>
976     * <p>
977     * If the {@code valueDelimiterMatcher} is null, then the variable default value resolution
978     * becomes disabled.
979     * </p>
980     *
981     * @param valueDelimiterMatcher  variable default value delimiter matcher to use, may be null
982     * @return this, to enable chaining
983     */
984    public StrSubstitutor setValueDelimiterMatcher(final StrMatcher valueDelimiterMatcher) {
985        this.valueDelimiterMatcher = valueDelimiterMatcher;
986        return this;
987    }
988
989    /**
990     * Sets the variable prefix to use.
991     * <p>
992     * The variable prefix is the character or characters that identify the
993     * start of a variable. This method allows a single character prefix to
994     * be easily set.
995     * </p>
996     *
997     * @param prefix  the prefix character to use
998     * @return this, to enable chaining
999     */
1000    public StrSubstitutor setVariablePrefix(final char prefix) {
1001        return setVariablePrefixMatcher(StrMatcher.charMatcher(prefix));
1002    }
1003
1004    /**
1005     * Sets the variable prefix to use.
1006     * <p>
1007     * The variable prefix is the character or characters that identify the
1008     * start of a variable. This method allows a string prefix to be easily set.
1009     * </p>
1010     *
1011     * @param prefix  the prefix for variables, not null
1012     * @return this, to enable chaining
1013     * @throws IllegalArgumentException if the prefix is null
1014     */
1015    public StrSubstitutor setVariablePrefix(final String prefix) {
1016        Validate.isTrue(prefix != null, "Variable prefix must not be null!");
1017        return setVariablePrefixMatcher(StrMatcher.stringMatcher(prefix));
1018    }
1019
1020    /**
1021     * Sets the variable prefix matcher currently in use.
1022     * <p>
1023     * The variable prefix is the character or characters that identify the
1024     * start of a variable. This prefix is expressed in terms of a matcher
1025     * allowing advanced prefix matches.
1026     * </p>
1027     *
1028     * @param prefixMatcher  the prefix matcher to use, null ignored
1029     * @return this, to enable chaining
1030     * @throws IllegalArgumentException if the prefix matcher is null
1031     */
1032    public StrSubstitutor setVariablePrefixMatcher(final StrMatcher prefixMatcher) {
1033        Validate.isTrue(prefixMatcher != null, "Variable prefix matcher must not be null!");
1034        this.prefixMatcher = prefixMatcher;
1035        return this;
1036    }
1037
1038    /**
1039     * Sets the VariableResolver that is used to lookup variables.
1040     *
1041     * @param variableResolver  the VariableResolver
1042     */
1043    public void setVariableResolver(final StrLookup<?> variableResolver) {
1044        this.variableResolver = variableResolver;
1045    }
1046
1047    /**
1048     * Sets the variable suffix to use.
1049     * <p>
1050     * The variable suffix is the character or characters that identify the
1051     * end of a variable. This method allows a single character suffix to
1052     * be easily set.
1053     * </p>
1054     *
1055     * @param suffix  the suffix character to use
1056     * @return this, to enable chaining
1057     */
1058    public StrSubstitutor setVariableSuffix(final char suffix) {
1059        return setVariableSuffixMatcher(StrMatcher.charMatcher(suffix));
1060    }
1061
1062    /**
1063     * Sets the variable suffix to use.
1064     * <p>
1065     * The variable suffix is the character or characters that identify the
1066     * end of a variable. This method allows a string suffix to be easily set.
1067     * </p>
1068     *
1069     * @param suffix  the suffix for variables, not null
1070     * @return this, to enable chaining
1071     * @throws IllegalArgumentException if the suffix is null
1072     */
1073    public StrSubstitutor setVariableSuffix(final String suffix) {
1074        Validate.isTrue(suffix != null, "Variable suffix must not be null!");
1075        return setVariableSuffixMatcher(StrMatcher.stringMatcher(suffix));
1076    }
1077
1078    /**
1079     * Sets the variable suffix matcher currently in use.
1080     * <p>
1081     * The variable suffix is the character or characters that identify the
1082     * end of a variable. This suffix is expressed in terms of a matcher
1083     * allowing advanced suffix matches.
1084     * </p>
1085     *
1086     * @param suffixMatcher  the suffix matcher to use, null ignored
1087     * @return this, to enable chaining
1088     * @throws IllegalArgumentException if the suffix matcher is null
1089     */
1090    public StrSubstitutor setVariableSuffixMatcher(final StrMatcher suffixMatcher) {
1091        Validate.isTrue(suffixMatcher != null, "Variable suffix matcher must not be null!");
1092        this.suffixMatcher = suffixMatcher;
1093        return this;
1094    }
1095
1096    /**
1097     * Internal method that substitutes the variables.
1098     * <p>
1099     * Most users of this class do not need to call this method. This method will
1100     * be called automatically by another (public) method.
1101     * </p>
1102     * <p>
1103     * Writers of subclasses can override this method if they need access to
1104     * the substitution process at the start or end.
1105     * </p>
1106     *
1107     * @param buf  the string builder to substitute into, not null
1108     * @param offset  the start offset within the builder, must be valid
1109     * @param length  the length within the builder to be processed, must be valid
1110     * @return true if altered
1111     */
1112    protected boolean substitute(final StrBuilder buf, final int offset, final int length) {
1113        return substitute(buf, offset, length, null) > 0;
1114    }
1115
1116    /**
1117     * Recursive handler for multiple levels of interpolation. This is the main
1118     * interpolation method, which resolves the values of all variable references
1119     * contained in the passed in text.
1120     *
1121     * @param buf  the string builder to substitute into, not null
1122     * @param offset  the start offset within the builder, must be valid
1123     * @param length  the length within the builder to be processed, must be valid
1124     * @param priorVariables  the stack keeping track of the replaced variables, may be null
1125     * @return The length change that occurs, unless priorVariables is null when the int
1126     *  represents a boolean flag as to whether any change occurred.
1127     */
1128    private int substitute(final StrBuilder buf, final int offset, final int length, List<String> priorVariables) {
1129        final StrMatcher pfxMatcher = getVariablePrefixMatcher();
1130        final StrMatcher suffMatcher = getVariableSuffixMatcher();
1131        final char escape = getEscapeChar();
1132        final StrMatcher valueDelimMatcher = getValueDelimiterMatcher();
1133        final boolean substitutionInVariablesEnabled = isEnableSubstitutionInVariables();
1134        final boolean substitutionInValuesDisabled = isDisableSubstitutionInValues();
1135
1136        final boolean top = priorVariables == null;
1137        boolean altered = false;
1138        int lengthChange = 0;
1139        char[] chars = buf.buffer;
1140        int bufEnd = offset + length;
1141        int pos = offset;
1142        while (pos < bufEnd) {
1143            final int startMatchLen = pfxMatcher.isMatch(chars, pos, offset,
1144                    bufEnd);
1145            if (startMatchLen == 0) {
1146                pos++;
1147            } else {
1148                // found variable start marker
1149                if (pos > offset && chars[pos - 1] == escape) {
1150                    // escaped
1151                    if (preserveEscapes) {
1152                        pos++;
1153                        continue;
1154                    }
1155                    buf.deleteCharAt(pos - 1);
1156                    chars = buf.buffer; // in case buffer was altered
1157                    lengthChange--;
1158                    altered = true;
1159                    bufEnd--;
1160                } else {
1161                    // find suffix
1162                    final int startPos = pos;
1163                    pos += startMatchLen;
1164                    int endMatchLen = 0;
1165                    int nestedVarCount = 0;
1166                    while (pos < bufEnd) {
1167                        if (substitutionInVariablesEnabled
1168                                && pfxMatcher.isMatch(chars,
1169                                        pos, offset, bufEnd) != 0) {
1170                            // found a nested variable start
1171                            endMatchLen = pfxMatcher.isMatch(chars,
1172                                    pos, offset, bufEnd);
1173                            nestedVarCount++;
1174                            pos += endMatchLen;
1175                            continue;
1176                        }
1177
1178                        endMatchLen = suffMatcher.isMatch(chars, pos, offset,
1179                                bufEnd);
1180                        if (endMatchLen == 0) {
1181                            pos++;
1182                        } else {
1183                            // found variable end marker
1184                            if (nestedVarCount == 0) {
1185                                String varNameExpr = new String(chars, startPos
1186                                        + startMatchLen, pos - startPos
1187                                        - startMatchLen);
1188                                if (substitutionInVariablesEnabled) {
1189                                    final StrBuilder bufName = new StrBuilder(varNameExpr);
1190                                    substitute(bufName, 0, bufName.length());
1191                                    varNameExpr = bufName.toString();
1192                                }
1193                                pos += endMatchLen;
1194                                final int endPos = pos;
1195
1196                                String varName = varNameExpr;
1197                                String varDefaultValue = null;
1198
1199                                if (valueDelimMatcher != null) {
1200                                    final char[] varNameExprChars = varNameExpr.toCharArray();
1201                                    int valueDelimiterMatchLen = 0;
1202                                    for (int i = 0; i < varNameExprChars.length; i++) {
1203                                        // if there's any nested variable when nested variable substitution disabled,
1204                                        // then stop resolving name and default value.
1205                                        if (!substitutionInVariablesEnabled
1206                                                && pfxMatcher.isMatch(varNameExprChars,
1207                                                                        i,
1208                                                                        i,
1209                                                                        varNameExprChars.length) != 0) {
1210                                            break;
1211                                        }
1212                                        if (valueDelimMatcher.isMatch(varNameExprChars, i) != 0) {
1213                                            valueDelimiterMatchLen = valueDelimMatcher.isMatch(varNameExprChars, i);
1214                                            varName = varNameExpr.substring(0, i);
1215                                            varDefaultValue = varNameExpr.substring(i + valueDelimiterMatchLen);
1216                                            break;
1217                                        }
1218                                    }
1219                                }
1220
1221                                // on the first call initialize priorVariables
1222                                if (priorVariables == null) {
1223                                    priorVariables = new ArrayList<>();
1224                                    priorVariables.add(new String(chars,
1225                                            offset, length));
1226                                }
1227
1228                                // handle cyclic substitution
1229                                checkCyclicSubstitution(varName, priorVariables);
1230                                priorVariables.add(varName);
1231
1232                                // resolve the variable
1233                                String varValue = resolveVariable(varName, buf,
1234                                        startPos, endPos);
1235                                if (varValue == null) {
1236                                    varValue = varDefaultValue;
1237                                }
1238                                if (varValue != null) {
1239                                    final int varLen = varValue.length();
1240                                    buf.replace(startPos, endPos, varValue);
1241                                    altered = true;
1242                                    int change = 0;
1243                                    if (!substitutionInValuesDisabled) { // recursive replace
1244                                        change = substitute(buf, startPos,
1245                                            varLen, priorVariables);
1246                                    }
1247                                    change = change
1248                                        + varLen - (endPos - startPos);
1249                                    pos += change;
1250                                    bufEnd += change;
1251                                    lengthChange += change;
1252                                    chars = buf.buffer; // in case buffer was
1253                                                        // altered
1254                                }
1255
1256                                // remove variable from the cyclic stack
1257                                priorVariables
1258                                        .remove(priorVariables.size() - 1);
1259                                break;
1260                            }
1261                            nestedVarCount--;
1262                            pos += endMatchLen;
1263                        }
1264                    }
1265                }
1266            }
1267        }
1268        if (top) {
1269            return altered ? 1 : 0;
1270        }
1271        return lengthChange;
1272    }
1273}