View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    * 
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   * 
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.lang.text;
18  
19  import java.util.ArrayList;
20  import java.util.List;
21  import java.util.Map;
22  
23  /**
24   * Substitutes variables within a string by values.
25   * <p>
26   * This class takes a piece of text and substitutes all the variables within it.
27   * The default definition of a variable is <code>${variableName}</code>.
28   * The prefix and suffix can be changed via constructors and set methods.
29   * <p>
30   * Variable values are typically resolved from a map, but could also be resolved
31   * from system properties, or by supplying a custom variable resolver.
32   * <p>
33   * The simplest example is to use this class to replace Java System properties. For example:
34   * <pre>
35   * StrSubstitutor.replaceSystemProperties(
36   *      "You are running with java.version = ${java.version} and os.name = ${os.name}.");
37   * </pre>
38   * <p>
39   * Typical usage of this class follows the following pattern: First an instance is created
40   * and initialized with the map that contains the values for the available variables.
41   * If a prefix and/or suffix for variables should be used other than the default ones,
42   * the appropriate settings can be performed. After that the <code>replace()</code>
43   * method can be called passing in the source text for interpolation. In the returned
44   * text all variable references (as long as their values are known) will be resolved.
45   * The following example demonstrates this:
46   * <pre>
47   * Map valuesMap = HashMap();
48   * valuesMap.put(&quot;animal&quot;, &quot;quick brown fox&quot;);
49   * valuesMap.put(&quot;target&quot;, &quot;lazy dog&quot;);
50   * String templateString = &quot;The ${animal} jumped over the ${target}.&quot;;
51   * StrSubstitutor sub = new StrSubstitutor(valuesMap);
52   * String resolvedString = sub.replace(templateString);
53   * </pre>
54   * yielding:
55   * <pre>
56   *      The quick brown fox jumped over the lazy dog.
57   * </pre>
58   * <p>
59   * In addition to this usage pattern there are some static convenience methods that
60   * cover the most common use cases. These methods can be used without the need of
61   * manually creating an instance. However if multiple replace operations are to be
62   * performed, creating and reusing an instance of this class will be more efficient.
63   * <p>
64   * Variable replacement works in a recursive way. Thus, if a variable value contains
65   * a variable then that variable will also be replaced. Cyclic replacements are
66   * detected and will cause an exception to be thrown.
67   * <p>
68   * Sometimes the interpolation's result must contain a variable prefix. As an example
69   * take the following source text:
70   * <pre>
71   *   The variable ${${name}} must be used.
72   * </pre>
73   * Here only the variable's name refered to in the text should be replaced resulting
74   * in the text (assuming that the value of the <code>name</code> variable is <code>x</code>):
75   * <pre>
76   *   The variable ${x} must be used.
77   * </pre>
78   * To achieve this effect there are two possibilities: Either set a different prefix
79   * and suffix for variables which do not conflict with the result text you want to
80   * produce. The other possibility is to use the escape character, by default '$'.
81   * If this character is placed before a variable reference, this reference is ignored
82   * and won't be replaced. For example:
83   * <pre>
84   *   The variable $${${name}} must be used.
85   * </pre>
86   *
87   * @author Oliver Heger
88   * @author Stephen Colebourne
89   * @version $Id: StrSubstitutor.java 592077 2007-11-05 16:47:10Z mbenson $
90   * @since 2.2
91   */
92  public class StrSubstitutor {
93  
94      /**
95       * Constant for the default escape character.
96       */
97      public static final char DEFAULT_ESCAPE = '$';
98      /**
99       * 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 }