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