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