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.io.Serializable;
020import java.util.ArrayList;
021import java.util.Arrays;
022import java.util.HashMap;
023import java.util.List;
024import java.util.Map;
025import java.util.Properties;
026
027import org.apache.camel.Endpoint;
028import org.apache.camel.impl.UriEndpointComponent;
029import org.apache.camel.util.FilePathResolver;
030import org.apache.camel.util.LRUSoftCache;
031import org.apache.camel.util.ObjectHelper;
032import org.slf4j.Logger;
033import org.slf4j.LoggerFactory;
034
035/**
036 * The <a href="http://camel.apache.org/properties">Properties Component</a> allows you to use property placeholders when defining Endpoint URIs
037 */
038public class PropertiesComponent extends UriEndpointComponent {
039
040    /**
041     * The default prefix token.
042     */
043    public static final String DEFAULT_PREFIX_TOKEN = "{{";
044    
045    /**
046     * The default suffix token.
047     */
048    public static final String DEFAULT_SUFFIX_TOKEN = "}}";
049    
050    /**
051     * The default prefix token.
052     * @deprecated Use {@link #DEFAULT_PREFIX_TOKEN} instead.
053     */
054    @Deprecated
055    public static final String PREFIX_TOKEN = DEFAULT_PREFIX_TOKEN;
056    
057    /**
058     * The default suffix token.
059     * @deprecated Use {@link #DEFAULT_SUFFIX_TOKEN} instead.
060     */
061    @Deprecated
062    public static final String SUFFIX_TOKEN = DEFAULT_SUFFIX_TOKEN;
063
064    /**
065     *  Never check system properties.
066     */
067    public static final int SYSTEM_PROPERTIES_MODE_NEVER = 0;
068
069    /**
070     * Check system properties if not resolvable in the specified properties.
071     */
072    public static final int SYSTEM_PROPERTIES_MODE_FALLBACK = 1;
073
074    /**
075     * Check system properties first, before trying the specified properties.
076     * This allows system properties to override any other property source.
077     * <p/>
078     * This is the default.
079     */
080    public static final int SYSTEM_PROPERTIES_MODE_OVERRIDE = 2;
081
082    /**
083     * Key for stores special override properties that containers such as OSGi can store
084     * in the OSGi service registry
085     */
086    public static final String OVERRIDE_PROPERTIES = PropertiesComponent.class.getName() + ".OverrideProperties";
087
088    private static final Logger LOG = LoggerFactory.getLogger(PropertiesComponent.class);
089    private final Map<CacheKey, Properties> cacheMap = new LRUSoftCache<CacheKey, Properties>(1000);
090    private final Map<String, PropertiesFunction> functions = new HashMap<String, PropertiesFunction>();
091    private PropertiesResolver propertiesResolver = new DefaultPropertiesResolver(this);
092    private PropertiesParser propertiesParser = new DefaultPropertiesParser(this);
093    private boolean isDefaultCreated;
094    private String[] locations;
095    private boolean ignoreMissingLocation;
096    private String encoding;
097    private boolean cache = true;
098    private String propertyPrefix;
099    private String propertyPrefixResolved;
100    private String propertySuffix;
101    private String propertySuffixResolved;
102    private boolean fallbackToUnaugmentedProperty = true;
103    private String prefixToken = DEFAULT_PREFIX_TOKEN;
104    private String suffixToken = DEFAULT_SUFFIX_TOKEN;
105    private Properties initialProperties;
106    private Properties overrideProperties;
107    private int systemPropertiesMode = SYSTEM_PROPERTIES_MODE_OVERRIDE;
108
109    public PropertiesComponent() {
110        super(PropertiesEndpoint.class);
111        // include out of the box functions
112        addFunction(new EnvPropertiesFunction());
113        addFunction(new SysPropertiesFunction());
114        addFunction(new ServicePropertiesFunction());
115        addFunction(new ServiceHostPropertiesFunction());
116        addFunction(new ServicePortPropertiesFunction());
117    }
118
119    public PropertiesComponent(boolean isDefaultCreated) {
120        this();
121        this.isDefaultCreated = isDefaultCreated;
122    }
123
124    public PropertiesComponent(String location) {
125        this();
126        setLocation(location);
127    }
128
129    public PropertiesComponent(String... locations) {
130        this();
131        setLocations(locations);
132    }
133
134    @Override
135    protected Endpoint createEndpoint(String uri, String remaining, Map<String, Object> parameters) throws Exception {
136        String[] paths = locations;
137
138        // override default locations
139        String locations = getAndRemoveParameter(parameters, "locations", String.class);
140        Boolean ignoreMissingLocationLoc = getAndRemoveParameter(parameters, "ignoreMissingLocation", Boolean.class);
141        if (locations != null) {
142            LOG.trace("Overriding default locations with location: {}", locations);
143            paths = locations.split(",");
144        }
145        if (ignoreMissingLocationLoc != null) {
146            ignoreMissingLocation = ignoreMissingLocationLoc;
147        }
148
149        String endpointUri = parseUri(remaining, paths);
150        LOG.debug("Endpoint uri parsed as: {}", endpointUri);
151
152        Endpoint delegate = getCamelContext().getEndpoint(endpointUri);
153        PropertiesEndpoint answer = new PropertiesEndpoint(uri, delegate, this);
154
155        setProperties(answer, parameters);
156        return answer;
157    }
158
159    public String parseUri(String uri) throws Exception {
160        return parseUri(uri, locations);
161    }
162
163    public String parseUri(String uri, String... paths) throws Exception {
164        Properties prop = new Properties();
165
166        // use initial properties
167        if (null != initialProperties) {
168            prop.putAll(initialProperties);
169        }
170
171        // use locations
172        if (paths != null) {
173            // location may contain JVM system property or OS environment variables
174            // so we need to parse those
175            String[] locations = parseLocations(paths);
176
177            // check cache first
178            CacheKey key = new CacheKey(locations);
179            Properties locationsProp = cache ? cacheMap.get(key) : null;
180            if (locationsProp == null) {
181                locationsProp = propertiesResolver.resolveProperties(getCamelContext(), ignoreMissingLocation, locations);
182                if (cache) {
183                    cacheMap.put(key, locationsProp);
184                }
185            }
186            prop.putAll(locationsProp);
187        }
188
189        // use override properties
190        if (overrideProperties != null) {
191            // make a copy to avoid affecting the original properties
192            Properties override = new Properties();
193            override.putAll(prop);
194            override.putAll(overrideProperties);
195            prop = override;
196        }
197
198        // enclose tokens if missing
199        if (!uri.contains(prefixToken) && !uri.startsWith(prefixToken)) {
200            uri = prefixToken + uri;
201        }
202        if (!uri.contains(suffixToken) && !uri.endsWith(suffixToken)) {
203            uri = uri + suffixToken;
204        }
205
206        LOG.trace("Parsing uri {} with properties: {}", uri, prop);
207        
208        if (propertiesParser instanceof AugmentedPropertyNameAwarePropertiesParser) {
209            return ((AugmentedPropertyNameAwarePropertiesParser) propertiesParser).parseUri(uri, prop, prefixToken, suffixToken,
210                                                                                            propertyPrefixResolved, propertySuffixResolved, fallbackToUnaugmentedProperty);
211        } else {
212            return propertiesParser.parseUri(uri, prop, prefixToken, suffixToken);
213        }
214    }
215
216    /**
217     * Is this component created as a default by {@link org.apache.camel.CamelContext} during starting up Camel.
218     */
219    public boolean isDefaultCreated() {
220        return isDefaultCreated;
221    }
222
223    public String[] getLocations() {
224        return locations;
225    }
226
227    /**
228     * A list of locations to load properties. You can use comma to separate multiple locations.
229     * This option will override any default locations and only use the locations from this option.
230     */
231    public void setLocations(String[] locations) {
232        // make sure to trim as people may use new lines when configuring using XML
233        // and do this in the setter as Spring/Blueprint resolves placeholders before Camel is being started
234        if (locations != null && locations.length > 0) {
235            for (int i = 0; i < locations.length; i++) {
236                String loc = locations[i];
237                locations[i] = loc.trim();
238            }
239        }
240
241        this.locations = locations;
242    }
243
244    /**
245     * A list of locations to load properties. You can use comma to separate multiple locations.
246     * This option will override any default locations and only use the locations from this option.
247     */
248    public void setLocation(String location) {
249        setLocations(location.split(","));
250    }
251
252    public String getEncoding() {
253        return encoding;
254    }
255
256    /**
257     * Encoding to use when loading properties file from the file system or classpath.
258     * <p/>
259     * If no encoding has been set, then the properties files is loaded using ISO-8859-1 encoding (latin-1)
260     * as documented by {@link java.util.Properties#load(java.io.InputStream)}
261     */
262    public void setEncoding(String encoding) {
263        this.encoding = encoding;
264    }
265
266    public PropertiesResolver getPropertiesResolver() {
267        return propertiesResolver;
268    }
269
270    /**
271     * To use a custom PropertiesResolver
272     */
273    public void setPropertiesResolver(PropertiesResolver propertiesResolver) {
274        this.propertiesResolver = propertiesResolver;
275    }
276
277    public PropertiesParser getPropertiesParser() {
278        return propertiesParser;
279    }
280
281    /**
282     * To use a custom PropertiesParser
283     */
284    public void setPropertiesParser(PropertiesParser propertiesParser) {
285        this.propertiesParser = propertiesParser;
286    }
287
288    public boolean isCache() {
289        return cache;
290    }
291
292    /**
293     * Whether or not to cache loaded properties. The default value is true.
294     */
295    public void setCache(boolean cache) {
296        this.cache = cache;
297    }
298    
299    public String getPropertyPrefix() {
300        return propertyPrefix;
301    }
302
303    /**
304     * Optional prefix prepended to property names before resolution.
305     */
306    public void setPropertyPrefix(String propertyPrefix) {
307        this.propertyPrefix = propertyPrefix;
308        this.propertyPrefixResolved = propertyPrefix;
309        if (ObjectHelper.isNotEmpty(this.propertyPrefix)) {
310            this.propertyPrefixResolved = FilePathResolver.resolvePath(this.propertyPrefix);
311        }
312    }
313
314    public String getPropertySuffix() {
315        return propertySuffix;
316    }
317
318    /**
319     * Optional suffix appended to property names before resolution.
320     */
321    public void setPropertySuffix(String propertySuffix) {
322        this.propertySuffix = propertySuffix;
323        this.propertySuffixResolved = propertySuffix;
324        if (ObjectHelper.isNotEmpty(this.propertySuffix)) {
325            this.propertySuffixResolved = FilePathResolver.resolvePath(this.propertySuffix);
326        }
327    }
328
329    public boolean isFallbackToUnaugmentedProperty() {
330        return fallbackToUnaugmentedProperty;
331    }
332
333    /**
334     * If true, first attempt resolution of property name augmented with propertyPrefix and propertySuffix
335     * before falling back the plain property name specified. If false, only the augmented property name is searched.
336     */
337    public void setFallbackToUnaugmentedProperty(boolean fallbackToUnaugmentedProperty) {
338        this.fallbackToUnaugmentedProperty = fallbackToUnaugmentedProperty;
339    }
340
341    public boolean isIgnoreMissingLocation() {
342        return ignoreMissingLocation;
343    }
344
345    /**
346     * Whether to silently ignore if a location cannot be located, such as a properties file not found.
347     */
348    public void setIgnoreMissingLocation(boolean ignoreMissingLocation) {
349        this.ignoreMissingLocation = ignoreMissingLocation;
350    }
351
352    public String getPrefixToken() {
353        return prefixToken;
354    }
355
356    /**
357     * Sets the value of the prefix token used to identify properties to replace.  Setting a value of
358     * {@code null} restores the default token (@link {@link #DEFAULT_PREFIX_TOKEN}).
359     */
360    public void setPrefixToken(String prefixToken) {
361        if (prefixToken == null) {
362            this.prefixToken = DEFAULT_PREFIX_TOKEN;
363        } else {
364            this.prefixToken = prefixToken;
365        }
366    }
367
368    public String getSuffixToken() {
369        return suffixToken;
370    }
371
372    /**
373     * Sets the value of the suffix token used to identify properties to replace.  Setting a value of
374     * {@code null} restores the default token (@link {@link #DEFAULT_SUFFIX_TOKEN}).
375     */
376    public void setSuffixToken(String suffixToken) {
377        if (suffixToken == null) {
378            this.suffixToken = DEFAULT_SUFFIX_TOKEN;
379        } else {
380            this.suffixToken = suffixToken;
381        }
382    }
383
384    public Properties getInitialProperties() {
385        return initialProperties;
386    }
387
388    /**
389     * Sets initial properties which will be used before any locations are resolved.
390     *
391     * @param initialProperties properties that are added first
392     */
393    public void setInitialProperties(Properties initialProperties) {
394        this.initialProperties = initialProperties;
395    }
396
397    public Properties getOverrideProperties() {
398        return overrideProperties;
399    }
400
401    /**
402     * Sets a special list of override properties that take precedence
403     * and will use first, if a property exist.
404     *
405     * @param overrideProperties properties that is used first
406     */
407    public void setOverrideProperties(Properties overrideProperties) {
408        this.overrideProperties = overrideProperties;
409    }
410
411    /**
412     * Gets the functions registered in this properties component.
413     */
414    public Map<String, PropertiesFunction> getFunctions() {
415        return functions;
416    }
417
418    /**
419     * Registers the {@link org.apache.camel.component.properties.PropertiesFunction} as a function to this component.
420     */
421    public void addFunction(PropertiesFunction function) {
422        this.functions.put(function.getName(), function);
423    }
424
425    /**
426     * Is there a {@link org.apache.camel.component.properties.PropertiesFunction} with the given name?
427     */
428    public boolean hasFunction(String name) {
429        return functions.containsKey(name);
430    }
431
432    public int getSystemPropertiesMode() {
433        return systemPropertiesMode;
434    }
435
436    /**
437     * Sets the system property mode.
438     *
439     * @see #SYSTEM_PROPERTIES_MODE_NEVER
440     * @see #SYSTEM_PROPERTIES_MODE_FALLBACK
441     * @see #SYSTEM_PROPERTIES_MODE_OVERRIDE
442     */
443    public void setSystemPropertiesMode(int systemPropertiesMode) {
444        this.systemPropertiesMode = systemPropertiesMode;
445    }
446
447    @Override
448    protected void doStart() throws Exception {
449        super.doStart();
450
451        if (systemPropertiesMode != SYSTEM_PROPERTIES_MODE_NEVER
452                && systemPropertiesMode != SYSTEM_PROPERTIES_MODE_FALLBACK
453                && systemPropertiesMode != SYSTEM_PROPERTIES_MODE_OVERRIDE) {
454            throw new IllegalArgumentException("Option systemPropertiesMode has invalid value: " + systemPropertiesMode);
455        }
456
457        // inject the component to the parser
458        if (propertiesParser instanceof DefaultPropertiesParser) {
459            ((DefaultPropertiesParser) propertiesParser).setPropertiesComponent(this);
460        }
461    }
462
463    @Override
464    protected void doStop() throws Exception {
465        cacheMap.clear();
466        super.doStop();
467    }
468
469    private String[] parseLocations(String[] locations) {
470        List<String> answer = new ArrayList<String>();
471
472        for (String location : locations) {
473            LOG.trace("Parsing location: {} ", location);
474
475            try {
476                location = FilePathResolver.resolvePath(location);
477                LOG.debug("Parsed location: {} ", location);
478                if (ObjectHelper.isNotEmpty(location)) {
479                    answer.add(location);
480                }
481            } catch (IllegalArgumentException e) {
482                if (!ignoreMissingLocation) {
483                    throw e;
484                } else {
485                    LOG.debug("Ignored missing location: {}", location);
486                }
487            }
488        }
489
490        // must return a not-null answer
491        return answer.toArray(new String[answer.size()]);
492    }
493
494    /**
495     * Key used in the locations cache
496     */
497    private static final class CacheKey implements Serializable {
498        private static final long serialVersionUID = 1L;
499        private final String[] locations;
500
501        private CacheKey(String[] locations) {
502            this.locations = locations;
503        }
504
505        @Override
506        public boolean equals(Object o) {
507            if (this == o) {
508                return true;
509            }
510            if (o == null || getClass() != o.getClass()) {
511                return false;
512            }
513
514            CacheKey that = (CacheKey) o;
515
516            if (!Arrays.equals(locations, that.locations)) {
517                return false;
518            }
519
520            return true;
521        }
522
523        @Override
524        public int hashCode() {
525            return locations != null ? Arrays.hashCode(locations) : 0;
526        }
527
528        @Override
529        public String toString() {
530            return "LocationKey[" + Arrays.asList(locations).toString() + "]";
531        }
532    }
533
534}