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