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