All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.netflix.config.ConcurrentCompositeConfiguration Maven / Gradle / Ivy

There is a newer version: 2.0.0-rc.7
Show newest version
/**
 * 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 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 list = new ArrayList(); // add all elements from the first configuration containing the requested key Iterator 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 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 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 null * 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 container configuration 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(); 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: *
    *
  • If the child configurations contains this key, the first one is returned.
  • *
  • If none of the child configurations contain the key, null is * returned.
  • *
* * @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 void appendListProperty(List dest, Configuration config, String key) { Object value = config.getProperty(key); if (value != null) { recordUsage(key); 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; } }