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