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