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