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