Coverage Report - org.apache.commons.lang3.text.StrSubstitutor
 
Classes in this File Line Coverage Branch Coverage Complexity
StrSubstitutor
99%
201/203
97%
78/80
2.5
 
 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.lang3.text;
 18  
 
 19  
 import java.util.ArrayList;
 20  
 import java.util.Enumeration;
 21  
 import java.util.HashMap;
 22  
 import java.util.List;
 23  
 import java.util.Map;
 24  
 import java.util.Properties;
 25  
 
 26  
 /**
 27  
  * Substitutes variables within a string by values.
 28  
  * <p>
 29  
  * This class takes a piece of text and substitutes all the variables within it.
 30  
  * The default definition of a variable is <code>${variableName}</code>.
 31  
  * The prefix and suffix can be changed via constructors and set methods.
 32  
  * <p>
 33  
  * Variable values are typically resolved from a map, but could also be resolved
 34  
  * from system properties, or by supplying a custom variable resolver.
 35  
  * <p>
 36  
  * The simplest example is to use this class to replace Java System properties. For example:
 37  
  * <pre>
 38  
  * StrSubstitutor.replaceSystemProperties(
 39  
  *      "You are running with java.version = ${java.version} and os.name = ${os.name}.");
 40  
  * </pre>
 41  
  * <p>
 42  
  * Typical usage of this class follows the following pattern: First an instance is created
 43  
  * and initialized with the map that contains the values for the available variables.
 44  
  * If a prefix and/or suffix for variables should be used other than the default ones,
 45  
  * the appropriate settings can be performed. After that the <code>replace()</code>
 46  
  * method can be called passing in the source text for interpolation. In the returned
 47  
  * text all variable references (as long as their values are known) will be resolved.
 48  
  * The following example demonstrates this:
 49  
  * <pre>
 50  
  * Map valuesMap = HashMap();
 51  
  * valuesMap.put(&quot;animal&quot;, &quot;quick brown fox&quot;);
 52  
  * valuesMap.put(&quot;target&quot;, &quot;lazy dog&quot;);
 53  
  * String templateString = &quot;The ${animal} jumped over the ${target}.&quot;;
 54  
  * StrSubstitutor sub = new StrSubstitutor(valuesMap);
 55  
  * String resolvedString = sub.replace(templateString);
 56  
  * </pre>
 57  
  * yielding:
 58  
  * <pre>
 59  
  *      The quick brown fox jumped over the lazy dog.
 60  
  * </pre>
 61  
  * <p>
 62  
  * In addition to this usage pattern there are some static convenience methods that
 63  
  * cover the most common use cases. These methods can be used without the need of
 64  
  * manually creating an instance. However if multiple replace operations are to be
 65  
  * performed, creating and reusing an instance of this class will be more efficient.
 66  
  * <p>
 67  
  * Variable replacement works in a recursive way. Thus, if a variable value contains
 68  
  * a variable then that variable will also be replaced. Cyclic replacements are
 69  
  * detected and will cause an exception to be thrown.
 70  
  * <p>
 71  
  * Sometimes the interpolation's result must contain a variable prefix. As an example
 72  
  * take the following source text:
 73  
  * <pre>
 74  
  *   The variable ${${name}} must be used.
 75  
  * </pre>
 76  
  * Here only the variable's name referred to in the text should be replaced resulting
 77  
  * in the text (assuming that the value of the <code>name</code> variable is <code>x</code>):
 78  
  * <pre>
 79  
  *   The variable ${x} must be used.
 80  
  * </pre>
 81  
  * To achieve this effect there are two possibilities: Either set a different prefix
 82  
  * and suffix for variables which do not conflict with the result text you want to
 83  
  * produce. The other possibility is to use the escape character, by default '$'.
 84  
  * If this character is placed before a variable reference, this reference is ignored
 85  
  * and won't be replaced. For example:
 86  
  * <pre>
 87  
  *   The variable $${${name}} must be used.
 88  
  * </pre>
 89  
  * <p>
 90  
  * In some complex scenarios you might even want to perform substitution in the
 91  
  * names of variables, for instance
 92  
  * <pre>
 93  
  * ${jre-${java.specification.version}}
 94  
  * </pre>
 95  
  * <code>StrSubstitutor</code> supports this recursive substitution in variable
 96  
  * names, but it has to be enabled explicitly by setting the
 97  
  * {@link #setEnableSubstitutionInVariables(boolean) enableSubstitutionInVariables}
 98  
  * property to <b>true</b>.
 99  
  *
 100  
  * @version $Id: StrSubstitutor.java 1436770 2013-01-22 07:09:45Z 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  1
     public static final StrMatcher DEFAULT_PREFIX = StrMatcher.stringMatcher("${");
 113  
     /**
 114  
      * Constant for the default variable suffix.
 115  
      */
 116  1
     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(final Object source, final Map<String, V> valueMap) {
 150  2
         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(final Object source, final Map<String, V> valueMap, final String prefix, final String suffix) {
 167  3
         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(final Object source, final Properties valueProperties) {
 179  1
         if (valueProperties == null) {
 180  0
             return source.toString();
 181  
         }
 182  1
         final Map<String,String> valueMap = new HashMap<String,String>();
 183  1
         final Enumeration<?> propNames = valueProperties.propertyNames();
 184  62
         while (propNames.hasMoreElements()) {
 185  61
             final String propName = (String)propNames.nextElement();
 186  61
             final String propValue = valueProperties.getProperty(propName);
 187  61
             valueMap.put(propName, propValue);
 188  61
         }
 189  1
         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(final Object source) {
 200  1
         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  5
         this((StrLookup<?>) null, DEFAULT_PREFIX, DEFAULT_SUFFIX, DEFAULT_ESCAPE);
 210  5
     }
 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(final Map<String, V> valueMap) {
 220  43
         this(StrLookup.mapLookup(valueMap), DEFAULT_PREFIX, DEFAULT_SUFFIX, DEFAULT_ESCAPE);
 221  43
     }
 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(final Map<String, V> valueMap, final String prefix, final String suffix) {
 233  4
         this(StrLookup.mapLookup(valueMap), prefix, suffix, DEFAULT_ESCAPE);
 234  4
     }
 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(final Map<String, V> valueMap, final String prefix, final String suffix, final char escape) {
 247  1
         this(StrLookup.mapLookup(valueMap), prefix, suffix, escape);
 248  1
     }
 249  
 
 250  
     /**
 251  
      * Creates a new instance and initializes it.
 252  
      *
 253  
      * @param variableResolver  the variable resolver, may be null
 254  
      */
 255  
     public StrSubstitutor(final StrLookup<?> variableResolver) {
 256  1
         this(variableResolver, DEFAULT_PREFIX, DEFAULT_SUFFIX, DEFAULT_ESCAPE);
 257  1
     }
 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  5
     public StrSubstitutor(final StrLookup<?> variableResolver, final String prefix, final String suffix, final char escape) {
 269  5
         this.setVariableResolver(variableResolver);
 270  5
         this.setVariablePrefix(prefix);
 271  5
         this.setVariableSuffix(suffix);
 272  5
         this.setEscapeChar(escape);
 273  5
     }
 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  49
             final StrLookup<?> variableResolver, final StrMatcher prefixMatcher, final StrMatcher suffixMatcher, final char escape) {
 286  49
         this.setVariableResolver(variableResolver);
 287  49
         this.setVariablePrefixMatcher(prefixMatcher);
 288  49
         this.setVariableSuffixMatcher(suffixMatcher);
 289  49
         this.setEscapeChar(escape);
 290  49
     }
 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(final String source) {
 301  44
         if (source == null) {
 302  1
             return null;
 303  
         }
 304  43
         final StrBuilder buf = new StrBuilder(source);
 305  43
         if (substitute(buf, 0, source.length()) == false) {
 306  20
             return source;
 307  
         }
 308  22
         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(final String source, final int offset, final int length) {
 324  13
         if (source == null) {
 325  1
             return null;
 326  
         }
 327  12
         final StrBuilder buf = new StrBuilder(length).append(source, offset, length);
 328  12
         if (substitute(buf, 0, length) == false) {
 329  1
             return source.substring(offset, offset + length);
 330  
         }
 331  11
         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(final char[] source) {
 344  14
         if (source == null) {
 345  1
             return null;
 346  
         }
 347  13
         final StrBuilder buf = new StrBuilder(source.length).append(source);
 348  13
         substitute(buf, 0, source.length);
 349  13
         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(final char[] source, final int offset, final int length) {
 366  12
         if (source == null) {
 367  1
             return null;
 368  
         }
 369  11
         final StrBuilder buf = new StrBuilder(length).append(source, offset, length);
 370  11
         substitute(buf, 0, length);
 371  11
         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(final StringBuffer source) {
 384  14
         if (source == null) {
 385  1
             return null;
 386  
         }
 387  13
         final StrBuilder buf = new StrBuilder(source.length()).append(source);
 388  13
         substitute(buf, 0, buf.length());
 389  13
         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(final StringBuffer source, final int offset, final int length) {
 406  12
         if (source == null) {
 407  1
             return null;
 408  
         }
 409  11
         final StrBuilder buf = new StrBuilder(length).append(source, offset, length);
 410  11
         substitute(buf, 0, length);
 411  11
         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(final StrBuilder source) {
 424  14
         if (source == null) {
 425  1
             return null;
 426  
         }
 427  13
         final StrBuilder buf = new StrBuilder(source.length()).append(source);
 428  13
         substitute(buf, 0, buf.length());
 429  13
         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(final StrBuilder source, final int offset, final int length) {
 446  12
         if (source == null) {
 447  1
             return null;
 448  
         }
 449  11
         final StrBuilder buf = new StrBuilder(length).append(source, offset, length);
 450  11
         substitute(buf, 0, length);
 451  11
         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(final Object source) {
 464  20
         if (source == null) {
 465  1
             return null;
 466  
         }
 467  19
         final StrBuilder buf = new StrBuilder().append(source);
 468  19
         substitute(buf, 0, buf.length());
 469  19
         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(final StringBuffer source) {
 482  14
         if (source == null) {
 483  1
             return false;
 484  
         }
 485  13
         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(final StringBuffer source, final int offset, final int length) {
 502  25
         if (source == null) {
 503  1
             return false;
 504  
         }
 505  24
         final StrBuilder buf = new StrBuilder(length).append(source, offset, length);
 506  24
         if (substitute(buf, 0, length) == false) {
 507  0
             return false;
 508  
         }
 509  24
         source.replace(offset, offset + length, buf.toString());
 510  24
         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(final StrBuilder source) {
 522  34
         if (source == null) {
 523  1
             return false;
 524  
         }
 525  33
         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(final StrBuilder source, final int offset, final int length) {
 541  12
         if (source == null) {
 542  1
             return false;
 543  
         }
 544  11
         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(final StrBuilder buf, final int offset, final int length) {
 563  224
         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(final StrBuilder buf, final int offset, final int length, List<String> priorVariables) {
 579  538
         final StrMatcher prefixMatcher = getVariablePrefixMatcher();
 580  538
         final StrMatcher suffixMatcher = getVariableSuffixMatcher();
 581  538
         final char escape = getEscapeChar();
 582  
 
 583  538
         final boolean top = priorVariables == null;
 584  538
         boolean altered = false;
 585  538
         int lengthChange = 0;
 586  538
         char[] chars = buf.buffer;
 587  538
         int bufEnd = offset + length;
 588  538
         int pos = offset;
 589  6395
         while (pos < bufEnd) {
 590  5861
             final int startMatchLen = prefixMatcher.isMatch(chars, pos, offset,
 591  
                     bufEnd);
 592  5861
             if (startMatchLen == 0) {
 593  5428
                 pos++;
 594  
             } else {
 595  
                 // found variable start marker
 596  433
                 if (pos > offset && chars[pos - 1] == escape) {
 597  
                     // escaped
 598  48
                     buf.deleteCharAt(pos - 1);
 599  48
                     chars = buf.buffer; // in case buffer was altered
 600  48
                     lengthChange--;
 601  48
                     altered = true;
 602  48
                     bufEnd--;
 603  
                 } else {
 604  
                     // find suffix
 605  385
                     final int startPos = pos;
 606  385
                     pos += startMatchLen;
 607  385
                     int endMatchLen = 0;
 608  385
                     int nestedVarCount = 0;
 609  3206
                     while (pos < bufEnd) {
 610  3202
                         if (isEnableSubstitutionInVariables()
 611  
                                 && (endMatchLen = prefixMatcher.isMatch(chars,
 612  
                                         pos, offset, bufEnd)) != 0) {
 613  
                             // found a nested variable start
 614  5
                             nestedVarCount++;
 615  5
                             pos += endMatchLen;
 616  5
                             continue;
 617  
                         }
 618  
 
 619  3197
                         endMatchLen = suffixMatcher.isMatch(chars, pos, offset,
 620  
                                 bufEnd);
 621  3197
                         if (endMatchLen == 0) {
 622  2811
                             pos++;
 623  
                         } else {
 624  
                             // found variable end marker
 625  386
                             if (nestedVarCount == 0) {
 626  381
                                 String varName = new String(chars, startPos
 627  
                                         + startMatchLen, pos - startPos
 628  
                                         - startMatchLen);
 629  381
                                 if (isEnableSubstitutionInVariables()) {
 630  10
                                     final StrBuilder bufName = new StrBuilder(varName);
 631  10
                                     substitute(bufName, 0, bufName.length());
 632  10
                                     varName = bufName.toString();
 633  
                                 }
 634  381
                                 pos += endMatchLen;
 635  381
                                 final int endPos = pos;
 636  
 
 637  
                                 // on the first call initialize priorVariables
 638  381
                                 if (priorVariables == null) {
 639  197
                                     priorVariables = new ArrayList<String>();
 640  197
                                     priorVariables.add(new String(chars,
 641  
                                             offset, length));
 642  
                                 }
 643  
 
 644  
                                 // handle cyclic substitution
 645  381
                                 checkCyclicSubstitution(varName, priorVariables);
 646  380
                                 priorVariables.add(varName);
 647  
 
 648  
                                 // resolve the variable
 649  380
                                 final String varValue = resolveVariable(varName, buf,
 650  
                                         startPos, endPos);
 651  380
                                 if (varValue != null) {
 652  
                                     // recursive replace
 653  314
                                     final int varLen = varValue.length();
 654  314
                                     buf.replace(startPos, endPos, varValue);
 655  314
                                     altered = true;
 656  314
                                     int change = substitute(buf, startPos,
 657  
                                             varLen, priorVariables);
 658  311
                                     change = change
 659  
                                             + varLen - (endPos - startPos);
 660  311
                                     pos += change;
 661  311
                                     bufEnd += change;
 662  311
                                     lengthChange += change;
 663  311
                                     chars = buf.buffer; // in case buffer was
 664  
                                                         // altered
 665  
                                 }
 666  
 
 667  
                                 // remove variable from the cyclic stack
 668  377
                                 priorVariables
 669  
                                         .remove(priorVariables.size() - 1);
 670  377
                                 break;
 671  
                             } else {
 672  5
                                 nestedVarCount--;
 673  5
                                 pos += endMatchLen;
 674  
                             }
 675  
                         }
 676  
                     }
 677  
                 }
 678  
             }
 679  5857
         }
 680  534
         if (top) {
 681  223
             return altered ? 1 : 0;
 682  
         }
 683  311
         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(final String varName, final List<String> priorVariables) {
 693  381
         if (priorVariables.contains(varName) == false) {
 694  380
             return;
 695  
         }
 696  1
         final StrBuilder buf = new StrBuilder(256);
 697  1
         buf.append("Infinite loop in property interpolation of ");
 698  1
         buf.append(priorVariables.remove(0));
 699  1
         buf.append(": ");
 700  1
         buf.appendWithSeparators(priorVariables, "->");
 701  1
         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(final String variableName, final StrBuilder buf, final int startPos, final int endPos) {
 722  379
         final StrLookup<?> resolver = getVariableResolver();
 723  379
         if (resolver == null) {
 724  2
             return null;
 725  
         }
 726  377
         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  540
         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(final char escapeCharacter) {
 748  55
         this.escapeChar = escapeCharacter;
 749  55
     }
 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  544
         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(final StrMatcher prefixMatcher) {
 778  58
         if (prefixMatcher == null) {
 779  1
             throw new IllegalArgumentException("Variable prefix matcher must not be null!");
 780  
         }
 781  57
         this.prefixMatcher = prefixMatcher;
 782  57
         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(final char prefix) {
 796  1
         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(final String prefix) {
 810  7
        if (prefix == null) {
 811  1
             throw new IllegalArgumentException("Variable prefix must not be null!");
 812  
         }
 813  6
         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  544
         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(final StrMatcher suffixMatcher) {
 843  58
         if (suffixMatcher == null) {
 844  1
             throw new IllegalArgumentException("Variable suffix matcher must not be null!");
 845  
         }
 846  57
         this.suffixMatcher = suffixMatcher;
 847  57
         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(final char suffix) {
 861  1
         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(final String suffix) {
 875  7
        if (suffix == null) {
 876  1
             throw new IllegalArgumentException("Variable suffix must not be null!");
 877  
         }
 878  6
         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  379
         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(final StrLookup<?> variableResolver) {
 898  54
         this.variableResolver = variableResolver;
 899  54
     }
 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  3583
         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  
             final boolean enableSubstitutionInVariables) {
 924  2
         this.enableSubstitutionInVariables = enableSubstitutionInVariables;
 925  2
     }
 926  
 }