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