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}