com.netflix.config.ConcurrentCompositeConfiguration Maven / Gradle / Ivy
/**
* Copyright 2014 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.netflix.config;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.ConcurrentModificationException;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;
import java.util.HashSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicReference;
import org.apache.commons.configuration.AbstractConfiguration;
import org.apache.commons.configuration.Configuration;
import org.apache.commons.configuration.ConfigurationRuntimeException;
import org.apache.commons.configuration.ConfigurationUtils;
import org.apache.commons.configuration.HierarchicalConfiguration;
import org.apache.commons.configuration.event.ConfigurationEvent;
import org.apache.commons.configuration.event.ConfigurationListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This class maintains a hierarchy of configurations in a list structure. The order of the list stands for the descending
* priority of the configurations when a property value is to be determined.
* For example, if you add Configuration1, and then Configuration2,
* {@link #getProperty(String)} will return any properties defined by Configuration1.
* Only if Configuration1 doesn't have the property, then
* Configuration2 will be checked.
*
* There are two internal configurations for properties that are programmatically set:
*
* - Configuration to hold any property introduced by {@link #addProperty(String, Object)} or {@link #setProperty(String, Object)}
* called directly on this class. This configuration will be called "container configuration" as it serves as the container of
* such properties. By default, this configuration remains at the last of the configurations list. It can be treated as
* a "base line" configuration that holds hard-coded parameters that can be overridden by any of other configurations added at runtime.
* You can replace this configuration by your own and change the position of the configuration in the list by calling
* {@link #setContainerConfiguration(AbstractConfiguration, String, int)}.
*
- Configuration to hold properties that are programmatically set (using {@link #setOverrideProperty(String, Object)}) to override values from any other
* configurations on the list. As contrast to container configuration, this configuration is always consulted first in
* {@link #getProperty(String)}.
*
*
* When adding configuration to this class, it is recommended to convert it into
* {@link ConcurrentMapConfiguration} or ConcurrentCompositeConfiguration using
* {@link com.netflix.config.util.ConfigurationUtils} to achieve
* maximal performance and thread safety.
*
*
* Example:
*
* // configuration from local properties file
String fileName = "...";
ConcurrentMapConfiguration configFromPropertiesFile =
new ConcurrentMapConfiguration(new PropertiesConfiguration(fileName));
// configuration from system properties
ConcurrentMapConfiguration configFromSystemProperties =
new ConcurrentMapConfiguration(new SystemConfiguration());
// configuration from a dynamic source
PolledConfigurationSource source = createMyOwnSource();
AbstractPollingScheduler scheduler = createMyOwnScheduler();
DynamicConfiguration dynamicConfiguration =
new DynamicConfiguration(source, scheduler);
// create a hierarchy of configuration that makes
// 1) dynamic configuration source override system properties and,
// 2) system properties override properties file
ConcurrentCompositeConfiguration finalConfig = new ConcurrentCompositeConfiguration();
finalConfig.add(dynamicConfiguration, "dynamicConfig");
finalConfig.add(configFromSystemProperties, "systemConfig");
finalConfig.add(configFromPropertiesFile, "fileConfig");
// register with DynamicPropertyFactory so that finalConfig
// becomes the source of dynamic properties
DynamicPropertyFactory.initWithConfigurationSource(finalConfig);
*
*
* @author awang
*
*/
public class ConcurrentCompositeConfiguration extends ConcurrentMapConfiguration
implements AggregatedConfiguration, ConfigurationListener, Cloneable {
public static final String ENABLE_STACK_TRACE = "archaius_enable_stack_trace";
public static final String ENABLE_INSTRUMENTATION = "archaius_enable_instrumentation";
private final boolean enableStackTrace = Boolean.parseBoolean(System.getProperty(ENABLE_STACK_TRACE));
private final boolean enableInstrumentation = Boolean.parseBoolean(System.getProperty(ENABLE_INSTRUMENTATION));
private Map namedConfigurations = new ConcurrentHashMap<>();
private final Map stackTraces = new ConcurrentHashMap<>();
private final AtomicReference> usedPropertiesRef = new AtomicReference<>(ConcurrentHashMap.newKeySet());
public Set getUsedProperties() {
return Collections.unmodifiableSet(new HashSet<>(usedPropertiesRef.get()));
}
public Set getAndClearUsedProperties() {
Set ret = usedPropertiesRef.getAndSet(ConcurrentHashMap.newKeySet());
return Collections.unmodifiableSet(ret);
}
private List configList = new CopyOnWriteArrayList();
private static final Logger logger = LoggerFactory.getLogger(ConcurrentCompositeConfiguration.class);
public static final int EVENT_CONFIGURATION_SOURCE_CHANGED = 10001;
private volatile boolean propagateEventToParent = true;
private AbstractConfiguration overrideProperties;
/**
* Configuration that holds properties set directly with {@link #setProperty(String, Object)}
*/
private AbstractConfiguration containerConfiguration;
/**
* Stores a flag whether the current in-memory configuration is also a
* child configuration.
*/
private volatile boolean containerConfigurationChanged = true;
private ConfigurationListener eventPropagater = new ConfigurationListener() {
@Override
public void configurationChanged(ConfigurationEvent event) {
boolean beforeUpdate = event.isBeforeUpdate();
if (propagateEventToParent) {
int type = event.getType();
String name = event.getPropertyName();
Object value = event.getPropertyValue();
Object finalValue;
switch(type) {
case HierarchicalConfiguration.EVENT_ADD_NODES:
case EVENT_CLEAR:
case EVENT_CONFIGURATION_SOURCE_CHANGED:
fireEvent(type, name, value, beforeUpdate);
break;
case EVENT_ADD_PROPERTY:
case EVENT_SET_PROPERTY:
if (beforeUpdate) {
// we want the validators to run even if the source is not
// the winning configuration
fireEvent(type, name, value, beforeUpdate);
} else {
AbstractConfiguration sourceConfig = (AbstractConfiguration) event.getSource();
AbstractConfiguration winningConf = (AbstractConfiguration) getSource(name);
if (winningConf == null || getIndexOfConfiguration(sourceConfig) <= getIndexOfConfiguration(winningConf)) {
fireEvent(type, name, value, beforeUpdate);
}
}
break;
case EVENT_CLEAR_PROPERTY:
finalValue = ConcurrentCompositeConfiguration.this.getProperty(name);
if (finalValue == null) {
fireEvent(type, name, value, beforeUpdate);
} else {
fireEvent(EVENT_SET_PROPERTY, name, finalValue, beforeUpdate);
}
break;
default:
break;
}
}
}
};
/**
* Creates an empty CompositeConfiguration object which can then
* be added some other Configuration files
*/
public ConcurrentCompositeConfiguration()
{
clear();
}
/**
* Creates a ConcurrentCompositeConfiguration object with a specified container
* configuration. This configuration will store any changes made by {@link #setProperty(String, Object)}
* and {@link #addProperty(String, Object)}.
*
* @param containerConfiguration the configuration to use as container configuration
*/
public ConcurrentCompositeConfiguration(AbstractConfiguration containerConfiguration)
{
configList.clear();
this.containerConfiguration = containerConfiguration;
configList.add(containerConfiguration);
}
/**
* Creates a ConcurrentCompositeConfiguration with a specified container
* configuration, and then adds the given collection of configurations.
*
* @param containerConfiguration container configuration to use
* @param configurations the collection of configurations to add
*/
public ConcurrentCompositeConfiguration(AbstractConfiguration containerConfiguration,
Collection extends AbstractConfiguration> configurations)
{
this(containerConfiguration);
if (configurations != null)
{
for (AbstractConfiguration c : configurations)
{
addConfiguration(c);
}
}
}
/**
* Event listener call back for configuration update events. This method is
* called whenever one of the contained configurations was modified. This method
* does nothing.
*
* @param event the update event
*/
@Override
public void configurationChanged(ConfigurationEvent event)
{
}
public void invalidate()
{
}
/**
* Add a child configuration without a name. Make a call to {@link #addConfiguration(AbstractConfiguration, String)}
* with the name being null.
*
* @param config the configuration to add
*/
public final void addConfiguration(AbstractConfiguration config)
{
addConfiguration(config, null);
}
/**
* Adds a new child configuration to this configuration with an optional
* name. The configuration will be added to the end of the list
* if container configuration has been changed to new one or no longer at the end of
* the list. Otherwise it will be added in front of the container configuration.
*
* @param config the configuration to add (must not be null)
* @param name the name of this configuration (can be null)
*/
public void addConfiguration(AbstractConfiguration config, String name)
{
if (containerConfigurationChanged) {
addConfigurationAtIndex(config, name, configList.size());
} else {
addConfigurationAtIndex(config, name, configList.indexOf(containerConfiguration));
}
}
/**
* Get the configurations added.
*/
public List getConfigurations() {
return Collections.unmodifiableList(configList);
}
public List getConfigurationNameList()
{
List list = new ArrayList(configList.size());
for (AbstractConfiguration configuration: configList)
{
boolean foundName = false;
for (String name: namedConfigurations.keySet()) {
if (configuration == namedConfigurations.get(name)) {
foundName = true;
list.add(name);
break;
}
}
if (!foundName) {
list.add(null);
}
}
return list;
}
public int getIndexOfConfiguration(AbstractConfiguration config) {
return configList.indexOf(config);
}
public int getIndexOfContainerConfiguration() {
return configList.indexOf(containerConfiguration);
}
private void checkIndex(int newIndex) {
if (newIndex < 0 || newIndex > configList.size()) {
throw new IndexOutOfBoundsException(newIndex + " is out of bounds of the size of configuration list " + configList.size());
}
}
/**
* Adds a child configuration and makes it the container
* configuration. This means that all future property write operations
* are executed on this configuration. Note that the current container
* configuration stays in the list of child configurations
* at its current position, but it passes its role as container
* configuration to the new one.
*
* @param config the configuration to be added
* @param name the name of the configuration to be added
* @param index index to add this configuration
*
* @throws IndexOutOfBoundsException
*/
public void setContainerConfiguration(AbstractConfiguration config, String name, int index) throws IndexOutOfBoundsException {
if (!configList.contains(config)) {
checkIndex(index);
containerConfigurationChanged = true;
containerConfiguration = config;
addConfigurationAtIndex(config, name, index);
} else {
logger.warn(config + " is not added as it already exits");
}
}
/**
* Change the position of the container configuration to a new index.
*
* @throws IndexOutOfBoundsException
*/
public void setContainerConfigurationIndex(int newIndex) throws IndexOutOfBoundsException {
if (newIndex < 0 || newIndex >= configList.size()) {
throw new IndexOutOfBoundsException("Cannot change to the new index " + newIndex + " in the list of size " + configList.size());
} else if (newIndex == configList.indexOf(containerConfiguration)) {
// nothing to do
return;
}
containerConfigurationChanged = true;
configList.remove(containerConfiguration);
configList.add(newIndex, containerConfiguration);
}
/**
* Add a configuration with a name at a particular index.
*
* @throws IndexOutOfBoundsException
*/
public void addConfigurationAtIndex(AbstractConfiguration config, String name, int index)
throws IndexOutOfBoundsException {
if (!configList.contains(config)) {
checkIndex(index);
configList.add(index, config);
if (name != null) {
namedConfigurations.put(name, config);
}
config.addConfigurationListener(eventPropagater);
fireEvent(EVENT_CONFIGURATION_SOURCE_CHANGED, null, null, false);
} else {
logger.warn(config + " is not added as it already exits");
}
}
public void addConfigurationAtFront(AbstractConfiguration config, String name) {
addConfigurationAtIndex(config, name, 0);
}
/**
* Remove a configuration. The container configuration cannot be removed.
*
* @param config The configuration to remove
*/
public boolean removeConfiguration(Configuration config)
{
// Make sure that you can't remove the inMemoryConfiguration from
// the CompositeConfiguration object
if (!config.equals(containerConfiguration))
{
String configName = getNameForConfiguration(config);
if (configName != null) {
namedConfigurations.remove(configName);
}
return configList.remove(config);
} else {
throw new IllegalArgumentException("Can't remove container configuration");
}
}
public AbstractConfiguration removeConfigurationAt(int index) {
AbstractConfiguration config = configList.remove(index);
String nameFound = getNameForConfiguration(config);
if (nameFound != null) {
namedConfigurations.remove(nameFound);
}
return config;
}
/**
* Removes the configuration with the specified name.
*
* @param name the name of the configuration to be removed
* @return the removed configuration (null if this configuration
* was not found)
*/
public Configuration removeConfiguration(String name)
{
Configuration conf = getConfiguration(name);
if (conf != null && !conf.equals(containerConfiguration))
{
configList.remove(conf);
namedConfigurations.remove(name);
} else if (conf != null && conf.equals(containerConfiguration)) {
throw new IllegalArgumentException("Can't remove container configuration");
}
return conf;
}
/**
* Return the number of configurations.
*
* @return the number of configuration
*/
public int getNumberOfConfigurations()
{
return configList.size();
}
/**
* Removes all child configurations and reinitializes the container
* configuration. Attention: A new container
* configuration is created; the old one is lost.
*/
@Override
public final void clear()
{
fireEvent(EVENT_CLEAR, null, null, true);
configList.clear();
namedConfigurations.clear();
// recreate the in memory configuration
containerConfiguration = new ConcurrentMapConfiguration();
containerConfiguration.setThrowExceptionOnMissing(isThrowExceptionOnMissing());
containerConfiguration.setListDelimiter(getListDelimiter());
containerConfiguration.setDelimiterParsingDisabled(isDelimiterParsingDisabled());
containerConfiguration.addConfigurationListener(eventPropagater);
configList.add(containerConfiguration);
overrideProperties = new ConcurrentMapConfiguration();
overrideProperties.setThrowExceptionOnMissing(isThrowExceptionOnMissing());
overrideProperties.setListDelimiter(getListDelimiter());
overrideProperties.setDelimiterParsingDisabled(isDelimiterParsingDisabled());
overrideProperties.addConfigurationListener(eventPropagater);
fireEvent(EVENT_CLEAR, null, null, false);
containerConfigurationChanged = false;
invalidate();
}
/**
* Override the same property in any other configurations in the list.
*/
public void setOverrideProperty(String key, Object finalValue) {
overrideProperties.setProperty(key, finalValue);
}
/**
* Remove the overriding property set by {@link #setOverrideProperty(String, Object)}
*/
public void clearOverrideProperty(String key) {
overrideProperties.clearProperty(key);
}
/**
* Set the property with the container configuration.
* Warning: {@link #getProperty(String)} on this key may not return the same value set by this method
* if there is any other configuration that contain the same property and is in front of the
* container configuration in the configurations list.
*/
@Override
public void setProperty(String key, Object value) {
containerConfiguration.setProperty(key, value);
}
/**
* Add the property with the container configuration.
* Warning: {@link #getProperty(String)} on this key may not return the same value set by this method
* if there is any other configuration that contain the same property and is in front of the
* container configuration in the configurations list.
*/
@Override
public void addProperty(String key, Object value) {
containerConfiguration.addProperty(key, value);
}
/**
* Clear the property with the container configuration.
* Warning: {@link #getProperty(String)} on this key may still return some value
* if there is any other configuration that contain the same property and is in front of the
* container configuration in the configurations list.
*/
@Override
public void clearProperty(String key) {
containerConfiguration.clearProperty(key);
}
public Object getProperty(String key) {
return getProperty(key, true);
}
public Object getPropertyUninstrumented(String key) {
return getProperty(key, false);
}
/**
* Read property from underlying composite. It first checks if the property has been overridden
* by {@link #setOverrideProperty(String, Object)} and if so return the overriding value.
* Otherwise, it iterates through the list of sub configurations until it finds one that contains the
* property and return the value from that sub configuration. It returns null of the property does
* not exist.
*
* @param key key to use for mapping
*
* @return object associated with the given configuration key. null if it does not exist.
*/
private Object getProperty(String key, boolean instrument)
{
if (overrideProperties.containsKey(key)) {
if (instrument) {
recordUsage(key);
}
return overrideProperties.getProperty(key);
}
Configuration firstMatchingConfiguration = null;
for (Configuration config : configList) {
if (config.containsKey(key)) {
if (instrument) {
recordUsage(key);
}
firstMatchingConfiguration = config;
break;
}
}
if (firstMatchingConfiguration != null) {
// Propagate uninstrumented call if necessary
if (enableInstrumentation
&& !instrument
&& firstMatchingConfiguration instanceof ConcurrentCompositeConfiguration) {
return ((ConcurrentCompositeConfiguration) firstMatchingConfiguration).getPropertyUninstrumented(key);
}
return firstMatchingConfiguration.getProperty(key);
}
else
{
return null;
}
}
/**
* Manual endpoint for recording usage of a property for instrumentation purposes.
* @param key Property whose usage is being reported
*/
public void recordUsage(String key) {
if (enableInstrumentation) {
usedPropertiesRef.get().add(key);
if (enableStackTrace) {
String trace = Arrays.toString(Thread.currentThread().getStackTrace());
stackTraces.merge(trace, 1, (v1, v2) -> v1 + 1);
}
}
}
/**
* Get all the keys contained by sub configurations.
*
* @throws ConcurrentModificationException if concurrent modification happens on any sub configuration
* when it is iterated to get all the keys
*
*/
public Iterator getKeys() throws ConcurrentModificationException
{
Set keys = new LinkedHashSet();
for (Iterator it = overrideProperties.getKeys(); it.hasNext();) {
keys.add(it.next());
}
for (Configuration config : configList)
{
for (Iterator it = config.getKeys(); it.hasNext();)
{
try {
keys.add(it.next());
} catch (ConcurrentModificationException e) {
logger.error("unexpected exception when iterating the keys for configuration " + config
+ " with name " + getNameForConfiguration(config));
throw e;
}
}
}
return keys.iterator();
}
private String getNameForConfiguration(Configuration config) {
for (Map.Entry entry: namedConfigurations.entrySet()) {
if (entry.getValue() == config) {
return entry.getKey();
}
}
return null;
}
/**
* Get the list of the keys contained in the sub configurations that match the
* specified prefix.
*
*/
@Override
public Iterator getKeys(String prefix)
{
Set keys = new LinkedHashSet();
for (Iterator it = overrideProperties.getKeys(prefix); it.hasNext();) {
keys.add(it.next());
}
for (Configuration config : configList)
{
for (Iterator it = config.getKeys(prefix); it.hasNext();)
{
keys.add(it.next());
}
}
return keys.iterator();
}
/**
* Returns a set with the names of all configurations contained in this
* configuration. Of course here are only these configurations
* listed, for which a name was specified when they were added.
*
* @return a set with the names of the contained configurations (never
* null)
*/
public Set getConfigurationNames()
{
return namedConfigurations.keySet();
}
@Override
public boolean isEmpty()
{
if (overrideProperties.isEmpty()) {
return false;
}
for (Configuration config : configList)
{
if (!config.isEmpty())
{
return false;
}
}
return true;
}
/**
* Check if the any of the sub configurations contains the specified key.
*
* @param key the key whose presence in this configuration is to be tested
*
* @return true
if the configuration contains a value for this
* key, false
otherwise
*
*/
@Override
public boolean containsKey(String key)
{
if (overrideProperties.containsKey(key)) {
return true;
}
for (Configuration config : configList)
{
if (config.containsKey(key))
{
return true;
}
}
return false;
}
/**
* Get a List of objects associated with the given configuration key.
* If the key doesn't map to an existing object, the default value
* is returned.
*
* @param key The configuration key.
* @param defaultValue The default value.
* @return The associated List of value.
*
*/
@Override
public List getList(String key, List defaultValue)
{
List