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