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}