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