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.configuration2.interpol; 018 019import java.lang.reflect.Array; 020import java.util.ArrayList; 021import java.util.Collection; 022import java.util.Collections; 023import java.util.HashMap; 024import java.util.Iterator; 025import java.util.List; 026import java.util.Map; 027import java.util.Objects; 028import java.util.Properties; 029import java.util.Set; 030import java.util.concurrent.ConcurrentHashMap; 031import java.util.concurrent.CopyOnWriteArrayList; 032import java.util.function.Function; 033 034import org.apache.commons.text.StringSubstitutor; 035 036/** 037 * <p> 038 * A class that handles interpolation (variable substitution) for configuration objects. 039 * </p> 040 * <p> 041 * Each instance of {@code AbstractConfiguration} is associated with an object of this class. All interpolation tasks 042 * are delegated to this object. 043 * </p> 044 * <p> 045 * {@code ConfigurationInterpolator} internally uses the {@code StringSubstitutor} class from 046 * <a href="https://commons.apache.org/text">Commons Text</a>. Thus it supports the same syntax of variable expressions. 047 * </p> 048 * <p> 049 * The basic idea of this class is that it can maintain a set of primitive {@link Lookup} objects, each of which is 050 * identified by a special prefix. The variables to be processed have the form {@code ${prefix:name}}. 051 * {@code ConfigurationInterpolator} will extract the prefix and determine, which primitive lookup object is registered 052 * for it. Then the name of the variable is passed to this object to obtain the actual value. It is also possible to 053 * define an arbitrary number of default lookup objects, which are used for variables that do not have a prefix or that 054 * cannot be resolved by their associated lookup object. When adding default lookup objects their order matters; they 055 * are queried in this order, and the first non-<b>null</b> variable value is used. 056 * </p> 057 * <p> 058 * After an instance has been created it does not contain any {@code Lookup} objects. The current set of lookup objects 059 * can be modified using the {@code registerLookup()} and {@code deregisterLookup()} methods. Default lookup objects 060 * (that are invoked for variables without a prefix) can be added or removed with the {@code addDefaultLookup()} and 061 * {@code removeDefaultLookup()} methods respectively. (When a {@code ConfigurationInterpolator} instance is created by 062 * a configuration object, a default lookup object is added pointing to the configuration itself, so that variables are 063 * resolved using the configuration's properties.) 064 * </p> 065 * <p> 066 * The default usage scenario is that on a fully initialized instance the {@code interpolate()} method is called. It is 067 * passed an object value which may contain variables. All these variables are substituted if they can be resolved. The 068 * result is the passed in value with variables replaced. Alternatively, the {@code resolve()} method can be called to 069 * obtain the values of specific variables without performing interpolation. 070 * </p> 071 * <p><strong>String Conversion</strong></p> 072 * <p> 073 * When variables are part of larger interpolated strings, the variable values, which can be of any type, must be 074 * converted to strings to produce the full result. Each interpolator instance has a configurable 075 * {@link #setStringConverter(Function) string converter} to perform this conversion. The default implementation of this 076 * function simply uses the value's {@code toString} method in the majority of cases. However, for maximum 077 * consistency with 078 * {@link org.apache.commons.configuration2.convert.DefaultConversionHandler DefaultConversionHandler}, when a variable 079 * value is a container type (such as a collection or array), then only the first element of the container is converted 080 * to a string instead of the container itself. For example, if the variable {@code x} resolves to the integer array 081 * {@code [1, 2, 3]}, then the string <code>"my value = ${x}"</code> will by default be interpolated to 082 * {@code "my value = 1"}. 083 * </p> 084 * <p> 085 * <strong>Implementation note:</strong> This class is thread-safe. Lookup objects can be added or removed at any time 086 * concurrent to interpolation operations. 087 * </p> 088 * 089 * @since 1.4 090 */ 091public class ConfigurationInterpolator { 092 093 /** 094 * Name of the system property used to determine the lookups added by the 095 * {@link #getDefaultPrefixLookups()} method. Use of this property is only required 096 * in cases where the set of default lookups must be modified. 097 * 098 * @since 2.8.0 099 */ 100 public static final String DEFAULT_PREFIX_LOOKUPS_PROPERTY = 101 "org.apache.commons.configuration2.interpol.ConfigurationInterpolator.defaultPrefixLookups"; 102 103 /** Constant for the prefix separator. */ 104 private static final char PREFIX_SEPARATOR = ':'; 105 106 /** The variable prefix. */ 107 private static final String VAR_START = "${"; 108 109 /** The length of {@link #VAR_START}. */ 110 private static final int VAR_START_LENGTH = VAR_START.length(); 111 112 /** The variable suffix. */ 113 private static final String VAR_END = "}"; 114 115 /** The length of {@link #VAR_END}. */ 116 private static final int VAR_END_LENGTH = VAR_END.length(); 117 118 /** A map with the currently registered lookup objects. */ 119 private final Map<String, Lookup> prefixLookups; 120 121 /** Stores the default lookup objects. */ 122 private final List<Lookup> defaultLookups; 123 124 /** The helper object performing variable substitution. */ 125 private final StringSubstitutor substitutor; 126 127 /** Stores a parent interpolator objects if the interpolator is nested hierarchically. */ 128 private volatile ConfigurationInterpolator parentInterpolator; 129 130 /** Function used to convert interpolated values to strings. */ 131 private volatile Function<Object, String> stringConverter = DefaultStringConverter.INSTANCE; 132 133 /** 134 * Creates a new instance of {@code ConfigurationInterpolator}. 135 */ 136 public ConfigurationInterpolator() { 137 prefixLookups = new ConcurrentHashMap<>(); 138 defaultLookups = new CopyOnWriteArrayList<>(); 139 substitutor = initSubstitutor(); 140 } 141 142 /** 143 * Creates a new instance based on the properties in the given specification object. 144 * 145 * @param spec the {@code InterpolatorSpecification} 146 * @return the newly created instance 147 */ 148 private static ConfigurationInterpolator createInterpolator(final InterpolatorSpecification spec) { 149 final ConfigurationInterpolator ci = new ConfigurationInterpolator(); 150 ci.addDefaultLookups(spec.getDefaultLookups()); 151 ci.registerLookups(spec.getPrefixLookups()); 152 ci.setParentInterpolator(spec.getParentInterpolator()); 153 ci.setStringConverter(spec.getStringConverter()); 154 return ci; 155 } 156 157 /** 158 * Extracts the variable name from a value that consists of a single variable. 159 * 160 * @param strValue the value 161 * @return the extracted variable name 162 */ 163 private static String extractVariableName(final String strValue) { 164 return strValue.substring(VAR_START_LENGTH, strValue.length() - VAR_END_LENGTH); 165 } 166 167 /** 168 * Creates a new {@code ConfigurationInterpolator} instance based on the passed in specification object. If the 169 * {@code InterpolatorSpecification} already contains a {@code ConfigurationInterpolator} object, it is used directly. 170 * Otherwise, a new instance is created and initialized with the properties stored in the specification. 171 * 172 * @param spec the {@code InterpolatorSpecification} (must not be <b>null</b>) 173 * @return the {@code ConfigurationInterpolator} obtained or created based on the given specification 174 * @throws IllegalArgumentException if the specification is <b>null</b> 175 * @since 2.0 176 */ 177 public static ConfigurationInterpolator fromSpecification(final InterpolatorSpecification spec) { 178 if (spec == null) { 179 throw new IllegalArgumentException("InterpolatorSpecification must not be null!"); 180 } 181 return spec.getInterpolator() != null ? spec.getInterpolator() : createInterpolator(spec); 182 } 183 184 /** 185 * Gets a map containing the default prefix lookups. Every configuration object derived from 186 * {@code AbstractConfiguration} is by default initialized with a {@code ConfigurationInterpolator} containing 187 * these {@code Lookup} objects and their prefixes. The map cannot be modified. 188 * 189 * <p> 190 * All of the lookups present in the returned map are from {@link DefaultLookups}. However, not all of the 191 * available lookups are included by default. Specifically, lookups that can execute code (e.g., 192 * {@link DefaultLookups#SCRIPT SCRIPT}) and those that can result in contact with remote servers (e.g., 193 * {@link DefaultLookups#URL URL} and {@link DefaultLookups#DNS DNS}) are not included. If this behavior 194 * must be modified, users can define the {@value #DEFAULT_PREFIX_LOOKUPS_PROPERTY} system property 195 * with a comma-separated list of {@link DefaultLookups} enum names to be included in the set of defaults. 196 * For example, setting this system property to {@code "BASE64_ENCODER,ENVIRONMENT"} will only include the 197 * {@link DefaultLookups#BASE64_ENCODER BASE64_ENCODER} and 198 * {@link DefaultLookups#ENVIRONMENT ENVIRONMENT} lookups. Setting the property to the empty string will 199 * cause no defaults to be configured. 200 * </p> 201 * 202 * <table> 203 * <caption>Default Lookups</caption> 204 * <tr> 205 * <th>Prefix</th> 206 * <th>Lookup</th> 207 * </tr> 208 * <tr> 209 * <td>"base64Decoder"</td> 210 * <td>{@link DefaultLookups#BASE64_DECODER BASE64_DECODER}</td> 211 * </tr> 212 * <tr> 213 * <td>"base64Encoder"</td> 214 * <td>{@link DefaultLookups#BASE64_ENCODER BASE64_ENCODER}</td> 215 * </tr> 216 * <tr> 217 * <td>"const"</td> 218 * <td>{@link DefaultLookups#CONST CONST}</td> 219 * </tr> 220 * <tr> 221 * <td>"date"</td> 222 * <td>{@link DefaultLookups#DATE DATE}</td> 223 * </tr> 224 * <tr> 225 * <td>"env"</td> 226 * <td>{@link DefaultLookups#ENVIRONMENT ENVIRONMENT}</td> 227 * </tr> 228 * <tr> 229 * <td>"file"</td> 230 * <td>{@link DefaultLookups#FILE FILE}</td> 231 * </tr> 232 * <tr> 233 * <td>"java"</td> 234 * <td>{@link DefaultLookups#JAVA JAVA}</td> 235 * </tr> 236 * <tr> 237 * <td>"localhost"</td> 238 * <td>{@link DefaultLookups#LOCAL_HOST LOCAL_HOST}</td> 239 * </tr> 240 * <tr> 241 * <td>"properties"</td> 242 * <td>{@link DefaultLookups#PROPERTIES PROPERTIES}</td> 243 * </tr> 244 * <tr> 245 * <td>"resourceBundle"</td> 246 * <td>{@link DefaultLookups#RESOURCE_BUNDLE RESOURCE_BUNDLE}</td> 247 * </tr> 248 * <tr> 249 * <td>"sys"</td> 250 * <td>{@link DefaultLookups#SYSTEM_PROPERTIES SYSTEM_PROPERTIES}</td> 251 * </tr> 252 * <tr> 253 * <td>"urlDecoder"</td> 254 * <td>{@link DefaultLookups#URL_DECODER URL_DECODER}</td> 255 * </tr> 256 * <tr> 257 * <td>"urlEncoder"</td> 258 * <td>{@link DefaultLookups#URL_ENCODER URL_ENCODER}</td> 259 * </tr> 260 * <tr> 261 * <td>"xml"</td> 262 * <td>{@link DefaultLookups#XML XML}</td> 263 * </tr> 264 * </table> 265 * 266 * <table> 267 * <caption>Additional Lookups (not included by default)</caption> 268 * <tr> 269 * <th>Prefix</th> 270 * <th>Lookup</th> 271 * </tr> 272 * <tr> 273 * <td>"dns"</td> 274 * <td>{@link DefaultLookups#DNS DNS}</td> 275 * </tr> 276 * <tr> 277 * <td>"url"</td> 278 * <td>{@link DefaultLookups#URL URL}</td> 279 * </tr> 280 * <tr> 281 * <td>"script"</td> 282 * <td>{@link DefaultLookups#SCRIPT SCRIPT}</td> 283 * </tr> 284 * </table> 285 * 286 * @return a map with the default prefix {@code Lookup} objects and their prefixes 287 * @since 2.0 288 */ 289 public static Map<String, Lookup> getDefaultPrefixLookups() { 290 return DefaultPrefixLookupsHolder.INSTANCE.getDefaultPrefixLookups(); 291 } 292 293 /** 294 * Utility method for obtaining a {@code Lookup} object in a safe way. This method always returns a non-<b>null</b> 295 * {@code Lookup} object. If the passed in {@code Lookup} is not <b>null</b>, it is directly returned. Otherwise, result 296 * is a dummy {@code Lookup} which does not provide any values. 297 * 298 * @param lookup the {@code Lookup} to check 299 * @return a non-<b>null</b> {@code Lookup} object 300 * @since 2.0 301 */ 302 public static Lookup nullSafeLookup(Lookup lookup) { 303 if (lookup == null) { 304 lookup = DummyLookup.INSTANCE; 305 } 306 return lookup; 307 } 308 309 /** 310 * Adds a default {@code Lookup} object. Default {@code Lookup} objects are queried (in the order they were added) for 311 * all variables without a special prefix. If no default {@code Lookup} objects are present, such variables won't be 312 * processed. 313 * 314 * @param defaultLookup the default {@code Lookup} object to be added (must not be <b>null</b>) 315 * @throws IllegalArgumentException if the {@code Lookup} object is <b>null</b> 316 */ 317 public void addDefaultLookup(final Lookup defaultLookup) { 318 defaultLookups.add(defaultLookup); 319 } 320 321 /** 322 * Adds all {@code Lookup} objects in the given collection as default lookups. The collection can be <b>null</b>, then 323 * this method has no effect. It must not contain <b>null</b> entries. 324 * 325 * @param lookups the {@code Lookup} objects to be added as default lookups 326 * @throws IllegalArgumentException if the collection contains a <b>null</b> entry 327 */ 328 public void addDefaultLookups(final Collection<? extends Lookup> lookups) { 329 if (lookups != null) { 330 defaultLookups.addAll(lookups); 331 } 332 } 333 334 /** 335 * Deregisters the {@code Lookup} object for the specified prefix at this instance. It will be removed from this 336 * instance. 337 * 338 * @param prefix the variable prefix 339 * @return a flag whether for this prefix a lookup object had been registered 340 */ 341 public boolean deregisterLookup(final String prefix) { 342 return prefixLookups.remove(prefix) != null; 343 } 344 345 /** 346 * Obtains the lookup object for the specified prefix. This method is called by the {@code lookup()} method. This 347 * implementation will check whether a lookup object is registered for the given prefix. If not, a <b>null</b> lookup 348 * object will be returned (never <b>null</b>). 349 * 350 * @param prefix the prefix 351 * @return the lookup object to be used for this prefix 352 */ 353 protected Lookup fetchLookupForPrefix(final String prefix) { 354 return nullSafeLookup(prefixLookups.get(prefix)); 355 } 356 357 /** 358 * Gets a collection with the default {@code Lookup} objects added to this {@code ConfigurationInterpolator}. These 359 * objects are not associated with a variable prefix. The returned list is a snapshot copy of the internal collection of 360 * default lookups; so manipulating it does not affect this instance. 361 * 362 * @return the default lookup objects 363 */ 364 public List<Lookup> getDefaultLookups() { 365 return new ArrayList<>(defaultLookups); 366 } 367 368 /** 369 * Gets a map with the currently registered {@code Lookup} objects and their prefixes. This is a snapshot copy of the 370 * internally used map. So modifications of this map do not effect this instance. 371 * 372 * @return a copy of the map with the currently registered {@code Lookup} objects 373 */ 374 public Map<String, Lookup> getLookups() { 375 return new HashMap<>(prefixLookups); 376 } 377 378 /** 379 * Gets the parent {@code ConfigurationInterpolator}. 380 * 381 * @return the parent {@code ConfigurationInterpolator} (can be <b>null</b>) 382 */ 383 public ConfigurationInterpolator getParentInterpolator() { 384 return this.parentInterpolator; 385 } 386 387 /** 388 * Creates and initializes a {@code StringSubstitutor} object which is used for variable substitution. This 389 * {@code StringSubstitutor} is assigned a specialized lookup object implementing the correct variable resolving 390 * algorithm. 391 * 392 * @return the {@code StringSubstitutor} used by this object 393 */ 394 private StringSubstitutor initSubstitutor() { 395 return new StringSubstitutor(key -> { 396 final Object value = resolve(key); 397 return value != null 398 ? stringConverter.apply(value) 399 : null; 400 }); 401 } 402 403 /** 404 * Performs interpolation of the passed in value. If the value is of type {@code String}, this method checks 405 * whether it contains variables. If so, all variables are replaced by their current values (if possible). For 406 * non string arguments, the value is returned without changes. In the special case where the value is a string 407 * consisting of a single variable reference, the interpolated variable value is <em>not</em> converted to a 408 * string before returning, so that callers can access the raw value. However, if the variable is part of a larger 409 * interpolated string, then the variable value is converted to a string using the configured 410 * {@link #getStringConverter() string converter}. (See the discussion on string conversion in the class 411 * documentation for more details.) 412 * 413 * <p><strong>Examples</strong></p> 414 * <p> 415 * For the following examples, assume that the default string conversion function is in place and that the 416 * variable {@code i} maps to the integer value {@code 42}. 417 * </p> 418 * <pre> 419 * interpolator.interpolate(1) → 1 // non-string argument returned unchanged 420 * interpolator.interpolate("${i}") → 42 // single variable value returned with raw type 421 * interpolator.interpolate("answer = ${i}") → "answer = 42" // variable value converted to string 422 * </pre> 423 * 424 * @param value the value to be interpolated 425 * @return the interpolated value 426 */ 427 public Object interpolate(final Object value) { 428 if (value instanceof String) { 429 final String strValue = (String) value; 430 if (isSingleVariable(strValue)) { 431 final Object resolvedValue = resolveSingleVariable(strValue); 432 if (resolvedValue != null && !(resolvedValue instanceof String)) { 433 // If the value is again a string, it needs no special 434 // treatment; it may also contain further variables which 435 // must be resolved; therefore, the default mechanism is 436 // applied. 437 return resolvedValue; 438 } 439 } 440 return substitutor.replace(strValue); 441 } 442 return value; 443 } 444 445 /** 446 * Sets a flag that variable names can contain other variables. If enabled, variable substitution is also done in 447 * variable names. 448 * 449 * @return the substitution in variables flag 450 */ 451 public boolean isEnableSubstitutionInVariables() { 452 return substitutor.isEnableSubstitutionInVariables(); 453 } 454 455 /** Gets the function used to convert interpolated values to strings. 456 * @return function used to convert interpolated values to strings 457 */ 458 public Function<Object, String> getStringConverter() { 459 return stringConverter; 460 } 461 462 /** Sets the function used to convert interpolated values to strings. Pass 463 * {@code null} to use the default conversion function. 464 * @param stringConverter function used to convert interpolated values to strings 465 * or {@code null} to use the default conversion function 466 */ 467 public void setStringConverter(final Function<Object, String> stringConverter) { 468 this.stringConverter = stringConverter != null 469 ? stringConverter 470 : DefaultStringConverter.INSTANCE; 471 } 472 473 /** 474 * Checks whether a value to be interpolated consists of single, simple variable reference, e.g., 475 * <code>${myvar}</code>. In this case, the variable is resolved directly without using the 476 * {@code StringSubstitutor}. 477 * 478 * @param strValue the value to be interpolated 479 * @return {@code true} if the value contains a single, simple variable reference 480 */ 481 private boolean isSingleVariable(final String strValue) { 482 return strValue.startsWith(VAR_START) 483 && strValue.indexOf(VAR_END, VAR_START_LENGTH) == strValue.length() - VAR_END_LENGTH; 484 } 485 486 /** 487 * Returns an unmodifiable set with the prefixes, for which {@code Lookup} objects are registered at this instance. This 488 * means that variables with these prefixes can be processed. 489 * 490 * @return a set with the registered variable prefixes 491 */ 492 public Set<String> prefixSet() { 493 return Collections.unmodifiableSet(prefixLookups.keySet()); 494 } 495 496 /** 497 * Registers the given {@code Lookup} object for the specified prefix at this instance. From now on this lookup object 498 * will be used for variables that have the specified prefix. 499 * 500 * @param prefix the variable prefix (must not be <b>null</b>) 501 * @param lookup the {@code Lookup} object to be used for this prefix (must not be <b>null</b>) 502 * @throws IllegalArgumentException if either the prefix or the {@code Lookup} object is <b>null</b> 503 */ 504 public void registerLookup(final String prefix, final Lookup lookup) { 505 if (prefix == null) { 506 throw new IllegalArgumentException("Prefix for lookup object must not be null!"); 507 } 508 if (lookup == null) { 509 throw new IllegalArgumentException("Lookup object must not be null!"); 510 } 511 prefixLookups.put(prefix, lookup); 512 } 513 514 /** 515 * Registers all {@code Lookup} objects in the given map with their prefixes at this {@code ConfigurationInterpolator}. 516 * Using this method multiple {@code Lookup} objects can be registered at once. If the passed in map is <b>null</b>, 517 * this method does not have any effect. 518 * 519 * @param lookups the map with lookups to register (may be <b>null</b>) 520 * @throws IllegalArgumentException if the map contains <b>entries</b> 521 */ 522 public void registerLookups(final Map<String, ? extends Lookup> lookups) { 523 if (lookups != null) { 524 prefixLookups.putAll(lookups); 525 } 526 } 527 528 /** 529 * Removes the specified {@code Lookup} object from the list of default {@code Lookup}s. 530 * 531 * @param lookup the {@code Lookup} object to be removed 532 * @return a flag whether this {@code Lookup} object actually existed and was removed 533 */ 534 public boolean removeDefaultLookup(final Lookup lookup) { 535 return defaultLookups.remove(lookup); 536 } 537 538 /** 539 * Resolves the specified variable. This implementation tries to extract a variable prefix from the given variable name 540 * (the first colon (':') is used as prefix separator). It then passes the name of the variable with the prefix stripped 541 * to the lookup object registered for this prefix. If no prefix can be found or if the associated lookup object cannot 542 * resolve this variable, the default lookup objects are used. If this is not successful either and a parent 543 * {@code ConfigurationInterpolator} is available, this object is asked to resolve the variable. 544 * 545 * @param var the name of the variable whose value is to be looked up which may contain a prefix. 546 * @return the value of this variable or <b>null</b> if it cannot be resolved 547 */ 548 public Object resolve(final String var) { 549 if (var == null) { 550 return null; 551 } 552 553 final int prefixPos = var.indexOf(PREFIX_SEPARATOR); 554 if (prefixPos >= 0) { 555 final String prefix = var.substring(0, prefixPos); 556 final String name = var.substring(prefixPos + 1); 557 final Object value = fetchLookupForPrefix(prefix).lookup(name); 558 if (value != null) { 559 return value; 560 } 561 } 562 563 for (final Lookup lookup : defaultLookups) { 564 final Object value = lookup.lookup(var); 565 if (value != null) { 566 return value; 567 } 568 } 569 570 final ConfigurationInterpolator parent = getParentInterpolator(); 571 if (parent != null) { 572 return getParentInterpolator().resolve(var); 573 } 574 return null; 575 } 576 577 /** 578 * Interpolates a string value that consists of a single variable. 579 * 580 * @param strValue the string to be interpolated 581 * @return the resolved value or <b>null</b> if resolving failed 582 */ 583 private Object resolveSingleVariable(final String strValue) { 584 return resolve(extractVariableName(strValue)); 585 } 586 587 /** 588 * Sets the flag whether variable names can contain other variables. This flag corresponds to the 589 * {@code enableSubstitutionInVariables} property of the underlying {@code StringSubstitutor} object. 590 * 591 * @param f the new value of the flag 592 */ 593 public void setEnableSubstitutionInVariables(final boolean f) { 594 substitutor.setEnableSubstitutionInVariables(f); 595 } 596 597 /** 598 * Sets the parent {@code ConfigurationInterpolator}. This object is used if the {@code Lookup} objects registered at 599 * this object cannot resolve a variable. 600 * 601 * @param parentInterpolator the parent {@code ConfigurationInterpolator} object (can be <b>null</b>) 602 */ 603 public void setParentInterpolator(final ConfigurationInterpolator parentInterpolator) { 604 this.parentInterpolator = parentInterpolator; 605 } 606 607 /** 608 * Internal class used to construct the default {@link Lookup} map used by 609 * {@link ConfigurationInterpolator#getDefaultPrefixLookups()}. 610 */ 611 static final class DefaultPrefixLookupsHolder { 612 613 /** Singleton instance, initialized with the system properties. */ 614 static final DefaultPrefixLookupsHolder INSTANCE = new DefaultPrefixLookupsHolder(System.getProperties()); 615 616 /** Default lookup map. */ 617 private final Map<String, Lookup> defaultLookups; 618 619 /** 620 * Constructs a new instance initialized with the given properties. 621 * @param props initialization properties 622 */ 623 DefaultPrefixLookupsHolder(final Properties props) { 624 final Map<String, Lookup> lookups = 625 props.containsKey(ConfigurationInterpolator.DEFAULT_PREFIX_LOOKUPS_PROPERTY) 626 ? parseLookups(props.getProperty(ConfigurationInterpolator.DEFAULT_PREFIX_LOOKUPS_PROPERTY)) 627 : createDefaultLookups(); 628 629 defaultLookups = Collections.unmodifiableMap(lookups); 630 } 631 632 /** 633 * Gets the default prefix lookups map. 634 * @return default prefix lookups map 635 */ 636 Map<String, Lookup> getDefaultPrefixLookups() { 637 return defaultLookups; 638 } 639 640 /** 641 * Create the lookup map used when the user has requested no customization. 642 * @return default lookup map 643 */ 644 private static Map<String, Lookup> createDefaultLookups() { 645 final Map<String, Lookup> lookupMap = new HashMap<>(); 646 647 addLookup(DefaultLookups.BASE64_DECODER, lookupMap); 648 addLookup(DefaultLookups.BASE64_ENCODER, lookupMap); 649 addLookup(DefaultLookups.CONST, lookupMap); 650 addLookup(DefaultLookups.DATE, lookupMap); 651 addLookup(DefaultLookups.ENVIRONMENT, lookupMap); 652 addLookup(DefaultLookups.FILE, lookupMap); 653 addLookup(DefaultLookups.JAVA, lookupMap); 654 addLookup(DefaultLookups.LOCAL_HOST, lookupMap); 655 addLookup(DefaultLookups.PROPERTIES, lookupMap); 656 addLookup(DefaultLookups.RESOURCE_BUNDLE, lookupMap); 657 addLookup(DefaultLookups.SYSTEM_PROPERTIES, lookupMap); 658 addLookup(DefaultLookups.URL_DECODER, lookupMap); 659 addLookup(DefaultLookups.URL_ENCODER, lookupMap); 660 addLookup(DefaultLookups.XML, lookupMap); 661 662 return lookupMap; 663 } 664 665 /** 666 * Constructs a lookup map by parsing the given string. The string is expected to contain 667 * comma or space-separated names of values from the {@link DefaultLookups} enum. 668 * @param str string to parse; not null 669 * @return lookup map parsed from the given string 670 * @throws IllegalArgumentException if the string does not contain a valid default lookup 671 * definition 672 */ 673 private static Map<String, Lookup> parseLookups(final String str) { 674 final Map<String, Lookup> lookupMap = new HashMap<>(); 675 676 try { 677 for (final String lookupName : str.split("[\\s,]+")) { 678 if (!lookupName.isEmpty()) { 679 addLookup(DefaultLookups.valueOf(lookupName.toUpperCase()), lookupMap); 680 } 681 } 682 } catch (final IllegalArgumentException exc) { 683 throw new IllegalArgumentException("Invalid default lookups definition: " + str, exc); 684 } 685 686 return lookupMap; 687 } 688 689 /** 690 * Add the prefix and lookup from {@code lookup} to {@code map}. 691 * @param lookup lookup to add 692 * @param map map to add to 693 */ 694 private static void addLookup(final DefaultLookups lookup, final Map<String, Lookup> map) { 695 map.put(lookup.getPrefix(), lookup.getLookup()); 696 } 697 } 698 699 /** Class encapsulating the default logic to convert resolved variable values into strings. 700 * This class is thread-safe. 701 */ 702 private static final class DefaultStringConverter implements Function<Object, String> { 703 704 /** Shared instance. */ 705 static final DefaultStringConverter INSTANCE = new DefaultStringConverter(); 706 707 /** {@inheritDoc} */ 708 @Override 709 public String apply(final Object obj) { 710 return Objects.toString(extractSimpleValue(obj), null); 711 } 712 713 /** Attempt to extract a simple value from {@code obj} for use in string conversion. 714 * If the input represents a collection of some sort (e.g., an iterable or array), 715 * the first item from the collection is returned. 716 * @param obj input object 717 * @return extracted simple object 718 */ 719 private Object extractSimpleValue(final Object obj) { 720 if (!(obj instanceof String)) { 721 if (obj instanceof Iterable) { 722 return nextOrNull(((Iterable<?>) obj).iterator()); 723 } 724 if (obj instanceof Iterator) { 725 return nextOrNull((Iterator<?>) obj); 726 } 727 if (obj.getClass().isArray()) { 728 return Array.getLength(obj) > 0 729 ? Array.get(obj, 0) 730 : null; 731 } 732 } 733 return obj; 734 } 735 736 /** Return the next value from {@code it} or {@code null} if no values remain. 737 * @param <T> iterated type 738 * @param it iterator 739 * @return next value from {@code it} or {@code null} if no values remain 740 */ 741 private <T> T nextOrNull(final Iterator<T> it) { 742 return it.hasNext() 743 ? it.next() 744 : null; 745 } 746 } 747}