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.camel.component.properties;
018
019import java.util.HashSet;
020import java.util.Properties;
021import java.util.Set;
022
023import org.apache.camel.util.ObjectHelper;
024import org.apache.camel.util.StringHelper;
025import org.slf4j.Logger;
026import org.slf4j.LoggerFactory;
027
028/**
029 * A parser to parse a string which contains property placeholders.
030 */
031public class DefaultPropertiesParser implements AugmentedPropertyNameAwarePropertiesParser {
032    private static final String GET_OR_ELSE_TOKEN = ":";
033
034    protected final Logger log = LoggerFactory.getLogger(getClass());
035
036    private PropertiesComponent propertiesComponent;
037
038    public DefaultPropertiesParser() {
039    }
040
041    public DefaultPropertiesParser(PropertiesComponent propertiesComponent) {
042        this.propertiesComponent = propertiesComponent;
043    }
044
045    public PropertiesComponent getPropertiesComponent() {
046        return propertiesComponent;
047    }
048
049    public void setPropertiesComponent(PropertiesComponent propertiesComponent) {
050        this.propertiesComponent = propertiesComponent;
051    }
052
053    @Override
054    public String parseUri(String text, Properties properties, String prefixToken, String suffixToken) throws IllegalArgumentException {
055        return parseUri(text, properties, prefixToken, suffixToken, null, null, false, false);
056    }
057
058    @Override
059    public String parseUri(String text, Properties properties,
060                           String prefixToken, String suffixToken, String propertyPrefix, String propertySuffix,
061            boolean fallbackToUnaugmentedProperty, boolean defaultFallbackEnabled) throws IllegalArgumentException {
062        ParsingContext context = new ParsingContext(properties, prefixToken, suffixToken, propertyPrefix, propertySuffix, fallbackToUnaugmentedProperty, defaultFallbackEnabled);
063        return context.parse(text);
064    }
065
066    public String parseProperty(String key, String value, Properties properties) {
067        return value;
068    }
069
070    /**
071     * This inner class helps replacing properties.
072     */
073    private final class ParsingContext {
074        private final Properties properties;
075        private final String prefixToken;
076        private final String suffixToken;
077        private final String propertyPrefix;
078        private final String propertySuffix;
079        private final boolean fallbackToUnaugmentedProperty;
080        private final boolean defaultFallbackEnabled;
081
082        ParsingContext(Properties properties, String prefixToken, String suffixToken, String propertyPrefix, String propertySuffix,
083                              boolean fallbackToUnaugmentedProperty, boolean defaultFallbackEnabled) {
084            this.properties = properties;
085            this.prefixToken = prefixToken;
086            this.suffixToken = suffixToken;
087            this.propertyPrefix = propertyPrefix;
088            this.propertySuffix = propertySuffix;
089            this.fallbackToUnaugmentedProperty = fallbackToUnaugmentedProperty;
090            this.defaultFallbackEnabled = defaultFallbackEnabled;
091        }
092
093        /**
094         * Parses the given input string and replaces all properties
095         *
096         * @param input Input string
097         * @return Evaluated string
098         */
099        public String parse(String input) {
100            return doParse(input, new HashSet<String>());
101        }
102
103        /**
104         * Recursively parses the given input string and replaces all properties
105         *
106         * @param input                Input string
107         * @param replacedPropertyKeys Already replaced property keys used for tracking circular references
108         * @return Evaluated string
109         */
110        private String doParse(String input, Set<String> replacedPropertyKeys) {
111            if (input == null) {
112                return null;
113            }
114            String answer = input;
115            Property property;
116            while ((property = readProperty(answer)) != null) {
117                // Check for circular references
118                if (replacedPropertyKeys.contains(property.getKey())) {
119                    throw new IllegalArgumentException("Circular reference detected with key [" + property.getKey() + "] from text: " + input);
120                }
121
122                Set<String> newReplaced = new HashSet<>(replacedPropertyKeys);
123                newReplaced.add(property.getKey());
124
125                String before = answer.substring(0, property.getBeginIndex());
126                String after = answer.substring(property.getEndIndex());
127                answer = before + doParse(property.getValue(), newReplaced) + after;
128            }
129            return answer;
130        }
131
132        /**
133         * Finds a property in the given string. It returns {@code null} if there's no property defined.
134         *
135         * @param input Input string
136         * @return A property in the given string or {@code null} if not found
137         */
138        private Property readProperty(String input) {
139            // Find the index of the first valid suffix token
140            int suffix = getSuffixIndex(input);
141
142            // If not found, ensure that there is no valid prefix token in the string
143            if (suffix == -1) {
144                if (getMatchingPrefixIndex(input, input.length()) != -1) {
145                    throw new IllegalArgumentException(String.format("Missing %s from the text: %s", suffixToken, input));
146                }
147                return null;
148            }
149
150            // Find the index of the prefix token that matches the suffix token
151            int prefix = getMatchingPrefixIndex(input, suffix);
152            if (prefix == -1) {
153                throw new IllegalArgumentException(String.format("Missing %s from the text: %s", prefixToken, input));
154            }
155
156            String key = input.substring(prefix + prefixToken.length(), suffix);
157            String value = getPropertyValue(key, input);
158            return new Property(prefix, suffix + suffixToken.length(), key, value);
159        }
160
161        /**
162         * Gets the first index of the suffix token that is not surrounded by quotes
163         *
164         * @param input Input string
165         * @return First index of the suffix token that is not surrounded by quotes
166         */
167        private int getSuffixIndex(String input) {
168            int index = -1;
169            do {
170                index = input.indexOf(suffixToken, index + 1);
171            } while (index != -1 && isQuoted(input, index, suffixToken));
172            return index;
173        }
174
175        /**
176         * Gets the index of the prefix token that matches the suffix at the given index and that is not surrounded by quotes
177         *
178         * @param input       Input string
179         * @param suffixIndex Index of the suffix token
180         * @return Index of the prefix token that matches the suffix at the given index and that is not surrounded by quotes
181         */
182        private int getMatchingPrefixIndex(String input, int suffixIndex) {
183            int index = suffixIndex;
184            do {
185                index = input.lastIndexOf(prefixToken, index - 1);
186            } while (index != -1 && isQuoted(input, index, prefixToken));
187            return index;
188        }
189
190        /**
191         * Indicates whether or not the token at the given index is surrounded by single or double quotes
192         *
193         * @param input Input string
194         * @param index Index of the token
195         * @param token Token
196         * @return {@code true}
197         */
198        private boolean isQuoted(String input, int index, String token) {
199            int beforeIndex = index - 1;
200            int afterIndex = index + token.length();
201            if (beforeIndex >= 0 && afterIndex < input.length()) {
202                char before = input.charAt(beforeIndex);
203                char after = input.charAt(afterIndex);
204                return (before == after) && (before == '\'' || before == '"');
205            }
206            return false;
207        }
208
209        /**
210         * Gets the value of the property with given key
211         *
212         * @param key   Key of the property
213         * @param input Input string (used for exception message if value not found)
214         * @return Value of the property with the given key
215         */
216        private String getPropertyValue(String key, String input) {
217
218            // the key may be a function, so lets check this first
219            if (propertiesComponent != null) {
220                for (PropertiesFunction function : propertiesComponent.getFunctions().values()) {
221                    String token = function.getName() + ":";
222                    if (key.startsWith(token)) {
223                        String remainder = key.substring(token.length());
224                        log.debug("Property with key [{}] is applied by function [{}]", key, function.getName());
225                        String value = function.apply(remainder);
226                        if (value == null) {
227                            throw new IllegalArgumentException("Property with key [" + key + "] using function [" + function.getName() + "]"
228                                    + " returned null value which is not allowed, from input: " + input);
229                        } else {
230                            if (log.isDebugEnabled()) {
231                                log.debug("Property with key [{}] applied by function [{}] -> {}", key, function.getName(), value);
232                            }
233                            return value;
234                        }
235                    }
236                }
237            }
238
239            // they key may have a get or else expression
240            String defaultValue = null;
241            if (defaultFallbackEnabled && key.contains(GET_OR_ELSE_TOKEN)) {
242                defaultValue = StringHelper.after(key, GET_OR_ELSE_TOKEN);
243                key = StringHelper.before(key, GET_OR_ELSE_TOKEN);
244            }
245
246            String augmentedKey = getAugmentedKey(key);
247            boolean shouldFallback = fallbackToUnaugmentedProperty && !key.equals(augmentedKey);
248
249            String value = doGetPropertyValue(augmentedKey);
250            if (value == null && shouldFallback) {
251                log.debug("Property with key [{}] not found, attempting with unaugmented key: {}", augmentedKey, key);
252                value = doGetPropertyValue(key);
253            }
254
255            if (value == null && defaultValue != null) {
256                log.debug("Property with key [{}] not found, using default value: {}", augmentedKey, defaultValue);
257                value = defaultValue;
258            }
259
260            if (value == null) {
261                StringBuilder esb = new StringBuilder();
262                if (propertiesComponent == null || propertiesComponent.isDefaultCreated()) {
263                    // if the component was auto created then include more information that the end user should define it
264                    esb.append("PropertiesComponent with name properties must be defined in CamelContext to support property placeholders. ");
265                }
266                esb.append("Property with key [").append(augmentedKey).append("] ");
267                if (shouldFallback) {
268                    esb.append("(and original key [").append(key).append("]) ");
269                }
270                esb.append("not found in properties from text: ").append(input);
271                throw new IllegalArgumentException(esb.toString());
272            }
273
274            return value;
275        }
276
277        /**
278         * Gets the augmented key of the given base key
279         *
280         * @param key Base key
281         * @return Augmented key
282         */
283        private String getAugmentedKey(String key) {
284            String augmentedKey = key;
285            if (propertyPrefix != null) {
286                log.debug("Augmenting property key [{}] with prefix: {}", key, propertyPrefix);
287                augmentedKey = propertyPrefix + augmentedKey;
288            }
289            if (propertySuffix != null) {
290                log.debug("Augmenting property key [{}] with suffix: {}", key, propertySuffix);
291                augmentedKey = augmentedKey + propertySuffix;
292            }
293            return augmentedKey;
294        }
295
296        /**
297         * Gets the property with the given key, it returns {@code null} if the property is not found
298         *
299         * @param key Key of the property
300         * @return Value of the property or {@code null} if not found
301         */
302        private String doGetPropertyValue(String key) {
303            if (ObjectHelper.isEmpty(key)) {
304                return parseProperty(key, null, properties);
305            }
306
307            String value = null;
308
309            // override is the default mode
310            int mode = propertiesComponent != null ? propertiesComponent.getSystemPropertiesMode() : PropertiesComponent.SYSTEM_PROPERTIES_MODE_OVERRIDE;
311
312            if (mode == PropertiesComponent.SYSTEM_PROPERTIES_MODE_OVERRIDE) {
313                value = System.getProperty(key);
314                if (value != null) {
315                    log.debug("Found a JVM system property: {} with value: {} to be used.", key, value);
316                }
317            }
318
319            if (value == null && properties != null) {
320                value = properties.getProperty(key);
321                if (value != null) {
322                    log.debug("Found property: {} with value: {} to be used.", key, value);
323                }
324            }
325
326            if (value == null && mode == PropertiesComponent.SYSTEM_PROPERTIES_MODE_FALLBACK) {
327                value = System.getProperty(key);
328                if (value != null) {
329                    log.debug("Found a JVM system property: {} with value: {} to be used.", key, value);
330                }
331            }
332
333            return parseProperty(key, value, properties);
334        }
335    }
336
337    /**
338     * This inner class is the definition of a property used in a string
339     */
340    private static final class Property {
341        private final int beginIndex;
342        private final int endIndex;
343        private final String key;
344        private final String value;
345
346        private Property(int beginIndex, int endIndex, String key, String value) {
347            this.beginIndex = beginIndex;
348            this.endIndex = endIndex;
349            this.key = key;
350            this.value = value;
351        }
352
353        /**
354         * Gets the begin index of the property (including the prefix token).
355         */
356        public int getBeginIndex() {
357            return beginIndex;
358        }
359
360        /**
361         * Gets the end index of the property (including the suffix token).
362         */
363        public int getEndIndex() {
364            return endIndex;
365        }
366
367        /**
368         * Gets the key of the property.
369         */
370        public String getKey() {
371            return key;
372        }
373
374        /**
375         * Gets the value of the property.
376         */
377        public String getValue() {
378            return value;
379        }
380    }
381}