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        if (location != null) {
250            setLocations(location.split(","));
251        }
252    }
253
254    public String getEncoding() {
255        return encoding;
256    }
257
258    /**
259     * Encoding to use when loading properties file from the file system or classpath.
260     * <p/>
261     * If no encoding has been set, then the properties files is loaded using ISO-8859-1 encoding (latin-1)
262     * as documented by {@link java.util.Properties#load(java.io.InputStream)}
263     */
264    public void setEncoding(String encoding) {
265        this.encoding = encoding;
266    }
267
268    public PropertiesResolver getPropertiesResolver() {
269        return propertiesResolver;
270    }
271
272    /**
273     * To use a custom PropertiesResolver
274     */
275    public void setPropertiesResolver(PropertiesResolver propertiesResolver) {
276        this.propertiesResolver = propertiesResolver;
277    }
278
279    public PropertiesParser getPropertiesParser() {
280        return propertiesParser;
281    }
282
283    /**
284     * To use a custom PropertiesParser
285     */
286    public void setPropertiesParser(PropertiesParser propertiesParser) {
287        this.propertiesParser = propertiesParser;
288    }
289
290    public boolean isCache() {
291        return cache;
292    }
293
294    /**
295     * Whether or not to cache loaded properties. The default value is true.
296     */
297    public void setCache(boolean cache) {
298        this.cache = cache;
299    }
300    
301    public String getPropertyPrefix() {
302        return propertyPrefix;
303    }
304
305    /**
306     * Optional prefix prepended to property names before resolution.
307     */
308    public void setPropertyPrefix(String propertyPrefix) {
309        this.propertyPrefix = propertyPrefix;
310        this.propertyPrefixResolved = propertyPrefix;
311        if (ObjectHelper.isNotEmpty(this.propertyPrefix)) {
312            this.propertyPrefixResolved = FilePathResolver.resolvePath(this.propertyPrefix);
313        }
314    }
315
316    public String getPropertySuffix() {
317        return propertySuffix;
318    }
319
320    /**
321     * Optional suffix appended to property names before resolution.
322     */
323    public void setPropertySuffix(String propertySuffix) {
324        this.propertySuffix = propertySuffix;
325        this.propertySuffixResolved = propertySuffix;
326        if (ObjectHelper.isNotEmpty(this.propertySuffix)) {
327            this.propertySuffixResolved = FilePathResolver.resolvePath(this.propertySuffix);
328        }
329    }
330
331    public boolean isFallbackToUnaugmentedProperty() {
332        return fallbackToUnaugmentedProperty;
333    }
334
335    /**
336     * If true, first attempt resolution of property name augmented with propertyPrefix and propertySuffix
337     * before falling back the plain property name specified. If false, only the augmented property name is searched.
338     */
339    public void setFallbackToUnaugmentedProperty(boolean fallbackToUnaugmentedProperty) {
340        this.fallbackToUnaugmentedProperty = fallbackToUnaugmentedProperty;
341    }
342
343    public boolean isIgnoreMissingLocation() {
344        return ignoreMissingLocation;
345    }
346
347    /**
348     * Whether to silently ignore if a location cannot be located, such as a properties file not found.
349     */
350    public void setIgnoreMissingLocation(boolean ignoreMissingLocation) {
351        this.ignoreMissingLocation = ignoreMissingLocation;
352    }
353
354    public String getPrefixToken() {
355        return prefixToken;
356    }
357
358    /**
359     * Sets the value of the prefix token used to identify properties to replace.  Setting a value of
360     * {@code null} restores the default token (@link {@link #DEFAULT_PREFIX_TOKEN}).
361     */
362    public void setPrefixToken(String prefixToken) {
363        if (prefixToken == null) {
364            this.prefixToken = DEFAULT_PREFIX_TOKEN;
365        } else {
366            this.prefixToken = prefixToken;
367        }
368    }
369
370    public String getSuffixToken() {
371        return suffixToken;
372    }
373
374    /**
375     * Sets the value of the suffix token used to identify properties to replace.  Setting a value of
376     * {@code null} restores the default token (@link {@link #DEFAULT_SUFFIX_TOKEN}).
377     */
378    public void setSuffixToken(String suffixToken) {
379        if (suffixToken == null) {
380            this.suffixToken = DEFAULT_SUFFIX_TOKEN;
381        } else {
382            this.suffixToken = suffixToken;
383        }
384    }
385
386    public Properties getInitialProperties() {
387        return initialProperties;
388    }
389
390    /**
391     * Sets initial properties which will be used before any locations are resolved.
392     *
393     * @param initialProperties properties that are added first
394     */
395    public void setInitialProperties(Properties initialProperties) {
396        this.initialProperties = initialProperties;
397    }
398
399    public Properties getOverrideProperties() {
400        return overrideProperties;
401    }
402
403    /**
404     * Sets a special list of override properties that take precedence
405     * and will use first, if a property exist.
406     *
407     * @param overrideProperties properties that is used first
408     */
409    public void setOverrideProperties(Properties overrideProperties) {
410        this.overrideProperties = overrideProperties;
411    }
412
413    /**
414     * Gets the functions registered in this properties component.
415     */
416    public Map<String, PropertiesFunction> getFunctions() {
417        return functions;
418    }
419
420    /**
421     * Registers the {@link org.apache.camel.component.properties.PropertiesFunction} as a function to this component.
422     */
423    public void addFunction(PropertiesFunction function) {
424        this.functions.put(function.getName(), function);
425    }
426
427    /**
428     * Is there a {@link org.apache.camel.component.properties.PropertiesFunction} with the given name?
429     */
430    public boolean hasFunction(String name) {
431        return functions.containsKey(name);
432    }
433
434    public int getSystemPropertiesMode() {
435        return systemPropertiesMode;
436    }
437
438    /**
439     * Sets the system property mode.
440     *
441     * @see #SYSTEM_PROPERTIES_MODE_NEVER
442     * @see #SYSTEM_PROPERTIES_MODE_FALLBACK
443     * @see #SYSTEM_PROPERTIES_MODE_OVERRIDE
444     */
445    public void setSystemPropertiesMode(int systemPropertiesMode) {
446        this.systemPropertiesMode = systemPropertiesMode;
447    }
448
449    @Override
450    protected void doStart() throws Exception {
451        super.doStart();
452
453        if (systemPropertiesMode != SYSTEM_PROPERTIES_MODE_NEVER
454                && systemPropertiesMode != SYSTEM_PROPERTIES_MODE_FALLBACK
455                && systemPropertiesMode != SYSTEM_PROPERTIES_MODE_OVERRIDE) {
456            throw new IllegalArgumentException("Option systemPropertiesMode has invalid value: " + systemPropertiesMode);
457        }
458
459        // inject the component to the parser
460        if (propertiesParser instanceof DefaultPropertiesParser) {
461            ((DefaultPropertiesParser) propertiesParser).setPropertiesComponent(this);
462        }
463    }
464
465    @Override
466    protected void doStop() throws Exception {
467        cacheMap.clear();
468        super.doStop();
469    }
470
471    private String[] parseLocations(String[] locations) {
472        List<String> answer = new ArrayList<String>();
473
474        for (String location : locations) {
475            LOG.trace("Parsing location: {} ", location);
476
477            try {
478                location = FilePathResolver.resolvePath(location);
479                LOG.debug("Parsed location: {} ", location);
480                if (ObjectHelper.isNotEmpty(location)) {
481                    answer.add(location);
482                }
483            } catch (IllegalArgumentException e) {
484                if (!ignoreMissingLocation) {
485                    throw e;
486                } else {
487                    LOG.debug("Ignored missing location: {}", location);
488                }
489            }
490        }
491
492        // must return a not-null answer
493        return answer.toArray(new String[answer.size()]);
494    }
495
496    /**
497     * Key used in the locations cache
498     */
499    private static final class CacheKey implements Serializable {
500        private static final long serialVersionUID = 1L;
501        private final String[] locations;
502
503        private CacheKey(String[] locations) {
504            this.locations = locations;
505        }
506
507        @Override
508        public boolean equals(Object o) {
509            if (this == o) {
510                return true;
511            }
512            if (o == null || getClass() != o.getClass()) {
513                return false;
514            }
515
516            CacheKey that = (CacheKey) o;
517
518            if (!Arrays.equals(locations, that.locations)) {
519                return false;
520            }
521
522            return true;
523        }
524
525        @Override
526        public int hashCode() {
527            return locations != null ? Arrays.hashCode(locations) : 0;
528        }
529
530        @Override
531        public String toString() {
532            return "LocationKey[" + Arrays.asList(locations).toString() + "]";
533        }
534    }
535
536}