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