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