001/* 002 * Copyright 2002-2019 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.util; 018 019import java.util.HashMap; 020import java.util.HashSet; 021import java.util.Map; 022import java.util.Properties; 023import java.util.Set; 024 025import org.apache.commons.logging.Log; 026import org.apache.commons.logging.LogFactory; 027 028import org.springframework.lang.Nullable; 029 030/** 031 * Utility class for working with Strings that have placeholder values in them. A placeholder takes the form 032 * {@code ${name}}. Using {@code PropertyPlaceholderHelper} these placeholders can be substituted for 033 * user-supplied values. <p> Values for substitution can be supplied using a {@link Properties} instance or 034 * using a {@link PlaceholderResolver}. 035 * 036 * @author Juergen Hoeller 037 * @author Rob Harrop 038 * @since 3.0 039 */ 040public class PropertyPlaceholderHelper { 041 042 private static final Log logger = LogFactory.getLog(PropertyPlaceholderHelper.class); 043 044 private static final Map<String, String> wellKnownSimplePrefixes = new HashMap<>(4); 045 046 static { 047 wellKnownSimplePrefixes.put("}", "{"); 048 wellKnownSimplePrefixes.put("]", "["); 049 wellKnownSimplePrefixes.put(")", "("); 050 } 051 052 053 private final String placeholderPrefix; 054 055 private final String placeholderSuffix; 056 057 private final String simplePrefix; 058 059 @Nullable 060 private final String valueSeparator; 061 062 private final boolean ignoreUnresolvablePlaceholders; 063 064 065 /** 066 * Creates a new {@code PropertyPlaceholderHelper} that uses the supplied prefix and suffix. 067 * Unresolvable placeholders are ignored. 068 * @param placeholderPrefix the prefix that denotes the start of a placeholder 069 * @param placeholderSuffix the suffix that denotes the end of a placeholder 070 */ 071 public PropertyPlaceholderHelper(String placeholderPrefix, String placeholderSuffix) { 072 this(placeholderPrefix, placeholderSuffix, null, true); 073 } 074 075 /** 076 * Creates a new {@code PropertyPlaceholderHelper} that uses the supplied prefix and suffix. 077 * @param placeholderPrefix the prefix that denotes the start of a placeholder 078 * @param placeholderSuffix the suffix that denotes the end of a placeholder 079 * @param valueSeparator the separating character between the placeholder variable 080 * and the associated default value, if any 081 * @param ignoreUnresolvablePlaceholders indicates whether unresolvable placeholders should 082 * be ignored ({@code true}) or cause an exception ({@code false}) 083 */ 084 public PropertyPlaceholderHelper(String placeholderPrefix, String placeholderSuffix, 085 @Nullable String valueSeparator, boolean ignoreUnresolvablePlaceholders) { 086 087 Assert.notNull(placeholderPrefix, "'placeholderPrefix' must not be null"); 088 Assert.notNull(placeholderSuffix, "'placeholderSuffix' must not be null"); 089 this.placeholderPrefix = placeholderPrefix; 090 this.placeholderSuffix = placeholderSuffix; 091 String simplePrefixForSuffix = wellKnownSimplePrefixes.get(this.placeholderSuffix); 092 if (simplePrefixForSuffix != null && this.placeholderPrefix.endsWith(simplePrefixForSuffix)) { 093 this.simplePrefix = simplePrefixForSuffix; 094 } 095 else { 096 this.simplePrefix = this.placeholderPrefix; 097 } 098 this.valueSeparator = valueSeparator; 099 this.ignoreUnresolvablePlaceholders = ignoreUnresolvablePlaceholders; 100 } 101 102 103 /** 104 * Replaces all placeholders of format {@code ${name}} with the corresponding 105 * property from the supplied {@link Properties}. 106 * @param value the value containing the placeholders to be replaced 107 * @param properties the {@code Properties} to use for replacement 108 * @return the supplied value with placeholders replaced inline 109 */ 110 public String replacePlaceholders(String value, final Properties properties) { 111 Assert.notNull(properties, "'properties' must not be null"); 112 return replacePlaceholders(value, properties::getProperty); 113 } 114 115 /** 116 * Replaces all placeholders of format {@code ${name}} with the value returned 117 * from the supplied {@link PlaceholderResolver}. 118 * @param value the value containing the placeholders to be replaced 119 * @param placeholderResolver the {@code PlaceholderResolver} to use for replacement 120 * @return the supplied value with placeholders replaced inline 121 */ 122 public String replacePlaceholders(String value, PlaceholderResolver placeholderResolver) { 123 Assert.notNull(value, "'value' must not be null"); 124 return parseStringValue(value, placeholderResolver, null); 125 } 126 127 protected String parseStringValue( 128 String value, PlaceholderResolver placeholderResolver, @Nullable Set<String> visitedPlaceholders) { 129 130 int startIndex = value.indexOf(this.placeholderPrefix); 131 if (startIndex == -1) { 132 return value; 133 } 134 135 StringBuilder result = new StringBuilder(value); 136 while (startIndex != -1) { 137 int endIndex = findPlaceholderEndIndex(result, startIndex); 138 if (endIndex != -1) { 139 String placeholder = result.substring(startIndex + this.placeholderPrefix.length(), endIndex); 140 String originalPlaceholder = placeholder; 141 if (visitedPlaceholders == null) { 142 visitedPlaceholders = new HashSet<>(4); 143 } 144 if (!visitedPlaceholders.add(originalPlaceholder)) { 145 throw new IllegalArgumentException( 146 "Circular placeholder reference '" + originalPlaceholder + "' in property definitions"); 147 } 148 // Recursive invocation, parsing placeholders contained in the placeholder key. 149 placeholder = parseStringValue(placeholder, placeholderResolver, visitedPlaceholders); 150 // Now obtain the value for the fully resolved key... 151 String propVal = placeholderResolver.resolvePlaceholder(placeholder); 152 if (propVal == null && this.valueSeparator != null) { 153 int separatorIndex = placeholder.indexOf(this.valueSeparator); 154 if (separatorIndex != -1) { 155 String actualPlaceholder = placeholder.substring(0, separatorIndex); 156 String defaultValue = placeholder.substring(separatorIndex + this.valueSeparator.length()); 157 propVal = placeholderResolver.resolvePlaceholder(actualPlaceholder); 158 if (propVal == null) { 159 propVal = defaultValue; 160 } 161 } 162 } 163 if (propVal != null) { 164 // Recursive invocation, parsing placeholders contained in the 165 // previously resolved placeholder value. 166 propVal = parseStringValue(propVal, placeholderResolver, visitedPlaceholders); 167 result.replace(startIndex, endIndex + this.placeholderSuffix.length(), propVal); 168 if (logger.isTraceEnabled()) { 169 logger.trace("Resolved placeholder '" + placeholder + "'"); 170 } 171 startIndex = result.indexOf(this.placeholderPrefix, startIndex + propVal.length()); 172 } 173 else if (this.ignoreUnresolvablePlaceholders) { 174 // Proceed with unprocessed value. 175 startIndex = result.indexOf(this.placeholderPrefix, endIndex + this.placeholderSuffix.length()); 176 } 177 else { 178 throw new IllegalArgumentException("Could not resolve placeholder '" + 179 placeholder + "'" + " in value \"" + value + "\""); 180 } 181 visitedPlaceholders.remove(originalPlaceholder); 182 } 183 else { 184 startIndex = -1; 185 } 186 } 187 return result.toString(); 188 } 189 190 private int findPlaceholderEndIndex(CharSequence buf, int startIndex) { 191 int index = startIndex + this.placeholderPrefix.length(); 192 int withinNestedPlaceholder = 0; 193 while (index < buf.length()) { 194 if (StringUtils.substringMatch(buf, index, this.placeholderSuffix)) { 195 if (withinNestedPlaceholder > 0) { 196 withinNestedPlaceholder--; 197 index = index + this.placeholderSuffix.length(); 198 } 199 else { 200 return index; 201 } 202 } 203 else if (StringUtils.substringMatch(buf, index, this.simplePrefix)) { 204 withinNestedPlaceholder++; 205 index = index + this.simplePrefix.length(); 206 } 207 else { 208 index++; 209 } 210 } 211 return -1; 212 } 213 214 215 /** 216 * Strategy interface used to resolve replacement values for placeholders contained in Strings. 217 */ 218 @FunctionalInterface 219 public interface PlaceholderResolver { 220 221 /** 222 * Resolve the supplied placeholder name to the replacement value. 223 * @param placeholderName the name of the placeholder to resolve 224 * @return the replacement value, or {@code null} if no replacement is to be made 225 */ 226 @Nullable 227 String resolvePlaceholder(String placeholderName); 228 } 229 230}