Coverage Report - org.kuali.spring.util.PlaceholderStringResolver
 
Classes in this File Line Coverage Branch Coverage Complexity
PlaceholderStringResolver
100%
159/159
95%
40/42
1.879
 
 1  
 package org.kuali.spring.util;
 2  
 
 3  
 import java.util.ArrayList;
 4  
 import java.util.Collection;
 5  
 import java.util.HashMap;
 6  
 import java.util.HashSet;
 7  
 import java.util.List;
 8  
 import java.util.Map;
 9  
 import java.util.Set;
 10  
 
 11  
 import org.slf4j.Logger;
 12  
 import org.slf4j.LoggerFactory;
 13  
 import org.springframework.beans.factory.config.PropertyPlaceholderConfigurer;
 14  
 import org.springframework.util.Assert;
 15  
 import org.springframework.util.StringUtils;
 16  
 
 17  
 /**
 18  
  * This class contains logic for recursively parsing/expanding PlaceholderString's. A PlaceholderString is a String that
 19  
  * contains Placeholder object(s).<br>
 20  
  * 
 21  
  * Recursion is used since the text of a Placeholder may itself be a PlaceholderString.<br>
 22  
  * 
 23  
  * Consider the text:<br>
 24  
  * 
 25  
  * <pre>
 26  
  * A ${cat} is ${${cat}.speed}.
 27  
  * </pre>
 28  
  * 
 29  
  * The PlaceholderString representing this text contains two Placeholders:<br>
 30  
  * 
 31  
  * <pre>
 32  
  * 1 - ${cat}
 33  
  * 2 - ${${cat}.speed}
 34  
  * </pre>
 35  
  * 
 36  
  * The second Placeholder has a nested PlaceholderString <code>${cat}.speed</code> which must be resolved in order to
 37  
  * locate the correct replacement value.
 38  
  * 
 39  
  * Given a properties file (or other property source) with the following mappings:<br>
 40  
  * 
 41  
  * <pre>
 42  
  * cat=cheetah
 43  
  * cheetah.speed=fast
 44  
  * </pre>
 45  
  * 
 46  
  * The text <code>A ${cat} is ${${cat}.speed}.</code> should be resolved to <code>A cheetah is fast.</code><br>
 47  
  * 
 48  
  * The object graph created to model the data is:<br>
 49  
  * 
 50  
  * <pre>
 51  
  * PlaceholderString 
 52  
  *     + text: A ${cat} is ${${cat}.speed}.
 53  
  *     + resolvedText: A cheetah is fast.
 54  
  *  - Placeholder 
 55  
  *     + value: cheetah
 56  
  *     - PlaceholderString
 57  
  *       + text: cat
 58  
  *       + resolvedText: cat
 59  
  *  - Placeholder 
 60  
  *     + value: fast
 61  
  *     - PlaceholderString
 62  
  *           + text: ${cat}.speed
 63  
  *           + resolvedText: cheetah.speed
 64  
  *        - Placeholder 
 65  
  *           + value: cheetah
 66  
  *           - PlaceholderString
 67  
  *             + text: cat
 68  
  *             + resolvedText: cat
 69  
  * </pre>
 70  
  */
 71  
 public class PlaceholderStringResolver {
 72  
 
 73  1
         private static final Logger logger = LoggerFactory.getLogger(PlaceholderStringResolver.class);
 74  
 
 75  1
         private static final Map<String, String> wellKnownSimplePrefixes = new HashMap<String, String>(4);
 76  
         public static final boolean DEFAULT_IS_IGNORE_UNRESOLVABLE_PLACEHOLDERS = false;
 77  
 
 78  
         static {
 79  1
                 wellKnownSimplePrefixes.put("}", "{");
 80  1
                 wellKnownSimplePrefixes.put("]", "[");
 81  1
                 wellKnownSimplePrefixes.put(")", "(");
 82  1
         }
 83  
 
 84  15
         PropertyLogger plogger = new PropertyLogger();
 85  
         String placeholderPrefix;
 86  
         String placeholderSuffix;
 87  
         String valueSeparator;
 88  
         boolean ignoreUnresolvablePlaceholders;
 89  
         String simplePrefix;
 90  
 
 91  
         public PlaceholderStringResolver() {
 92  13
                 this(DEFAULT_IS_IGNORE_UNRESOLVABLE_PLACEHOLDERS);
 93  13
         }
 94  
 
 95  
         public PlaceholderStringResolver(boolean ignoreUnresolvablePlaceholders) {
 96  13
                 this(PropertyPlaceholderConfigurer.DEFAULT_PLACEHOLDER_PREFIX,
 97  
                                 PropertyPlaceholderConfigurer.DEFAULT_PLACEHOLDER_SUFFIX, null, ignoreUnresolvablePlaceholders);
 98  13
         }
 99  
 
 100  
         public PlaceholderStringResolver(String placeholderPrefix, String placeholderSuffix) {
 101  1
                 this(placeholderPrefix, placeholderSuffix, null, DEFAULT_IS_IGNORE_UNRESOLVABLE_PLACEHOLDERS);
 102  1
         }
 103  
 
 104  
         public PlaceholderStringResolver(String placeholderPrefix, String placeholderSuffix, String valueSeparator,
 105  15
                         boolean ignoreUnresolvablePlaceholders) {
 106  
 
 107  15
                 Assert.notNull(placeholderPrefix, "placeholderPrefix must not be null");
 108  15
                 Assert.notNull(placeholderSuffix, "placeholderSuffix must not be null");
 109  
 
 110  15
                 this.placeholderPrefix = placeholderPrefix;
 111  15
                 this.placeholderSuffix = placeholderSuffix;
 112  
 
 113  15
                 String simplePrefixForSuffix = wellKnownSimplePrefixes.get(this.placeholderSuffix);
 114  15
                 if (simplePrefixForSuffix != null && this.placeholderPrefix.endsWith(simplePrefixForSuffix)) {
 115  13
                         this.simplePrefix = simplePrefixForSuffix;
 116  
                 } else {
 117  2
                         this.simplePrefix = this.placeholderPrefix;
 118  
                 }
 119  15
                 this.valueSeparator = valueSeparator;
 120  15
                 this.ignoreUnresolvablePlaceholders = ignoreUnresolvablePlaceholders;
 121  15
         }
 122  
 
 123  
         /**
 124  
          * Return true if the collection is null or has no elements
 125  
          */
 126  
         protected boolean isEmpty(Collection<?> c) {
 127  37
                 return c == null || c.size() == 0;
 128  
         }
 129  
 
 130  
         /**
 131  
          * Trim the prefix and suffix off of the placeholder ie change ${foo}->foo
 132  
          */
 133  
         protected String getTrimmedText(String text) {
 134  52
                 int prefixLength = this.placeholderPrefix.length();
 135  52
                 int suffixLength = this.placeholderSuffix.length();
 136  52
                 int beginIndex = prefixLength;
 137  52
                 int endIndex = text.length() - suffixLength;
 138  52
                 return text.substring(beginIndex, endIndex);
 139  
         }
 140  
 
 141  
         /**
 142  
          * Handle placeholders that have default values supplied with them ie ${jdbc.vendor=mysql}
 143  
          */
 144  
         protected String getDefaultValue(String key, ValueRetriever retriever) {
 145  
                 // If they haven't specified a valueSeparator, we're done
 146  5
                 if (this.valueSeparator == null) {
 147  2
                         return null;
 148  
                 }
 149  
 
 150  
                 // If they supplied a valueSeparator but this key isn't using it, we are done
 151  3
                 int separatorIndex = key.indexOf(this.valueSeparator);
 152  3
                 if (separatorIndex == -1) {
 153  1
                         return null;
 154  
                 }
 155  
 
 156  
                 // Extract 'jdbc.vendor' from 'jdbc.vendor=mysql'
 157  2
                 String actualKey = key.substring(0, separatorIndex);
 158  
 
 159  
                 // Extract 'mysql' from 'jdbc.vendor=mysql'
 160  2
                 String defaultValue = key.substring(separatorIndex + this.valueSeparator.length());
 161  
 
 162  
                 // Give the retriever a chance to locate a value for 'jdbc.vendor'
 163  2
                 String value = retriever.retrieveValue(actualKey);
 164  
 
 165  
                 // If the retriever found something, use it
 166  2
                 if (value != null) {
 167  1
                         return value;
 168  
                 } else {
 169  
                         // Otherwise return the default value
 170  1
                         return defaultValue;
 171  
                 }
 172  
         }
 173  
 
 174  
         /**
 175  
          * Get a value for this key
 176  
          */
 177  
         protected String getRawValue(String key, ValueRetriever retriever) {
 178  
                 // Give the retriever first dibs at locating a value
 179  37
                 String value = retriever.retrieveValue(key);
 180  
 
 181  
                 // The retriever found something, use it
 182  37
                 if (value != null) {
 183  32
                         return value;
 184  
                 }
 185  
 
 186  
                 // There might be a default value
 187  5
                 return getDefaultValue(key, retriever);
 188  
         }
 189  
 
 190  
         /**
 191  
          * Throw an exception unless they've activated the flag for ignoring unresolved placeholders
 192  
          */
 193  
         protected void handleUnresolvedPlaceholder(String placeholderText) {
 194  
                 // Value is null, meaning we could not get find a value for the Placeholder
 195  3
                 if (!this.ignoreUnresolvablePlaceholders) {
 196  
                         // If we are not ignoring unresolved placeholder's, we are done
 197  1
                         throw new IllegalArgumentException("Could not resolve a value for " + placeholderText);
 198  
                 } else {
 199  2
                         logger.trace("Ignoring unresolvable placeholder: '" + placeholderText + "'");
 200  
                 }
 201  2
         }
 202  
 
 203  
         /**
 204  
          * Return the value that should be substituted for the placeholder text
 205  
          */
 206  
         protected String getValue(String key, Placeholder placeholder, ValueRetriever retriever, Set<String> resolving) {
 207  
                 // Get a value for this property key
 208  37
                 String value = getRawValue(key, retriever);
 209  
 
 210  37
                 if (value == null) {
 211  
                         // No value could be located
 212  3
                         String placeholderText = getPlaceholderPrefix() + placeholder.getPlaceholderString().getText()
 213  
                                         + getPlaceholderSuffix();
 214  3
                         handleUnresolvedPlaceholder(placeholderText);
 215  2
                         return placeholderText;
 216  
                 } else {
 217  
                         // Resolve any placeholders in the value we located
 218  34
                         PlaceholderString phs = new PlaceholderString(value);
 219  34
                         resolvePlaceholderString(phs, retriever, resolving);
 220  32
                         return phs.getResolvedText();
 221  
                 }
 222  
         }
 223  
 
 224  
         /**
 225  
          * Return a String that has had all of it's placeholders resolved
 226  
          */
 227  
         public String resolve(String text, ValueRetriever retriever) {
 228  293
                 PlaceholderString phs = new PlaceholderString(text);
 229  293
                 resolve(phs, retriever);
 230  291
                 return phs.getResolvedText();
 231  
         }
 232  
 
 233  
         /**
 234  
          * Resolve any Placeholders in the supplied PlaceholderString. When this method completes,
 235  
          * PlaceholderString.getResolvedText() will return the fully resolved text.
 236  
          */
 237  
         public void resolve(PlaceholderString placeholderString, ValueRetriever retriever) {
 238  293
                 resolvePlaceholderString(placeholderString, retriever, new HashSet<String>());
 239  291
         }
 240  
 
 241  
         /**
 242  
          * When this method completes, PlaceholderString.getResolvedText() will return the fully resolved text
 243  
          */
 244  
         protected void resolvePlaceholderString(PlaceholderString phs, ValueRetriever retriever, Set<String> resolving) {
 245  
                 // Parse the PlaceholderString to find any Placeholder's it may contain
 246  
                 // phs.getPlaceholders() returns any parsed Placeholders
 247  339
                 parse(phs);
 248  
 
 249  
                 // Iterate through the Placeholder objects
 250  339
                 StringBuilder buffer = new StringBuilder(phs.getText());
 251  339
                 int offset = 0;
 252  339
                 for (Placeholder pholder : phs.getPlaceholders()) {
 253  
                         // Resolve each placeholder
 254  38
                         resolvePlaceholder(pholder, retriever, resolving);
 255  
                         // Update the buffer with the new text
 256  34
                         offset += updateBuffer(buffer, pholder, offset);
 257  
                 }
 258  
                 // Store the new text, after all placeholders have been replaced
 259  335
                 phs.setResolvedText(buffer.toString());
 260  335
         }
 261  
 
 262  
         /**
 263  
          * Update the buffer to replace the placeholder text with a value
 264  
          */
 265  
         protected int updateBuffer(StringBuilder buffer, Placeholder pholder, int offset) {
 266  34
                 int prefixLength = this.placeholderPrefix.length();
 267  34
                 int suffixLength = this.placeholderSuffix.length();
 268  34
                 int startIndex = pholder.getStartIndex() + offset;
 269  34
                 int endIndex = pholder.getEndIndex() + offset;
 270  34
                 buffer.replace(startIndex, endIndex, pholder.getValue());
 271  34
                 int textLength = pholder.getPlaceholderString().getText().length();
 272  34
                 int originalLength = prefixLength + textLength + suffixLength;
 273  34
                 int newLength = pholder.getValue().length();
 274  34
                 int diff = newLength - originalLength;
 275  34
                 return diff;
 276  
         }
 277  
 
 278  
         /**
 279  
          * Make sure we aren't in an infinite loop. If someone does something like this in a properties file:<br>
 280  
          * <code>
 281  
          *  a=${b} 
 282  
          *  b=${a}
 283  
          * </code>
 284  
          * 
 285  
          * We would end up with a stack overflow error without this check
 286  
          */
 287  
         protected void circularReferenceCheck(String placeholderText, Set<String> resolving) {
 288  38
                 logger.trace("Adding '{}' to the list of placeholders being resolved", placeholderText);
 289  38
                 boolean added = resolving.add(placeholderText);
 290  
 
 291  38
                 if (!added) {
 292  
                         // If we get here, one of the placeholders we are trying to resolve contains a reference to another
 293  
                         // placeholder that we are already trying to resolve.
 294  1
                         throw new IllegalArgumentException("Circular reference detected on " + placeholderText);
 295  
                 }
 296  37
         }
 297  
 
 298  
         /**
 299  
          * When this method completes, Placeholder.getValue() will return the value that should be used in place of the
 300  
          * Placeholder
 301  
          */
 302  
         protected void resolvePlaceholder(Placeholder placeholder, ValueRetriever retriever, Set<String> resolving) {
 303  
                 // Extract the PlaceholderString for this Placeholder
 304  38
                 PlaceholderString phs = placeholder.getPlaceholderString();
 305  
 
 306  
                 // Make sure we are not in an infinite loop
 307  38
                 circularReferenceCheck(phs.getText(), resolving);
 308  
 
 309  
                 // The base case is a placeholder that does not contain nested placeholder's
 310  37
                 if (isEmpty(phs.getPlaceholders())) {
 311  25
                         phs.setResolvedText(phs.getText());
 312  
                 } else {
 313  
                         // Recurse to handle nested placeholders
 314  12
                         resolvePlaceholderString(phs, retriever, resolving);
 315  
                 }
 316  
 
 317  
                 // The resolved text is our property key
 318  37
                 String key = phs.getResolvedText();
 319  
 
 320  
                 // Get a value for the property
 321  37
                 String value = getValue(key, placeholder, retriever, resolving);
 322  
 
 323  
                 // Store the value in the placeholder object
 324  34
                 placeholder.setValue(value);
 325  
 
 326  
                 // The Placeholder is now resolved, remove it from the set
 327  34
                 logger.trace("Removing '{}' from the list of placeholders being resolved", phs.getText());
 328  34
                 resolving.remove(phs.getText());
 329  34
         }
 330  
 
 331  
         /**
 332  
          * Parse the PlaceholderString to get the List of Placeholder objects (if any). If there are none, an empty list is
 333  
          * returned.
 334  
          */
 335  
         protected void parse(PlaceholderString phs) {
 336  
 
 337  
                 // Extract the text we will be parsing
 338  391
                 String text = phs.getText();
 339  
 
 340  
                 // Locate the first occurrence of our prefix
 341  391
                 int startIndex = text.indexOf(this.placeholderPrefix);
 342  391
                 if (startIndex == -1) {
 343  345
                         logger.trace("Skip parsing.  No placeholders found in [{}]", text);
 344  
                 }
 345  
 
 346  
                 // Storage for the placeholders we find
 347  391
                 List<Placeholder> placeholders = new ArrayList<Placeholder>();
 348  
 
 349  
                 // While there is at least one prefix remaining
 350  443
                 while (startIndex != -1) {
 351  
                         // Attempt to get a placeholder object
 352  53
                         Placeholder placeholder = getPlaceholder(text, startIndex);
 353  
 
 354  
                         // No placeholder could be found
 355  53
                         if (placeholder == null) {
 356  1
                                 break;
 357  
                         }
 358  
 
 359  
                         // Add our placeholder to the list
 360  52
                         placeholders.add(placeholder);
 361  
 
 362  
                         // Start looking again, skipping past the placeholder we just found
 363  52
                         int newFromIndex = placeholder.getEndIndex();
 364  
 
 365  
                         // Attempt to locate another prefix
 366  52
                         startIndex = text.indexOf(this.placeholderPrefix, newFromIndex);
 367  52
                 }
 368  
 
 369  
                 // Store the Placeholder's on the PlaceHolderString
 370  391
                 phs.setPlaceholders(placeholders);
 371  391
         }
 372  
 
 373  
         /**
 374  
          * Get a Placeholder object representing the first placeholder after startIndex
 375  
          */
 376  
         protected Placeholder getPlaceholder(String source, int startIndex) {
 377  53
                 int suffixLength = this.placeholderSuffix.length();
 378  
 
 379  
                 // Attempt to locate the end of the placeholder
 380  53
                 int endIndex = findPlaceholderEndIndex(source, startIndex);
 381  
 
 382  53
                 if (endIndex == -1) {
 383  
                         // Could not locate the end
 384  1
                         return null;
 385  
                 } else {
 386  
                         // Move the endIndex past the suffix
 387  52
                         endIndex = endIndex + suffixLength;
 388  
                 }
 389  
 
 390  
                 // The placeholder exactly as it appears in the source string ie ${foo}
 391  52
                 String text = source.substring(startIndex, endIndex);
 392  
 
 393  
                 // Trim off prefix and suffix
 394  52
                 String trimmedText = getTrimmedText(text);
 395  
 
 396  
                 // Create a new PlaceholderString
 397  52
                 PlaceholderString phs = new PlaceholderString(trimmedText);
 398  
 
 399  
                 // Recursive call to parse the new PlaceholderString (and any Placeholder's it may contain)
 400  52
                 parse(phs);
 401  
 
 402  
                 // Populate a placeholder object
 403  52
                 Placeholder placeholder = new Placeholder();
 404  52
                 placeholder.setStartIndex(startIndex);
 405  52
                 placeholder.setEndIndex(endIndex);
 406  52
                 placeholder.setPlaceholderString(phs);
 407  52
                 return placeholder;
 408  
         }
 409  
 
 410  
         /**
 411  
          * Return true if the index into the buffer is pointing at the suffix, false otherwise
 412  
          */
 413  
         protected boolean haveArrivedAtSuffix(CharSequence buf, int index) {
 414  284
                 return StringUtils.substringMatch(buf, index, this.placeholderSuffix);
 415  
         }
 416  
 
 417  
         /**
 418  
          * Return true if the index into the buffer is pointing at the simplePrefix, false otherwise
 419  
          */
 420  
         protected boolean haveArrivedAtPrefix(CharSequence buf, int index) {
 421  218
                 return StringUtils.substringMatch(buf, index, this.simplePrefix);
 422  
         }
 423  
 
 424  
         /**
 425  
          * Given a buffer and a startingIndex, find the suffix that matches our prefix. The default prefix is "${" and the
 426  
          * default suffix is "}"
 427  
          */
 428  
         protected int findPlaceholderEndIndex(CharSequence buf, int startIndex) {
 429  
                 // Skip past the prefix
 430  53
                 int index = startIndex + this.placeholderPrefix.length();
 431  
 
 432  
                 // Track nested place holders as we encounter them
 433  53
                 int nestedPlaceHolderCount = 0;
 434  
 
 435  
                 // Iterate through the buffer looking for the closing bracket that matches our opening bracket
 436  285
                 while (index < buf.length()) {
 437  
                         // Check to see if we've arrived at a closing bracket
 438  284
                         if (haveArrivedAtSuffix(buf, index)) {
 439  
 
 440  
                                 // If we aren't nested, return the index, nothing more to do
 441  66
                                 if (nestedPlaceHolderCount == 0) {
 442  52
                                         return index;
 443  
                                 }
 444  
 
 445  
                                 // Otherwise, decrement the nested count
 446  14
                                 nestedPlaceHolderCount--;
 447  
 
 448  
                                 // and skip past the nested suffix
 449  14
                                 index = index + this.placeholderSuffix.length();
 450  
 
 451  218
                         } else if (haveArrivedAtPrefix(buf, index)) {
 452  
                                 // If we get here, we've encountered a nested placeholder
 453  
 
 454  
                                 // Increment the counter
 455  14
                                 nestedPlaceHolderCount++;
 456  
 
 457  
                                 // Skip past the nested prefix
 458  14
                                 index = index + this.simplePrefix.length();
 459  
                         } else {
 460  
                                 // We are not on a prefix or a suffix, keep searching
 461  204
                                 index++;
 462  
                         }
 463  
                 }
 464  
 
 465  
                 // We never found a suffix to match our prefix
 466  1
                 return -1;
 467  
         }
 468  
 
 469  
         public String getPlaceholderPrefix() {
 470  9
                 return placeholderPrefix;
 471  
         }
 472  
 
 473  
         public void setPlaceholderPrefix(String placeholderPrefix) {
 474  2
                 this.placeholderPrefix = placeholderPrefix;
 475  2
         }
 476  
 
 477  
         public String getPlaceholderSuffix() {
 478  8
                 return placeholderSuffix;
 479  
         }
 480  
 
 481  
         public void setPlaceholderSuffix(String placeholderSuffix) {
 482  2
                 this.placeholderSuffix = placeholderSuffix;
 483  2
         }
 484  
 
 485  
         public String getValueSeparator() {
 486  3
                 return valueSeparator;
 487  
         }
 488  
 
 489  
         public void setValueSeparator(String valueSeparator) {
 490  2
                 this.valueSeparator = valueSeparator;
 491  2
         }
 492  
 
 493  
         public boolean isIgnoreUnresolvablePlaceholders() {
 494  2
                 return ignoreUnresolvablePlaceholders;
 495  
         }
 496  
 
 497  
         public void setIgnoreUnresolvablePlaceholders(boolean ignoreUnresolvablePlaceholders) {
 498  3
                 this.ignoreUnresolvablePlaceholders = ignoreUnresolvablePlaceholders;
 499  3
         }
 500  
 
 501  
         public PropertyLogger getPlogger() {
 502  2
                 return plogger;
 503  
         }
 504  
 
 505  
         public void setPlogger(PropertyLogger plogger) {
 506  3
                 this.plogger = plogger;
 507  3
         }
 508  
 
 509  
         public String getSimplePrefix() {
 510  3
                 return simplePrefix;
 511  
         }
 512  
 
 513  
         public void setSimplePrefix(String simplePrefix) {
 514  2
                 this.simplePrefix = simplePrefix;
 515  2
         }
 516  
 
 517  
 }