/**
* 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.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.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
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. </p>
* There are two internal configurations for properties that are programmatically set:
* <ul>
* <li>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)}.
* <li>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)}.
* </ul>
*
* 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.
*
* <p>
* Example:
* <pre>
* // 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);
* </pre>
*
* @author awang
*
*/
public class ConcurrentCompositeConfiguration extends ConcurrentMapConfiguration
implements AggregatedConfiguration, ConfigurationListener, Cloneable {
private Map<String, AbstractConfiguration> namedConfigurations = new ConcurrentHashMap<String, AbstractConfiguration>();
private List<AbstractConfiguration> configList = new CopyOnWriteArrayList<AbstractConfiguration>();
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 <em>container
* configuration</em>. 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 <em>container
* configuration</em>, 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 <em>container configuration</em> has been changed to new one or no longer at the end of
* the list. Otherwise it will be added in front of the <em>container configuration</em>.
*
* @param config the configuration to add (must not be <b>null</b>)
* @param name the name of this configuration (can be <b>null</b>)
*/
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<AbstractConfiguration> getConfigurations() {
return Collections.unmodifiableList(configList);
}
public List<String> getConfigurationNameList()
{
List<String> list = new ArrayList<String>(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 <em>container
* configuration</em>. 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 <em>container configuration</em> 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 (<b>null</b> 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 <em>container
* configuration</em>. <strong>Attention:</strong> 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 <em>container configuration</em>.
* <b>Warning: </b>{@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
* <em>container configuration</em> in the configurations list.
*/
@Override
public void setProperty(String key, Object value) {
containerConfiguration.setProperty(key, value);
}
/**
* Add the property with the <em>container configuration</em>.
* <b>Warning: </b>{@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
* <em>container configuration</em> in the configurations list.
*/
@Override
public void addProperty(String key, Object value) {
containerConfiguration.addProperty(key, value);
}
/**
* Clear the property with the <em>container configuration</em>.
* <b>Warning: </b>{@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
* <em>container configuration</em> in the configurations list.
*/
@Override
public void clearProperty(String key) {
containerConfiguration.clearProperty(key);
}
/**
* 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.
*/
public Object getProperty(String key)
{
if (overrideProperties.containsKey(key)) {
return overrideProperties.getProperty(key);
}
Configuration firstMatchingConfiguration = null;
for (Configuration config : configList)
{
if (config.containsKey(key))
{
firstMatchingConfiguration = config;
break;
}
}
if (firstMatchingConfiguration != null)
{
return firstMatchingConfiguration.getProperty(key);
}
else
{
return null;
}
}
/**
* 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<String> getKeys() throws ConcurrentModificationException
{
Set<String> keys = new LinkedHashSet<String>();
for (Iterator<String> it = overrideProperties.getKeys(); it.hasNext();) {
keys.add(it.next());
}
for (Configuration config : configList)
{
for (Iterator<String> 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<String, AbstractConfiguration> 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<String> getKeys(String prefix)
{
Set<String> keys = new LinkedHashSet<String>();
for (Iterator<String> it = overrideProperties.getKeys(prefix); it.hasNext();) {
keys.add(it.next());
}
for (Configuration config : configList)
{
for (Iterator<String> 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
* <b>null</b>)
*/
public Set<String> 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 <code>true</code> if the configuration contains a value for this
* key, <code>false</code> 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<Object> list = new ArrayList<Object>();
// add all elements from the first configuration containing the requested key
Iterator<AbstractConfiguration> it = configList.iterator();
if (overrideProperties.containsKey(key)) {
appendListProperty(list, overrideProperties, key);
}
while (it.hasNext() && list.isEmpty())
{
Configuration config = it.next();
if ((config != containerConfiguration || containerConfigurationChanged)
&& config.containsKey(key))
{
appendListProperty(list, config, key);
}
}
// add all elements from the in memory configuration
if (list.isEmpty()) {
appendListProperty(list, containerConfiguration, key);
}
if (list.isEmpty())
{
return defaultValue;
}
ListIterator<Object> lit = list.listIterator();
while (lit.hasNext())
{
lit.set(interpolate(lit.next()));
}
return list;
}
/**
* Get an array of strings associated with the given configuration key.
* If the key doesn't map to an existing object an empty array is returned
*
* @param key The configuration key.
* @return The associated string array if key is found.
*
*/
@Override
public String[] getStringArray(String key)
{
List<Object> list = getList(key);
// transform property values into strings
String[] tokens = new String[list.size()];
for (int i = 0; i < tokens.length; i++)
{
tokens[i] = String.valueOf(list.get(i));
}
return tokens;
}
/**
* Return the configuration at the specified index.
*
* @param index The index of the configuration to retrieve
* @return the configuration at this index
*/
public Configuration getConfiguration(int index)
{
return configList.get(index);
}
/**
* Returns the configuration with the given name. This can be <b>null</b>
* if no such configuration exists.
*
* @param name the name of the configuration
* @return the configuration with this name
*/
public Configuration getConfiguration(String name)
{
return namedConfigurations.get(name);
}
/**
* Returns the <em>container configuration</em> In this configuration
* changes are stored.
*
* @return the container configuration
*/
public Configuration getContainerConfiguration()
{
return containerConfiguration;
}
/**
* Returns a copy of this object. This implementation will create a deep
* clone, i.e. all configurations contained in this composite will also be
* cloned. This only works if all contained configurations support cloning;
* otherwise a runtime exception will be thrown. Registered event handlers
* won't get cloned.
*
*/
@Override
public Object clone()
{
try
{
ConcurrentCompositeConfiguration copy = (ConcurrentCompositeConfiguration) super
.clone();
copy.clearConfigurationListeners();
copy.configList = new LinkedList<AbstractConfiguration>();
copy.containerConfiguration = (AbstractConfiguration) ConfigurationUtils
.cloneConfiguration(getContainerConfiguration());
copy.configList.add(copy.containerConfiguration);
for (Configuration config : configList)
{
if (config != getContainerConfiguration())
{
copy.addConfiguration((AbstractConfiguration) ConfigurationUtils
.cloneConfiguration(config));
}
}
return copy;
}
catch (CloneNotSupportedException cnex)
{
// cannot happen
throw new ConfigurationRuntimeException(cnex);
}
}
/**
* Sets a flag whether added values for string properties should be checked
* for the list delimiter. This implementation ensures that the container
* configuration is correctly initialized.
*
* @param delimiterParsingDisabled the new value of the flag
*/
@Override
public void setDelimiterParsingDisabled(boolean delimiterParsingDisabled)
{
containerConfiguration.setDelimiterParsingDisabled(delimiterParsingDisabled);
super.setDelimiterParsingDisabled(delimiterParsingDisabled);
}
/**
* Sets the character that is used as list delimiter. This implementation
* ensures that the container configuration is correctly initialized.
*
* @param listDelimiter the new list delimiter character
*/
@Override
public void setListDelimiter(char listDelimiter)
{
containerConfiguration.setListDelimiter(listDelimiter);
super.setListDelimiter(listDelimiter);
}
/**
* Returns the configuration source, in which the specified key is defined.
* This method will iterate over all existing child configurations and check
* whether they contain the specified key. The following constellations are
* possible:
* <ul>
* <li>If the child configurations contains this key, the first one is returned.</li>
* <li>If none of the child configurations contain the key, <b>null</b> is
* returned.</li>
* </ul>
*
* @param key the key to be checked
* @return the source configuration of this key
*/
public Configuration getSource(String key)
{
if (key == null)
{
throw new IllegalArgumentException("Key must not be null!");
}
if (overrideProperties.containsKey(key)) {
return overrideProperties;
}
for (Configuration conf : configList)
{
if (conf.containsKey(key))
{
return conf;
}
}
return null;
}
/**
* Adds the value of a property to the given list. This method is used by
* {@code getList()} for gathering property values from the child
* configurations.
*
* @param dest the list for collecting the data
* @param config the configuration to query
* @param key the key of the property
*/
private static void appendListProperty(List<Object> dest, Configuration config,
String key)
{
Object value = config.getProperty(key);
if (value != null)
{
if (value instanceof Collection)
{
Collection<?> col = (Collection<?>) value;
dest.addAll(col);
}
else
{
dest.add(value);
}
}
}
/**
* Return whether sub configurations should propagate events to
* listeners to this configuration.
*/
public final boolean isPropagateEventFromSubConfigurations() {
return propagateEventToParent;
}
/**
* Set whether sub configurations should propagate events to
* listeners to this configuration. This is needed if this configuration
* is used as the configuration source of {@link DynamicPropertyFactory}.
*
* @param propagateEventToParent value to set
*/
public final void setPropagateEventFromSubConfigurations(boolean propagateEventToParent) {
this.propagateEventToParent = propagateEventToParent;
}
}