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