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