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 <strong>true</strong>.
121 * <p>This class is <strong>not</strong> 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(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        setVariableResolver(variableResolver);
333        setVariablePrefix(prefix);
334        setVariableSuffix(suffix);
335        setEscapeChar(escape);
336        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        setVariableResolver(variableResolver);
352        setVariablePrefix(prefix);
353        setVariableSuffix(suffix);
354        setEscapeChar(escape);
355        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        setVariableResolver(variableResolver);
387        setVariablePrefixMatcher(prefixMatcher);
388        setVariableSuffixMatcher(suffixMatcher);
389        setEscapeChar(escape);
390        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     * <strong>true</strong>, 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     * @since 1.2
493     */
494    public boolean isDisableSubstitutionInValues() {
495        return disableSubstitutionInValues;
496    }
497
498    /**
499     * Returns a flag whether substitution is done in variable names.
500     *
501     * @return The substitution in variable names flag
502     */
503    public boolean isEnableSubstitutionInVariables() {
504        return enableSubstitutionInVariables;
505    }
506
507    /**
508     * Returns the flag controlling whether escapes are preserved during
509     * substitution.
510     *
511     * @return The preserve escape flag
512     */
513    public boolean isPreserveEscapes() {
514        return preserveEscapes;
515    }
516
517    /**
518     * Replaces all the occurrences of variables with their matching values
519     * from the resolver using the given source array as a template.
520     * The array is not altered by this method.
521     *
522     * @param source  the character array to replace in, not altered, null returns null
523     * @return The result of the replace operation
524     */
525    public String replace(final char[] source) {
526        if (source == null) {
527            return null;
528        }
529        final StrBuilder buf = new StrBuilder(source.length).append(source);
530        substitute(buf, 0, source.length);
531        return buf.toString();
532    }
533
534    /**
535     * Replaces all the occurrences of variables with their matching values
536     * from the resolver using the given source array as a template.
537     * The array is not altered by this method.
538     * <p>
539     * Only the specified portion of the array will be processed.
540     * The rest of the array is not processed, and is not returned.
541     * </p>
542     *
543     * @param source  the character array to replace in, not altered, null returns null
544     * @param offset  the start offset within the array, must be valid
545     * @param length  the length within the array to be processed, must be valid
546     * @return The result of the replace operation
547     */
548    public String replace(final char[] source, final int offset, final int length) {
549        if (source == null) {
550            return null;
551        }
552        final StrBuilder buf = new StrBuilder(length).append(source, offset, length);
553        substitute(buf, 0, length);
554        return buf.toString();
555    }
556
557    /**
558     * Replaces all the occurrences of variables with their matching values
559     * from the resolver using the given source as a template.
560     * The source is not altered by this method.
561     *
562     * @param source  the buffer to use as a template, not changed, null returns null
563     * @return The result of the replace operation
564     */
565    public String replace(final CharSequence source) {
566        if (source == null) {
567            return null;
568        }
569        return replace(source, 0, source.length());
570    }
571
572    /**
573     * Replaces all the occurrences of variables with their matching values
574     * from the resolver using the given source as a template.
575     * The source is not altered by this method.
576     * <p>
577     * Only the specified portion of the buffer will be processed.
578     * The rest of the buffer is not processed, and is not returned.
579     * </p>
580     *
581     * @param source  the buffer to use as a template, not changed, null returns null
582     * @param offset  the start offset within the array, must be valid
583     * @param length  the length within the array to be processed, must be valid
584     * @return The result of the replace operation
585     */
586    public String replace(final CharSequence source, final int offset, final int length) {
587        if (source == null) {
588            return null;
589        }
590        final StrBuilder buf = new StrBuilder(length).append(source, offset, length);
591        substitute(buf, 0, length);
592        return buf.toString();
593    }
594
595    /**
596     * Replaces all the occurrences of variables in the given source object with
597     * their matching values from the resolver. The input source object is
598     * converted to a string using {@code toString} and is not altered.
599     *
600     * @param source  the source to replace in, null returns null
601     * @return The result of the replace operation
602     */
603    public String replace(final Object source) {
604        if (source == null) {
605            return null;
606        }
607        final StrBuilder buf = new StrBuilder().append(source);
608        substitute(buf, 0, buf.length());
609        return buf.toString();
610    }
611
612    /**
613     * Replaces all the occurrences of variables with their matching values
614     * from the resolver using the given source builder as a template.
615     * The builder is not altered by this method.
616     *
617     * @param source  the builder to use as a template, not changed, null returns null
618     * @return The result of the replace operation
619     */
620    public String replace(final StrBuilder source) {
621        if (source == null) {
622            return null;
623        }
624        final StrBuilder buf = new StrBuilder(source.length()).append(source);
625        substitute(buf, 0, buf.length());
626        return buf.toString();
627    }
628
629    /**
630     * Replaces all the occurrences of variables with their matching values
631     * from the resolver using the given source builder as a template.
632     * The builder is not altered by this method.
633     * <p>
634     * Only the specified portion of the builder will be processed.
635     * The rest of the builder is not processed, and is not returned.
636     * </p>
637     *
638     * @param source  the builder to use as a template, not changed, null returns null
639     * @param offset  the start offset within the array, must be valid
640     * @param length  the length within the array to be processed, must be valid
641     * @return The result of the replace operation
642     */
643    public String replace(final StrBuilder source, final int offset, final int length) {
644        if (source == null) {
645            return null;
646        }
647        final StrBuilder buf = new StrBuilder(length).append(source, offset, length);
648        substitute(buf, 0, length);
649        return buf.toString();
650    }
651
652    /**
653     * Replaces all the occurrences of variables with their matching values
654     * from the resolver using the given source string as a template.
655     *
656     * @param source  the string to replace in, null returns null
657     * @return The result of the replace operation
658     */
659    public String replace(final String source) {
660        if (source == null) {
661            return null;
662        }
663        final StrBuilder buf = new StrBuilder(source);
664        if (!substitute(buf, 0, source.length())) {
665            return source;
666        }
667        return buf.toString();
668    }
669
670    /**
671     * Replaces all the occurrences of variables with their matching values
672     * from the resolver using the given source string as a template.
673     * <p>
674     * Only the specified portion of the string will be processed.
675     * The rest of the string is not processed, and is not returned.
676     * </p>
677     *
678     * @param source  the string to replace in, null returns null
679     * @param offset  the start offset within the array, must be valid
680     * @param length  the length within the array to be processed, must be valid
681     * @return The result of the replace operation
682     */
683    public String replace(final String source, final int offset, final int length) {
684        if (source == null) {
685            return null;
686        }
687        final StrBuilder buf = new StrBuilder(length).append(source, offset, length);
688        if (!substitute(buf, 0, length)) {
689            return source.substring(offset, offset + length);
690        }
691        return buf.toString();
692    }
693
694    /**
695     * Replaces all the occurrences of variables with their matching values
696     * from the resolver using the given source buffer as a template.
697     * The buffer is not altered by this method.
698     *
699     * @param source  the buffer to use as a template, not changed, null returns null
700     * @return The result of the replace operation
701     */
702    public String replace(final StringBuffer source) {
703        if (source == null) {
704            return null;
705        }
706        final StrBuilder buf = new StrBuilder(source.length()).append(source);
707        substitute(buf, 0, buf.length());
708        return buf.toString();
709    }
710
711    /**
712     * Replaces all the occurrences of variables with their matching values
713     * from the resolver using the given source buffer as a template.
714     * The buffer is not altered by this method.
715     * <p>
716     * Only the specified portion of the buffer will be processed.
717     * The rest of the buffer is not processed, and is not returned.
718     * </p>
719     *
720     * @param source  the buffer to use as a template, not changed, null returns null
721     * @param offset  the start offset within the array, must be valid
722     * @param length  the length within the array to be processed, must be valid
723     * @return The result of the replace operation
724     */
725    public String replace(final StringBuffer source, final int offset, final int length) {
726        if (source == null) {
727            return null;
728        }
729        final StrBuilder buf = new StrBuilder(length).append(source, offset, length);
730        substitute(buf, 0, length);
731        return buf.toString();
732    }
733
734    /**
735     * Replaces all the occurrences of variables within the given source
736     * builder with their matching values from the resolver.
737     *
738     * @param source  the builder to replace in, updated, null returns zero
739     * @return true if altered
740     */
741    public boolean replaceIn(final StrBuilder source) {
742        if (source == null) {
743            return false;
744        }
745        return substitute(source, 0, source.length());
746    }
747
748    /**
749     * Replaces all the occurrences of variables within the given source
750     * builder with their matching values from the resolver.
751     * <p>
752     * Only the specified portion of the builder will be processed.
753     * The rest of the builder is not processed, but it is not deleted.
754     * </p>
755     *
756     * @param source  the builder to replace in, null returns zero
757     * @param offset  the start offset within the array, must be valid
758     * @param length  the length within the builder to be processed, must be valid
759     * @return true if altered
760     */
761    public boolean replaceIn(final StrBuilder source, final int offset, final int length) {
762        if (source == null) {
763            return false;
764        }
765        return substitute(source, offset, length);
766    }
767
768    /**
769     * Replaces all the occurrences of variables within the given source buffer
770     * with their matching values from the resolver.
771     * The buffer is updated with the result.
772     *
773     * @param source  the buffer to replace in, updated, null returns zero
774     * @return true if altered
775     */
776    public boolean replaceIn(final StringBuffer source) {
777        if (source == null) {
778            return false;
779        }
780        return replaceIn(source, 0, source.length());
781    }
782
783    /**
784     * Replaces all the occurrences of variables within the given source buffer
785     * with their matching values from the resolver.
786     * The buffer is updated with the result.
787     * <p>
788     * Only the specified portion of the buffer will be processed.
789     * The rest of the buffer is not processed, but it is not deleted.
790     * </p>
791     *
792     * @param source  the buffer to replace in, updated, null returns zero
793     * @param offset  the start offset within the array, must be valid
794     * @param length  the length within the buffer to be processed, must be valid
795     * @return true if altered
796     */
797    public boolean replaceIn(final StringBuffer source, final int offset, final int length) {
798        if (source == null) {
799            return false;
800        }
801        final StrBuilder buf = new StrBuilder(length).append(source, offset, length);
802        if (!substitute(buf, 0, length)) {
803            return false;
804        }
805        source.replace(offset, offset + length, buf.toString());
806        return true;
807    }
808
809    /**
810     * Replaces all the occurrences of variables within the given source buffer
811     * with their matching values from the resolver.
812     * The buffer is updated with the result.
813     *
814     * @param source  the buffer to replace in, updated, null returns zero
815     * @return true if altered
816     */
817    public boolean replaceIn(final StringBuilder source) {
818        if (source == null) {
819            return false;
820        }
821        return replaceIn(source, 0, source.length());
822    }
823
824    /**
825     * Replaces all the occurrences of variables within the given source builder
826     * with their matching values from the resolver.
827     * The builder is updated with the result.
828     * <p>
829     * Only the specified portion of the buffer will be processed.
830     * The rest of the buffer is not processed, but it is not deleted.
831     * </p>
832     *
833     * @param source  the buffer to replace in, updated, null returns zero
834     * @param offset  the start offset within the array, must be valid
835     * @param length  the length within the buffer to be processed, must be valid
836     * @return true if altered
837     */
838    public boolean replaceIn(final StringBuilder source, final int offset, final int length) {
839        if (source == null) {
840            return false;
841        }
842        final StrBuilder buf = new StrBuilder(length).append(source, offset, length);
843        if (!substitute(buf, 0, length)) {
844            return false;
845        }
846        source.replace(offset, offset + length, buf.toString());
847        return true;
848    }
849
850    /**
851     * Internal method that resolves the value of a variable.
852     * <p>
853     * Most users of this class do not need to call this method. This method is
854     * called automatically by the substitution process.
855     * </p>
856     * <p>
857     * Writers of subclasses can override this method if they need to alter
858     * how each substitution occurs. The method is passed the variable's name
859     * and must return the corresponding value. This implementation uses the
860     * {@link #getVariableResolver()} with the variable's name as the key.
861     * </p>
862     *
863     * @param variableName  the name of the variable, not null
864     * @param buf  the buffer where the substitution is occurring, not null
865     * @param startPos  the start position of the variable including the prefix, valid
866     * @param endPos  the end position of the variable including the suffix, valid
867     * @return The variable's value or <strong>null</strong> if the variable is unknown
868     */
869    protected String resolveVariable(final String variableName,
870                                     final StrBuilder buf,
871                                     final int startPos,
872                                     final int endPos) {
873        final StrLookup<?> resolver = getVariableResolver();
874        if (resolver == null) {
875            return null;
876        }
877        return resolver.lookup(variableName);
878    }
879
880    /**
881     * Sets a flag whether substitution is done in variable values (recursive).
882     *
883     * @param disableSubstitutionInValues true if substitution in variable value are disabled
884     * @since 1.2
885     */
886    public void setDisableSubstitutionInValues(final boolean disableSubstitutionInValues) {
887        this.disableSubstitutionInValues = disableSubstitutionInValues;
888    }
889
890    /**
891     * Sets a flag whether substitution is done in variable names. If set to
892     * <strong>true</strong>, the names of variables can contain other variables which are
893     * processed first before the original variable is evaluated, e.g.
894     * {@code ${jre-${java.version}}}. The default value is <strong>false</strong>.
895     *
896     * @param enableSubstitutionInVariables the new value of the flag
897     */
898    public void setEnableSubstitutionInVariables(
899            final boolean enableSubstitutionInVariables) {
900        this.enableSubstitutionInVariables = enableSubstitutionInVariables;
901    }
902
903    /**
904     * Sets the escape character.
905     * If this character is placed before a variable reference in the source
906     * text, this variable will be ignored.
907     *
908     * @param escapeCharacter  the escape character (0 for disabling escaping)
909     */
910    public void setEscapeChar(final char escapeCharacter) {
911        this.escapeChar = escapeCharacter;
912    }
913
914    /**
915     * Sets a flag controlling whether escapes are preserved during
916     * substitution.  If set to <strong>true</strong>, the escape character is retained
917     * during substitution (e.g. {@code $${this-is-escaped}} remains
918     * {@code $${this-is-escaped}}).  If set to <strong>false</strong>, the escape
919     * character is removed during substitution (e.g.
920     * {@code $${this-is-escaped}} becomes
921     * {@code ${this-is-escaped}}).  The default value is <strong>false</strong>
922     *
923     * @param preserveEscapes true if escapes are to be preserved
924     */
925    public void setPreserveEscapes(final boolean preserveEscapes) {
926        this.preserveEscapes = preserveEscapes;
927    }
928
929    /**
930     * Sets the variable default value delimiter to use.
931     * <p>
932     * The variable default value delimiter is the character or characters that delimit the
933     * variable name and the variable default value. This method allows a single character
934     * variable default value delimiter to be easily set.
935     * </p>
936     *
937     * @param valueDelimiter  the variable default value delimiter character to use
938     * @return this, to enable chaining
939     */
940    public StrSubstitutor setValueDelimiter(final char valueDelimiter) {
941        return setValueDelimiterMatcher(StrMatcher.charMatcher(valueDelimiter));
942    }
943
944    /**
945     * Sets the variable default value delimiter to use.
946     * <p>
947     * The variable default value delimiter is the character or characters that delimit the
948     * variable name and the variable default value. This method allows a string
949     * variable default value delimiter to be easily set.
950     * </p>
951     * <p>
952     * If the {@code valueDelimiter} is null or empty string, then the variable default
953     * value resolution becomes disabled.
954     * </p>
955     *
956     * @param valueDelimiter  the variable default value delimiter string to use, may be null or empty
957     * @return this, to enable chaining
958     */
959    public StrSubstitutor setValueDelimiter(final String valueDelimiter) {
960        if (valueDelimiter == null || valueDelimiter.isEmpty()) {
961            setValueDelimiterMatcher(null);
962            return this;
963        }
964        return setValueDelimiterMatcher(StrMatcher.stringMatcher(valueDelimiter));
965    }
966
967    /**
968     * Sets the variable default value delimiter matcher to use.
969     * <p>
970     * The variable default value delimiter is the character or characters that delimit the
971     * variable name and the variable default value. This delimiter is expressed in terms of a matcher
972     * allowing advanced variable default value delimiter matches.
973     * </p>
974     * <p>
975     * If the {@code valueDelimiterMatcher} is null, then the variable default value resolution
976     * becomes disabled.
977     * </p>
978     *
979     * @param valueDelimiterMatcher  variable default value delimiter matcher to use, may be null
980     * @return this, to enable chaining
981     */
982    public StrSubstitutor setValueDelimiterMatcher(final StrMatcher valueDelimiterMatcher) {
983        this.valueDelimiterMatcher = valueDelimiterMatcher;
984        return this;
985    }
986
987    /**
988     * Sets the variable prefix to use.
989     * <p>
990     * The variable prefix is the character or characters that identify the
991     * start of a variable. This method allows a single character prefix to
992     * be easily set.
993     * </p>
994     *
995     * @param prefix  the prefix character to use
996     * @return this, to enable chaining
997     */
998    public StrSubstitutor setVariablePrefix(final char prefix) {
999        return setVariablePrefixMatcher(StrMatcher.charMatcher(prefix));
1000    }
1001
1002    /**
1003     * Sets the variable prefix to use.
1004     * <p>
1005     * The variable prefix is the character or characters that identify the
1006     * start of a variable. This method allows a string prefix to be easily set.
1007     * </p>
1008     *
1009     * @param prefix  the prefix for variables, not null
1010     * @return this, to enable chaining
1011     * @throws IllegalArgumentException if the prefix is null
1012     */
1013    public StrSubstitutor setVariablePrefix(final String prefix) {
1014        Validate.isTrue(prefix != null, "Variable prefix must not be null!");
1015        return setVariablePrefixMatcher(StrMatcher.stringMatcher(prefix));
1016    }
1017
1018    /**
1019     * Sets the variable prefix matcher currently in use.
1020     * <p>
1021     * The variable prefix is the character or characters that identify the
1022     * start of a variable. This prefix is expressed in terms of a matcher
1023     * allowing advanced prefix matches.
1024     * </p>
1025     *
1026     * @param prefixMatcher  the prefix matcher to use, null ignored
1027     * @return this, to enable chaining
1028     * @throws IllegalArgumentException if the prefix matcher is null
1029     */
1030    public StrSubstitutor setVariablePrefixMatcher(final StrMatcher prefixMatcher) {
1031        Validate.isTrue(prefixMatcher != null, "Variable prefix matcher must not be null!");
1032        this.prefixMatcher = prefixMatcher;
1033        return this;
1034    }
1035
1036    /**
1037     * Sets the VariableResolver that is used to lookup variables.
1038     *
1039     * @param variableResolver  the VariableResolver
1040     */
1041    public void setVariableResolver(final StrLookup<?> variableResolver) {
1042        this.variableResolver = variableResolver;
1043    }
1044
1045    /**
1046     * Sets the variable suffix to use.
1047     * <p>
1048     * The variable suffix is the character or characters that identify the
1049     * end of a variable. This method allows a single character suffix to
1050     * be easily set.
1051     * </p>
1052     *
1053     * @param suffix  the suffix character to use
1054     * @return this, to enable chaining
1055     */
1056    public StrSubstitutor setVariableSuffix(final char suffix) {
1057        return setVariableSuffixMatcher(StrMatcher.charMatcher(suffix));
1058    }
1059
1060    /**
1061     * Sets the variable suffix to use.
1062     * <p>
1063     * The variable suffix is the character or characters that identify the
1064     * end of a variable. This method allows a string suffix to be easily set.
1065     * </p>
1066     *
1067     * @param suffix  the suffix for variables, not null
1068     * @return this, to enable chaining
1069     * @throws IllegalArgumentException if the suffix is null
1070     */
1071    public StrSubstitutor setVariableSuffix(final String suffix) {
1072        Validate.isTrue(suffix != null, "Variable suffix must not be null!");
1073        return setVariableSuffixMatcher(StrMatcher.stringMatcher(suffix));
1074    }
1075
1076    /**
1077     * Sets the variable suffix matcher currently in use.
1078     * <p>
1079     * The variable suffix is the character or characters that identify the
1080     * end of a variable. This suffix is expressed in terms of a matcher
1081     * allowing advanced suffix matches.
1082     * </p>
1083     *
1084     * @param suffixMatcher  the suffix matcher to use, null ignored
1085     * @return this, to enable chaining
1086     * @throws IllegalArgumentException if the suffix matcher is null
1087     */
1088    public StrSubstitutor setVariableSuffixMatcher(final StrMatcher suffixMatcher) {
1089        Validate.isTrue(suffixMatcher != null, "Variable suffix matcher must not be null!");
1090        this.suffixMatcher = suffixMatcher;
1091        return this;
1092    }
1093
1094    /**
1095     * Internal method that substitutes the variables.
1096     * <p>
1097     * Most users of this class do not need to call this method. This method will
1098     * be called automatically by another (public) method.
1099     * </p>
1100     * <p>
1101     * Writers of subclasses can override this method if they need access to
1102     * the substitution process at the start or end.
1103     * </p>
1104     *
1105     * @param buf  the string builder to substitute into, not null
1106     * @param offset  the start offset within the builder, must be valid
1107     * @param length  the length within the builder to be processed, must be valid
1108     * @return true if altered
1109     */
1110    protected boolean substitute(final StrBuilder buf, final int offset, final int length) {
1111        return substitute(buf, offset, length, null) > 0;
1112    }
1113
1114    /**
1115     * Recursive handler for multiple levels of interpolation. This is the main
1116     * interpolation method, which resolves the values of all variable references
1117     * contained in the passed in text.
1118     *
1119     * @param buf  the string builder to substitute into, not null
1120     * @param offset  the start offset within the builder, must be valid
1121     * @param length  the length within the builder to be processed, must be valid
1122     * @param priorVariables  the stack keeping track of the replaced variables, may be null
1123     * @return The length change that occurs, unless priorVariables is null when the int
1124     *  represents a boolean flag as to whether any change occurred.
1125     */
1126    private int substitute(final StrBuilder buf, final int offset, final int length, List<String> priorVariables) {
1127        final StrMatcher pfxMatcher = getVariablePrefixMatcher();
1128        final StrMatcher suffMatcher = getVariableSuffixMatcher();
1129        final char escape = getEscapeChar();
1130        final StrMatcher valueDelimMatcher = getValueDelimiterMatcher();
1131        final boolean substitutionInVariablesEnabled = isEnableSubstitutionInVariables();
1132        final boolean substitutionInValuesDisabled = isDisableSubstitutionInValues();
1133
1134        final boolean top = priorVariables == null;
1135        boolean altered = false;
1136        int lengthChange = 0;
1137        char[] chars = buf.buffer;
1138        int bufEnd = offset + length;
1139        int pos = offset;
1140        while (pos < bufEnd) {
1141            final int startMatchLen = pfxMatcher.isMatch(chars, pos, offset,
1142                    bufEnd);
1143            if (startMatchLen == 0) {
1144                pos++;
1145            } else // found variable start marker
1146            if (pos > offset && chars[pos - 1] == escape) {
1147                // escaped
1148                if (preserveEscapes) {
1149                    pos++;
1150                    continue;
1151                }
1152                buf.deleteCharAt(pos - 1);
1153                chars = buf.buffer; // in case buffer was altered
1154                lengthChange--;
1155                altered = true;
1156                bufEnd--;
1157            } else {
1158                // find suffix
1159                final int startPos = pos;
1160                pos += startMatchLen;
1161                int endMatchLen = 0;
1162                int nestedVarCount = 0;
1163                while (pos < bufEnd) {
1164                    if (substitutionInVariablesEnabled
1165                            && pfxMatcher.isMatch(chars,
1166                                    pos, offset, bufEnd) != 0) {
1167                        // found a nested variable start
1168                        endMatchLen = pfxMatcher.isMatch(chars,
1169                                pos, offset, bufEnd);
1170                        nestedVarCount++;
1171                        pos += endMatchLen;
1172                        continue;
1173                    }
1174
1175                    endMatchLen = suffMatcher.isMatch(chars, pos, offset,
1176                            bufEnd);
1177                    if (endMatchLen == 0) {
1178                        pos++;
1179                    } else {
1180                        // found variable end marker
1181                        if (nestedVarCount == 0) {
1182                            String varNameExpr = new String(chars, startPos
1183                                    + startMatchLen, pos - startPos
1184                                    - startMatchLen);
1185                            if (substitutionInVariablesEnabled) {
1186                                final StrBuilder bufName = new StrBuilder(varNameExpr);
1187                                substitute(bufName, 0, bufName.length());
1188                                varNameExpr = bufName.toString();
1189                            }
1190                            pos += endMatchLen;
1191                            final int endPos = pos;
1192
1193                            String varName = varNameExpr;
1194                            String varDefaultValue = null;
1195
1196                            if (valueDelimMatcher != null) {
1197                                final char[] varNameExprChars = varNameExpr.toCharArray();
1198                                int valueDelimiterMatchLen = 0;
1199                                for (int i = 0; i < varNameExprChars.length; i++) {
1200                                    // if there's any nested variable when nested variable substitution disabled,
1201                                    // then stop resolving name and default value.
1202                                    if (!substitutionInVariablesEnabled
1203                                            && pfxMatcher.isMatch(varNameExprChars,
1204                                                                    i,
1205                                                                    i,
1206                                                                    varNameExprChars.length) != 0) {
1207                                        break;
1208                                    }
1209                                    if (valueDelimMatcher.isMatch(varNameExprChars, i) != 0) {
1210                                        valueDelimiterMatchLen = valueDelimMatcher.isMatch(varNameExprChars, i);
1211                                        varName = varNameExpr.substring(0, i);
1212                                        varDefaultValue = varNameExpr.substring(i + valueDelimiterMatchLen);
1213                                        break;
1214                                    }
1215                                }
1216                            }
1217
1218                            // on the first call initialize priorVariables
1219                            if (priorVariables == null) {
1220                                priorVariables = new ArrayList<>();
1221                                priorVariables.add(new String(chars,
1222                                        offset, length));
1223                            }
1224
1225                            // handle cyclic substitution
1226                            checkCyclicSubstitution(varName, priorVariables);
1227                            priorVariables.add(varName);
1228
1229                            // resolve the variable
1230                            String varValue = resolveVariable(varName, buf,
1231                                    startPos, endPos);
1232                            if (varValue == null) {
1233                                varValue = varDefaultValue;
1234                            }
1235                            if (varValue != null) {
1236                                final int varLen = varValue.length();
1237                                buf.replace(startPos, endPos, varValue);
1238                                altered = true;
1239                                int change = 0;
1240                                if (!substitutionInValuesDisabled) { // recursive replace
1241                                    change = substitute(buf, startPos,
1242                                        varLen, priorVariables);
1243                                }
1244                                change = change
1245                                    + varLen - (endPos - startPos);
1246                                pos += change;
1247                                bufEnd += change;
1248                                lengthChange += change;
1249                                chars = buf.buffer; // in case buffer was
1250                                                    // altered
1251                            }
1252
1253                            // remove variable from the cyclic stack
1254                            priorVariables
1255                                    .remove(priorVariables.size() - 1);
1256                            break;
1257                        }
1258                        nestedVarCount--;
1259                        pos += endMatchLen;
1260                    }
1261                }
1262            }
1263        }
1264        if (top) {
1265            return altered ? 1 : 0;
1266        }
1267        return lengthChange;
1268    }
1269}