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