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     */
017    package org.apache.commons.lang3.text;
018    
019    import java.util.ArrayList;
020    import java.util.Enumeration;
021    import java.util.HashMap;
022    import java.util.List;
023    import java.util.Map;
024    import java.util.Properties;
025    
026    /**
027     * Substitutes variables within a string by values.
028     * <p>
029     * This class takes a piece of text and substitutes all the variables within it.
030     * The default definition of a variable is <code>${variableName}</code>.
031     * The prefix and suffix can be changed via constructors and set methods.
032     * <p>
033     * Variable values are typically resolved from a map, but could also be resolved
034     * from system properties, or by supplying a custom variable resolver.
035     * <p>
036     * The simplest example is to use this class to replace Java System properties. For example:
037     * <pre>
038     * StrSubstitutor.replaceSystemProperties(
039     *      "You are running with java.version = ${java.version} and os.name = ${os.name}.");
040     * </pre>
041     * <p>
042     * Typical usage of this class follows the following pattern: First an instance is created
043     * and initialized with the map that contains the values for the available variables.
044     * If a prefix and/or suffix for variables should be used other than the default ones,
045     * the appropriate settings can be performed. After that the <code>replace()</code>
046     * method can be called passing in the source text for interpolation. In the returned
047     * text all variable references (as long as their values are known) will be resolved.
048     * The following example demonstrates this:
049     * <pre>
050     * Map valuesMap = HashMap();
051     * valuesMap.put(&quot;animal&quot;, &quot;quick brown fox&quot;);
052     * valuesMap.put(&quot;target&quot;, &quot;lazy dog&quot;);
053     * String templateString = &quot;The ${animal} jumped over the ${target}.&quot;;
054     * StrSubstitutor sub = new StrSubstitutor(valuesMap);
055     * String resolvedString = sub.replace(templateString);
056     * </pre>
057     * yielding:
058     * <pre>
059     *      The quick brown fox jumped over the lazy dog.
060     * </pre>
061     * <p>
062     * In addition to this usage pattern there are some static convenience methods that
063     * cover the most common use cases. These methods can be used without the need of
064     * manually creating an instance. However if multiple replace operations are to be
065     * performed, creating and reusing an instance of this class will be more efficient.
066     * <p>
067     * Variable replacement works in a recursive way. Thus, if a variable value contains
068     * a variable then that variable will also be replaced. Cyclic replacements are
069     * detected and will cause an exception to be thrown.
070     * <p>
071     * Sometimes the interpolation's result must contain a variable prefix. As an example
072     * take the following source text:
073     * <pre>
074     *   The variable ${${name}} must be used.
075     * </pre>
076     * Here only the variable's name referred to in the text should be replaced resulting
077     * in the text (assuming that the value of the <code>name</code> variable is <code>x</code>):
078     * <pre>
079     *   The variable ${x} must be used.
080     * </pre>
081     * To achieve this effect there are two possibilities: Either set a different prefix
082     * and suffix for variables which do not conflict with the result text you want to
083     * produce. The other possibility is to use the escape character, by default '$'.
084     * If this character is placed before a variable reference, this reference is ignored
085     * and won't be replaced. For example:
086     * <pre>
087     *   The variable $${${name}} must be used.
088     * </pre>
089     * <p>
090     * In some complex scenarios you might even want to perform substitution in the
091     * names of variables, for instance
092     * <pre>
093     * ${jre-${java.specification.version}}
094     * </pre>
095     * <code>StrSubstitutor</code> supports this recursive substitution in variable
096     * names, but it has to be enabled explicitly by setting the
097     * {@link #setEnableSubstitutionInVariables(boolean) enableSubstitutionInVariables}
098     * property to <b>true</b>.
099     *
100     * @version $Id: StrSubstitutor.java 1199894 2011-11-09 17:53:59Z ggregory $
101     * @since 2.2
102     */
103    public class StrSubstitutor {
104    
105        /**
106         * Constant for the default escape character.
107         */
108        public static final char DEFAULT_ESCAPE = '$';
109        /**
110         * Constant for the default variable prefix.
111         */
112        public static final StrMatcher DEFAULT_PREFIX = StrMatcher.stringMatcher("${");
113        /**
114         * Constant for the default variable suffix.
115         */
116        public static final StrMatcher DEFAULT_SUFFIX = StrMatcher.stringMatcher("}");
117    
118        /**
119         * Stores the escape character.
120         */
121        private char escapeChar;
122        /**
123         * Stores the variable prefix.
124         */
125        private StrMatcher prefixMatcher;
126        /**
127         * Stores the variable suffix.
128         */
129        private StrMatcher suffixMatcher;
130        /**
131         * Variable resolution is delegated to an implementor of VariableResolver.
132         */
133        private StrLookup<?> variableResolver;
134        /**
135         * The flag whether substitution in variable names is enabled.
136         */
137        private boolean enableSubstitutionInVariables;
138    
139        //-----------------------------------------------------------------------
140        /**
141         * Replaces all the occurrences of variables in the given source object with
142         * their matching values from the map.
143         *
144         * @param <V> the type of the values in the map
145         * @param source  the source text containing the variables to substitute, null returns null
146         * @param valueMap  the map with the values, may be null
147         * @return the result of the replace operation
148         */
149        public static <V> String replace(Object source, Map<String, V> valueMap) {
150            return new StrSubstitutor(valueMap).replace(source);
151        }
152    
153        /**
154         * Replaces all the occurrences of variables in the given source object with
155         * their matching values from the map. This method allows to specifiy a
156         * custom variable prefix and suffix
157         *
158         * @param <V> the type of the values in the map
159         * @param source  the source text containing the variables to substitute, null returns null
160         * @param valueMap  the map with the values, may be null
161         * @param prefix  the prefix of variables, not null
162         * @param suffix  the suffix of variables, not null
163         * @return the result of the replace operation
164         * @throws IllegalArgumentException if the prefix or suffix is null
165         */
166        public static <V> String replace(Object source, Map<String, V> valueMap, String prefix, String suffix) {
167            return new StrSubstitutor(valueMap, prefix, suffix).replace(source);
168        }
169    
170        /**
171         * Replaces all the occurrences of variables in the given source object with their matching
172         * values from the properties.
173         *
174         * @param source the source text containing the variables to substitute, null returns null
175         * @param valueProperties the properties with values, may be null
176         * @return the result of the replace operation
177         */
178        public static String replace(Object source, Properties valueProperties) {
179            if (valueProperties == null) {
180                return source.toString();
181            }
182            Map<String,String> valueMap = new HashMap<String,String>();
183            Enumeration<?> propNames = valueProperties.propertyNames();
184            while (propNames.hasMoreElements()) {
185                String propName = (String)propNames.nextElement();
186                String propValue = valueProperties.getProperty(propName);
187                valueMap.put(propName, propValue);
188            }
189            return StrSubstitutor.replace(source, valueMap);
190        }
191    
192        /**
193         * Replaces all the occurrences of variables in the given source object with
194         * their matching values from the system properties.
195         *
196         * @param source  the source text containing the variables to substitute, null returns null
197         * @return the result of the replace operation
198         */
199        public static String replaceSystemProperties(Object source) {
200            return new StrSubstitutor(StrLookup.systemPropertiesLookup()).replace(source);
201        }
202    
203        //-----------------------------------------------------------------------
204        /**
205         * Creates a new instance with defaults for variable prefix and suffix
206         * and the escaping character.
207         */
208        public StrSubstitutor() {
209            this((StrLookup<?>) null, DEFAULT_PREFIX, DEFAULT_SUFFIX, DEFAULT_ESCAPE);
210        }
211    
212        /**
213         * Creates a new instance and initializes it. Uses defaults for variable
214         * prefix and suffix and the escaping character.
215         *
216         * @param <V> the type of the values in the map
217         * @param valueMap  the map with the variables' values, may be null
218         */
219        public <V> StrSubstitutor(Map<String, V> valueMap) {
220            this(StrLookup.mapLookup(valueMap), DEFAULT_PREFIX, DEFAULT_SUFFIX, DEFAULT_ESCAPE);
221        }
222    
223        /**
224         * Creates a new instance and initializes it. Uses a default escaping character.
225         *
226         * @param <V> the type of the values in the map
227         * @param valueMap  the map with the variables' values, may be null
228         * @param prefix  the prefix for variables, not null
229         * @param suffix  the suffix for variables, not null
230         * @throws IllegalArgumentException if the prefix or suffix is null
231         */
232        public <V> StrSubstitutor(Map<String, V> valueMap, String prefix, String suffix) {
233            this(StrLookup.mapLookup(valueMap), prefix, suffix, DEFAULT_ESCAPE);
234        }
235    
236        /**
237         * Creates a new instance and initializes it.
238         *
239         * @param <V> the type of the values in the map
240         * @param valueMap  the map with the variables' values, may be null
241         * @param prefix  the prefix for variables, not null
242         * @param suffix  the suffix for variables, not null
243         * @param escape  the escape character
244         * @throws IllegalArgumentException if the prefix or suffix is null
245         */
246        public <V> StrSubstitutor(Map<String, V> valueMap, String prefix, String suffix, char escape) {
247            this(StrLookup.mapLookup(valueMap), prefix, suffix, escape);
248        }
249    
250        /**
251         * Creates a new instance and initializes it.
252         *
253         * @param variableResolver  the variable resolver, may be null
254         */
255        public StrSubstitutor(StrLookup<?> variableResolver) {
256            this(variableResolver, DEFAULT_PREFIX, DEFAULT_SUFFIX, DEFAULT_ESCAPE);
257        }
258    
259        /**
260         * Creates a new instance and initializes it.
261         *
262         * @param variableResolver  the variable resolver, may be null
263         * @param prefix  the prefix for variables, not null
264         * @param suffix  the suffix for variables, not null
265         * @param escape  the escape character
266         * @throws IllegalArgumentException if the prefix or suffix is null
267         */
268        public StrSubstitutor(StrLookup<?> variableResolver, String prefix, String suffix, char escape) {
269            this.setVariableResolver(variableResolver);
270            this.setVariablePrefix(prefix);
271            this.setVariableSuffix(suffix);
272            this.setEscapeChar(escape);
273        }
274    
275        /**
276         * Creates a new instance and initializes it.
277         *
278         * @param variableResolver  the variable resolver, may be null
279         * @param prefixMatcher  the prefix for variables, not null
280         * @param suffixMatcher  the suffix for variables, not null
281         * @param escape  the escape character
282         * @throws IllegalArgumentException if the prefix or suffix is null
283         */
284        public StrSubstitutor(
285                StrLookup<?> variableResolver, StrMatcher prefixMatcher, StrMatcher suffixMatcher, char escape) {
286            this.setVariableResolver(variableResolver);
287            this.setVariablePrefixMatcher(prefixMatcher);
288            this.setVariableSuffixMatcher(suffixMatcher);
289            this.setEscapeChar(escape);
290        }
291    
292        //-----------------------------------------------------------------------
293        /**
294         * Replaces all the occurrences of variables with their matching values
295         * from the resolver using the given source string as a template.
296         *
297         * @param source  the string to replace in, null returns null
298         * @return the result of the replace operation
299         */
300        public String replace(String source) {
301            if (source == null) {
302                return null;
303            }
304            StrBuilder buf = new StrBuilder(source);
305            if (substitute(buf, 0, source.length()) == false) {
306                return source;
307            }
308            return buf.toString();
309        }
310    
311        /**
312         * Replaces all the occurrences of variables with their matching values
313         * from the resolver using the given source string as a template.
314         * <p>
315         * Only the specified portion of the string will be processed.
316         * The rest of the string is not processed, and is not returned.
317         *
318         * @param source  the string to replace in, null returns null
319         * @param offset  the start offset within the array, must be valid
320         * @param length  the length within the array to be processed, must be valid
321         * @return the result of the replace operation
322         */
323        public String replace(String source, int offset, int length) {
324            if (source == null) {
325                return null;
326            }
327            StrBuilder buf = new StrBuilder(length).append(source, offset, length);
328            if (substitute(buf, 0, length) == false) {
329                return source.substring(offset, offset + length);
330            }
331            return buf.toString();
332        }
333    
334        //-----------------------------------------------------------------------
335        /**
336         * Replaces all the occurrences of variables with their matching values
337         * from the resolver using the given source array as a template.
338         * The array is not altered by this method.
339         *
340         * @param source  the character array to replace in, not altered, null returns null
341         * @return the result of the replace operation
342         */
343        public String replace(char[] source) {
344            if (source == null) {
345                return null;
346            }
347            StrBuilder buf = new StrBuilder(source.length).append(source);
348            substitute(buf, 0, source.length);
349            return buf.toString();
350        }
351    
352        /**
353         * Replaces all the occurrences of variables with their matching values
354         * from the resolver using the given source array as a template.
355         * The array is not altered by this method.
356         * <p>
357         * Only the specified portion of the array will be processed.
358         * The rest of the array is not processed, and is not returned.
359         *
360         * @param source  the character array to replace in, not altered, null returns null
361         * @param offset  the start offset within the array, must be valid
362         * @param length  the length within the array to be processed, must be valid
363         * @return the result of the replace operation
364         */
365        public String replace(char[] source, int offset, int length) {
366            if (source == null) {
367                return null;
368            }
369            StrBuilder buf = new StrBuilder(length).append(source, offset, length);
370            substitute(buf, 0, length);
371            return buf.toString();
372        }
373    
374        //-----------------------------------------------------------------------
375        /**
376         * Replaces all the occurrences of variables with their matching values
377         * from the resolver using the given source buffer as a template.
378         * The buffer is not altered by this method.
379         *
380         * @param source  the buffer to use as a template, not changed, null returns null
381         * @return the result of the replace operation
382         */
383        public String replace(StringBuffer source) {
384            if (source == null) {
385                return null;
386            }
387            StrBuilder buf = new StrBuilder(source.length()).append(source);
388            substitute(buf, 0, buf.length());
389            return buf.toString();
390        }
391    
392        /**
393         * Replaces all the occurrences of variables with their matching values
394         * from the resolver using the given source buffer as a template.
395         * The buffer is not altered by this method.
396         * <p>
397         * Only the specified portion of the buffer will be processed.
398         * The rest of the buffer is not processed, and is not returned.
399         *
400         * @param source  the buffer to use as a template, not changed, null returns null
401         * @param offset  the start offset within the array, must be valid
402         * @param length  the length within the array to be processed, must be valid
403         * @return the result of the replace operation
404         */
405        public String replace(StringBuffer source, int offset, int length) {
406            if (source == null) {
407                return null;
408            }
409            StrBuilder buf = new StrBuilder(length).append(source, offset, length);
410            substitute(buf, 0, length);
411            return buf.toString();
412        }
413    
414        //-----------------------------------------------------------------------
415        /**
416         * Replaces all the occurrences of variables with their matching values
417         * from the resolver using the given source builder as a template.
418         * The builder is not altered by this method.
419         *
420         * @param source  the builder to use as a template, not changed, null returns null
421         * @return the result of the replace operation
422         */
423        public String replace(StrBuilder source) {
424            if (source == null) {
425                return null;
426            }
427            StrBuilder buf = new StrBuilder(source.length()).append(source);
428            substitute(buf, 0, buf.length());
429            return buf.toString();
430        }
431    
432        /**
433         * Replaces all the occurrences of variables with their matching values
434         * from the resolver using the given source builder as a template.
435         * The builder is not altered by this method.
436         * <p>
437         * Only the specified portion of the builder will be processed.
438         * The rest of the builder is not processed, and is not returned.
439         *
440         * @param source  the builder to use as a template, not changed, null returns null
441         * @param offset  the start offset within the array, must be valid
442         * @param length  the length within the array to be processed, must be valid
443         * @return the result of the replace operation
444         */
445        public String replace(StrBuilder source, int offset, int length) {
446            if (source == null) {
447                return null;
448            }
449            StrBuilder buf = new StrBuilder(length).append(source, offset, length);
450            substitute(buf, 0, length);
451            return buf.toString();
452        }
453    
454        //-----------------------------------------------------------------------
455        /**
456         * Replaces all the occurrences of variables in the given source object with
457         * their matching values from the resolver. The input source object is
458         * converted to a string using <code>toString</code> and is not altered.
459         *
460         * @param source  the source to replace in, null returns null
461         * @return the result of the replace operation
462         */
463        public String replace(Object source) {
464            if (source == null) {
465                return null;
466            }
467            StrBuilder buf = new StrBuilder().append(source);
468            substitute(buf, 0, buf.length());
469            return buf.toString();
470        }
471    
472        //-----------------------------------------------------------------------
473        /**
474         * Replaces all the occurrences of variables within the given source buffer
475         * with their matching values from the resolver.
476         * The buffer is updated with the result.
477         *
478         * @param source  the buffer to replace in, updated, null returns zero
479         * @return true if altered
480         */
481        public boolean replaceIn(StringBuffer source) {
482            if (source == null) {
483                return false;
484            }
485            return replaceIn(source, 0, source.length());
486        }
487    
488        /**
489         * Replaces all the occurrences of variables within the given source buffer
490         * with their matching values from the resolver.
491         * The buffer is updated with the result.
492         * <p>
493         * Only the specified portion of the buffer will be processed.
494         * The rest of the buffer is not processed, but it is not deleted.
495         *
496         * @param source  the buffer to replace in, updated, null returns zero
497         * @param offset  the start offset within the array, must be valid
498         * @param length  the length within the buffer to be processed, must be valid
499         * @return true if altered
500         */
501        public boolean replaceIn(StringBuffer source, int offset, int length) {
502            if (source == null) {
503                return false;
504            }
505            StrBuilder buf = new StrBuilder(length).append(source, offset, length);
506            if (substitute(buf, 0, length) == false) {
507                return false;
508            }
509            source.replace(offset, offset + length, buf.toString());
510            return true;
511        }
512    
513        //-----------------------------------------------------------------------
514        /**
515         * Replaces all the occurrences of variables within the given source
516         * builder with their matching values from the resolver.
517         *
518         * @param source  the builder to replace in, updated, null returns zero
519         * @return true if altered
520         */
521        public boolean replaceIn(StrBuilder source) {
522            if (source == null) {
523                return false;
524            }
525            return substitute(source, 0, source.length());
526        }
527    
528        /**
529         * Replaces all the occurrences of variables within the given source
530         * builder with their matching values from the resolver.
531         * <p>
532         * Only the specified portion of the builder will be processed.
533         * The rest of the builder is not processed, but it is not deleted.
534         *
535         * @param source  the builder to replace in, null returns zero
536         * @param offset  the start offset within the array, must be valid
537         * @param length  the length within the builder to be processed, must be valid
538         * @return true if altered
539         */
540        public boolean replaceIn(StrBuilder source, int offset, int length) {
541            if (source == null) {
542                return false;
543            }
544            return substitute(source, offset, length);
545        }
546    
547        //-----------------------------------------------------------------------
548        /**
549         * Internal method that substitutes the variables.
550         * <p>
551         * Most users of this class do not need to call this method. This method will
552         * be called automatically by another (public) method.
553         * <p>
554         * Writers of subclasses can override this method if they need access to
555         * the substitution process at the start or end.
556         *
557         * @param buf  the string builder to substitute into, not null
558         * @param offset  the start offset within the builder, must be valid
559         * @param length  the length within the builder to be processed, must be valid
560         * @return true if altered
561         */
562        protected boolean substitute(StrBuilder buf, int offset, int length) {
563            return substitute(buf, offset, length, null) > 0;
564        }
565    
566        /**
567         * Recursive handler for multiple levels of interpolation. This is the main
568         * interpolation method, which resolves the values of all variable references
569         * contained in the passed in text.
570         *
571         * @param buf  the string builder to substitute into, not null
572         * @param offset  the start offset within the builder, must be valid
573         * @param length  the length within the builder to be processed, must be valid
574         * @param priorVariables  the stack keeping track of the replaced variables, may be null
575         * @return the length change that occurs, unless priorVariables is null when the int
576         *  represents a boolean flag as to whether any change occurred.
577         */
578        private int substitute(StrBuilder buf, int offset, int length, List<String> priorVariables) {
579            StrMatcher prefixMatcher = getVariablePrefixMatcher();
580            StrMatcher suffixMatcher = getVariableSuffixMatcher();
581            char escape = getEscapeChar();
582    
583            boolean top = priorVariables == null;
584            boolean altered = false;
585            int lengthChange = 0;
586            char[] chars = buf.buffer;
587            int bufEnd = offset + length;
588            int pos = offset;
589            while (pos < bufEnd) {
590                int startMatchLen = prefixMatcher.isMatch(chars, pos, offset,
591                        bufEnd);
592                if (startMatchLen == 0) {
593                    pos++;
594                } else {
595                    // found variable start marker
596                    if (pos > offset && chars[pos - 1] == escape) {
597                        // escaped
598                        buf.deleteCharAt(pos - 1);
599                        chars = buf.buffer; // in case buffer was altered
600                        lengthChange--;
601                        altered = true;
602                        bufEnd--;
603                    } else {
604                        // find suffix
605                        int startPos = pos;
606                        pos += startMatchLen;
607                        int endMatchLen = 0;
608                        int nestedVarCount = 0;
609                        while (pos < bufEnd) {
610                            if (isEnableSubstitutionInVariables()
611                                    && (endMatchLen = prefixMatcher.isMatch(chars,
612                                            pos, offset, bufEnd)) != 0) {
613                                // found a nested variable start
614                                nestedVarCount++;
615                                pos += endMatchLen;
616                                continue;
617                            }
618    
619                            endMatchLen = suffixMatcher.isMatch(chars, pos, offset,
620                                    bufEnd);
621                            if (endMatchLen == 0) {
622                                pos++;
623                            } else {
624                                // found variable end marker
625                                if (nestedVarCount == 0) {
626                                    String varName = new String(chars, startPos
627                                            + startMatchLen, pos - startPos
628                                            - startMatchLen);
629                                    if (isEnableSubstitutionInVariables()) {
630                                        StrBuilder bufName = new StrBuilder(varName);
631                                        substitute(bufName, 0, bufName.length());
632                                        varName = bufName.toString();
633                                    }
634                                    pos += endMatchLen;
635                                    int endPos = pos;
636    
637                                    // on the first call initialize priorVariables
638                                    if (priorVariables == null) {
639                                        priorVariables = new ArrayList<String>();
640                                        priorVariables.add(new String(chars,
641                                                offset, length));
642                                    }
643    
644                                    // handle cyclic substitution
645                                    checkCyclicSubstitution(varName, priorVariables);
646                                    priorVariables.add(varName);
647    
648                                    // resolve the variable
649                                    String varValue = resolveVariable(varName, buf,
650                                            startPos, endPos);
651                                    if (varValue != null) {
652                                        // recursive replace
653                                        int varLen = varValue.length();
654                                        buf.replace(startPos, endPos, varValue);
655                                        altered = true;
656                                        int change = substitute(buf, startPos,
657                                                varLen, priorVariables);
658                                        change = change
659                                                + varLen - (endPos - startPos);
660                                        pos += change;
661                                        bufEnd += change;
662                                        lengthChange += change;
663                                        chars = buf.buffer; // in case buffer was
664                                                            // altered
665                                    }
666    
667                                    // remove variable from the cyclic stack
668                                    priorVariables
669                                            .remove(priorVariables.size() - 1);
670                                    break;
671                                } else {
672                                    nestedVarCount--;
673                                    pos += endMatchLen;
674                                }
675                            }
676                        }
677                    }
678                }
679            }
680            if (top) {
681                return altered ? 1 : 0;
682            }
683            return lengthChange;
684        }
685    
686        /**
687         * Checks if the specified variable is already in the stack (list) of variables.
688         *
689         * @param varName  the variable name to check
690         * @param priorVariables  the list of prior variables
691         */
692        private void checkCyclicSubstitution(String varName, List<String> priorVariables) {
693            if (priorVariables.contains(varName) == false) {
694                return;
695            }
696            StrBuilder buf = new StrBuilder(256);
697            buf.append("Infinite loop in property interpolation of ");
698            buf.append(priorVariables.remove(0));
699            buf.append(": ");
700            buf.appendWithSeparators(priorVariables, "->");
701            throw new IllegalStateException(buf.toString());
702        }
703    
704        /**
705         * Internal method that resolves the value of a variable.
706         * <p>
707         * Most users of this class do not need to call this method. This method is
708         * called automatically by the substitution process.
709         * <p>
710         * Writers of subclasses can override this method if they need to alter
711         * how each substitution occurs. The method is passed the variable's name
712         * and must return the corresponding value. This implementation uses the
713         * {@link #getVariableResolver()} with the variable's name as the key.
714         *
715         * @param variableName  the name of the variable, not null
716         * @param buf  the buffer where the substitution is occurring, not null
717         * @param startPos  the start position of the variable including the prefix, valid
718         * @param endPos  the end position of the variable including the suffix, valid
719         * @return the variable's value or <b>null</b> if the variable is unknown
720         */
721        protected String resolveVariable(String variableName, StrBuilder buf, int startPos, int endPos) {
722            StrLookup<?> resolver = getVariableResolver();
723            if (resolver == null) {
724                return null;
725            }
726            return resolver.lookup(variableName);
727        }
728    
729        // Escape
730        //-----------------------------------------------------------------------
731        /**
732         * Returns the escape character.
733         *
734         * @return the character used for escaping variable references
735         */
736        public char getEscapeChar() {
737            return this.escapeChar;
738        }
739    
740        /**
741         * Sets the escape character.
742         * If this character is placed before a variable reference in the source
743         * text, this variable will be ignored.
744         *
745         * @param escapeCharacter  the escape character (0 for disabling escaping)
746         */
747        public void setEscapeChar(char escapeCharacter) {
748            this.escapeChar = escapeCharacter;
749        }
750    
751        // Prefix
752        //-----------------------------------------------------------------------
753        /**
754         * Gets the variable prefix matcher currently in use.
755         * <p>
756         * The variable prefix is the characer or characters that identify the
757         * start of a variable. This prefix is expressed in terms of a matcher
758         * allowing advanced prefix matches.
759         *
760         * @return the prefix matcher in use
761         */
762        public StrMatcher getVariablePrefixMatcher() {
763            return prefixMatcher;
764        }
765    
766        /**
767         * Sets the variable prefix matcher currently in use.
768         * <p>
769         * The variable prefix is the characer or characters that identify the
770         * start of a variable. This prefix is expressed in terms of a matcher
771         * allowing advanced prefix matches.
772         *
773         * @param prefixMatcher  the prefix matcher to use, null ignored
774         * @return this, to enable chaining
775         * @throws IllegalArgumentException if the prefix matcher is null
776         */
777        public StrSubstitutor setVariablePrefixMatcher(StrMatcher prefixMatcher) {
778            if (prefixMatcher == null) {
779                throw new IllegalArgumentException("Variable prefix matcher must not be null!");
780            }
781            this.prefixMatcher = prefixMatcher;
782            return this;
783        }
784    
785        /**
786         * Sets the variable prefix to use.
787         * <p>
788         * The variable prefix is the character or characters that identify the
789         * start of a variable. This method allows a single character prefix to
790         * be easily set.
791         *
792         * @param prefix  the prefix character to use
793         * @return this, to enable chaining
794         */
795        public StrSubstitutor setVariablePrefix(char prefix) {
796            return setVariablePrefixMatcher(StrMatcher.charMatcher(prefix));
797        }
798    
799        /**
800         * Sets the variable prefix to use.
801         * <p>
802         * The variable prefix is the characer or characters that identify the
803         * start of a variable. This method allows a string prefix to be easily set.
804         *
805         * @param prefix  the prefix for variables, not null
806         * @return this, to enable chaining
807         * @throws IllegalArgumentException if the prefix is null
808         */
809        public StrSubstitutor setVariablePrefix(String prefix) {
810           if (prefix == null) {
811                throw new IllegalArgumentException("Variable prefix must not be null!");
812            }
813            return setVariablePrefixMatcher(StrMatcher.stringMatcher(prefix));
814        }
815    
816        // Suffix
817        //-----------------------------------------------------------------------
818        /**
819         * Gets the variable suffix matcher currently in use.
820         * <p>
821         * The variable suffix is the characer or characters that identify the
822         * end of a variable. This suffix is expressed in terms of a matcher
823         * allowing advanced suffix matches.
824         *
825         * @return the suffix matcher in use
826         */
827        public StrMatcher getVariableSuffixMatcher() {
828            return suffixMatcher;
829        }
830    
831        /**
832         * Sets the variable suffix matcher currently in use.
833         * <p>
834         * The variable suffix is the characer or characters that identify the
835         * end of a variable. This suffix is expressed in terms of a matcher
836         * allowing advanced suffix matches.
837         *
838         * @param suffixMatcher  the suffix matcher to use, null ignored
839         * @return this, to enable chaining
840         * @throws IllegalArgumentException if the suffix matcher is null
841         */
842        public StrSubstitutor setVariableSuffixMatcher(StrMatcher suffixMatcher) {
843            if (suffixMatcher == null) {
844                throw new IllegalArgumentException("Variable suffix matcher must not be null!");
845            }
846            this.suffixMatcher = suffixMatcher;
847            return this;
848        }
849    
850        /**
851         * Sets the variable suffix to use.
852         * <p>
853         * The variable suffix is the characer or characters that identify the
854         * end of a variable. This method allows a single character suffix to
855         * be easily set.
856         *
857         * @param suffix  the suffix character to use
858         * @return this, to enable chaining
859         */
860        public StrSubstitutor setVariableSuffix(char suffix) {
861            return setVariableSuffixMatcher(StrMatcher.charMatcher(suffix));
862        }
863    
864        /**
865         * Sets the variable suffix to use.
866         * <p>
867         * The variable suffix is the character or characters that identify the
868         * end of a variable. This method allows a string suffix to be easily set.
869         *
870         * @param suffix  the suffix for variables, not null
871         * @return this, to enable chaining
872         * @throws IllegalArgumentException if the suffix is null
873         */
874        public StrSubstitutor setVariableSuffix(String suffix) {
875           if (suffix == null) {
876                throw new IllegalArgumentException("Variable suffix must not be null!");
877            }
878            return setVariableSuffixMatcher(StrMatcher.stringMatcher(suffix));
879        }
880    
881        // Resolver
882        //-----------------------------------------------------------------------
883        /**
884         * Gets the VariableResolver that is used to lookup variables.
885         *
886         * @return the VariableResolver
887         */
888        public StrLookup<?> getVariableResolver() {
889            return this.variableResolver;
890        }
891    
892        /**
893         * Sets the VariableResolver that is used to lookup variables.
894         *
895         * @param variableResolver  the VariableResolver
896         */
897        public void setVariableResolver(StrLookup<?> variableResolver) {
898            this.variableResolver = variableResolver;
899        }
900    
901        // Substitution support in variable names
902        //-----------------------------------------------------------------------
903        /**
904         * Returns a flag whether substitution is done in variable names.
905         *
906         * @return the substitution in variable names flag
907         * @since 3.0
908         */
909        public boolean isEnableSubstitutionInVariables() {
910            return enableSubstitutionInVariables;
911        }
912    
913        /**
914         * Sets a flag whether substitution is done in variable names. If set to
915         * <b>true</b>, the names of variables can contain other variables which are
916         * processed first before the original variable is evaluated, e.g.
917         * <code>${jre-${java.version}}</code>. The default value is <b>false</b>.
918         *
919         * @param enableSubstitutionInVariables the new value of the flag
920         * @since 3.0
921         */
922        public void setEnableSubstitutionInVariables(
923                boolean enableSubstitutionInVariables) {
924            this.enableSubstitutionInVariables = enableSubstitutionInVariables;
925        }
926    }