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