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