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