/**
 * 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.sources;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.HashMap;
import java.util.Map;

import javax.sql.DataSource;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.netflix.config.PollResult;
import com.netflix.config.PolledConfigurationSource;

/**
 * Simple JDBC source of config properties. Assuming you have a table with a
 * column that stores the keys and a column to store values, this class can be
 * used as your source of polled configuration.
 *
 * @author stonse
 *
 */
public class JDBCConfigurationSource implements PolledConfigurationSource {

	private static Logger log = LoggerFactory
			.getLogger(JDBCConfigurationSource.class);

	/** The datasource to connect to the database. */
	private DataSource datasource;

	/** The JDBC query to obtain properties stored in an RDBMS. */
	// example:
	// "Select distinct property_key, property_value from SITE_PROPERTIES"
	private String query;

	/** The column containing the keys. */
	private String keyColumnName = "property_key";
	/** The column containing the values. */
	private String valueColumnName = "property_value";

	/**
	 * Constructor
	 *
	 * @param datasource
	 *            The DataSource object for the JDBC; e.g.
	 *            new OracleDataSource(databaseName, schema);
	 * @param query
	 *            The query statement to fetch the properties; e.g.
	 *            "Select distinct property_key, property_value from SITE_PROPERTIES"
	 * @param keyColumnName
	 *            The column name which stores the property keys; e.g.
	 *            property_key
	 * @param valueColumnName
	 *            The column name which stores the property values; e.g.
	 *            property_value
	 */
	public JDBCConfigurationSource(DataSource datasource, String query,
			String keyColumnName, String valueColumnName) {
		this.datasource = datasource;
		this.query = query;
		this.keyColumnName = keyColumnName;
		this.valueColumnName = valueColumnName;
	}

	// ...
	@Override
	public PollResult poll(boolean initial, Object checkPoint) throws Exception {
		// implement logic to retrieve properties from DB
		Map<String, Object> map = load();
		return PollResult.createFull(map);
	}

	/**
	 * Returns a Map<String, Object> of properties stored in the
	 * database
	 *
	 * @throws Exception
	 */
	synchronized Map<String, Object> load() throws Exception {
		Map<String, Object> map = new HashMap<String, Object>();
		Connection conn = null;
		PreparedStatement pstmt = null;
		ResultSet rs = null;

		try {
			conn = getConnection();
			pstmt = conn.prepareStatement(query.toString());
			rs = pstmt.executeQuery();
			while (rs.next()) {
				String key = (String) rs.getObject(keyColumnName);
				Object value = rs.getObject(valueColumnName);
				map.put(key, value);
			}

		} catch (SQLException e) {
			throw e;
		} finally {
			close(conn, pstmt, rs);
		}
		return map;
	}

	/**
	 * Returns the used DataSource object.
	 *
	 * @return the data source
	 * @since 1.4
	 */
	public DataSource getDatasource() {
		return datasource;
	}

	/**
	 * Returns a Connection object. This method is called when ever
	 * the database is to be accessed. This implementation returns a connection
	 * from the current DataSource.
	 *
	 * @return the Connection object to be used
	 * @throws SQLException
	 *             if an error occurs
	 * @since 1.4
	 */
	protected Connection getConnection() throws SQLException {
		return getDatasource().getConnection();
	}

	/**
	 * Close a Connection and, Statement. Avoid
	 * closing if null and hide any SQLExceptions that occur.
	 *
	 * @param conn
	 *            The database connection to close
	 * @param stmt
	 *            The statement to close
	 */
	private void close(Connection conn, Statement stmt, ResultSet rs) {
		try {
			if (rs != null) {
				rs.close();
			}
		} catch (SQLException e) {
			log.error("An error occured on closing the ResultSet", e);
		}

		try {
			if (stmt != null) {
				stmt.close();
			}
		} catch (SQLException e) {
			log.error("An error occured on closing the statement", e);
		}

		try {
			if (conn != null) {
				conn.close();
			}
		} catch (SQLException e) {
			log.error("An error occured on closing the connection", e);
		}
	}

}


/**
 * 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.util;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.Map.Entry;

import org.apache.commons.configuration.AbstractConfiguration;
import org.apache.commons.configuration.CombinedConfiguration;
import org.apache.commons.configuration.Configuration;
import org.apache.commons.configuration.ConfigurationException;
import org.apache.commons.configuration.PropertiesConfiguration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.netflix.config.ConcurrentCompositeConfiguration;
import com.netflix.config.ConcurrentMapConfiguration;
import com.netflix.config.ConfigurationManager;

/**
 * Utility class for configuration.
 *
 * @author awang
 *
 */
public class ConfigurationUtils {

    private static final Logger logger = LoggerFactory.getLogger(ConfigurationUtils.class);
    /**
     * Convert CombinedConfiguration into {@link ConcurrentCompositeConfiguration} as the later has better performance
     * and thread safety.
     *
     * @param config Configuration to be converted
     */
    public static ConcurrentCompositeConfiguration convertToConcurrentCompositeConfiguration(CombinedConfiguration config) {
        ConcurrentCompositeConfiguration root = new ConcurrentCompositeConfiguration();
        IdentityHashMap<Configuration, String> reverseMap = new IdentityHashMap<Configuration, String>();
        for (String name: (Set<String>) config.getConfigurationNames()) {
            Configuration child = config.getConfiguration(name);
            reverseMap.put(child, name);
        }
        for (int i = 0; i < config.getNumberOfConfigurations(); i++) {
            Configuration child = config.getConfiguration(i);
            String name = reverseMap.get(child);
            if (child instanceof CombinedConfiguration) {
                CombinedConfiguration combinedConf = (CombinedConfiguration) child;
                ConcurrentCompositeConfiguration newConf = convertToConcurrentCompositeConfiguration(combinedConf);
                root.addConfiguration(newConf, name);
            } else {
                Configuration conf = new ConcurrentMapConfiguration(child);
                root.addConfiguration((AbstractConfiguration) conf, name);
            }
        }
        return root;
    }

    /**
     * Gets all named sub-configuration from a configuration in a map. This method examines each sub-configuration
     * which is an instance of {@link ConcurrentCompositeConfiguration} or CombinedConfiguration and extract the
     * named configurations out of them.
     *
     * @param conf Configuration to get all the named sub-configurations
     * @return map where key is the name of the sub-configuration and value is the sub-configuration
     */
    public static Map<String, Configuration> getAllNamedConfiguration(Configuration conf) {
        List<Configuration> toProcess = new ArrayList<Configuration>();
        Map<String, Configuration> map = new HashMap<String, Configuration>();
        toProcess.add(conf);
        while (!toProcess.isEmpty()) {
            Configuration current = toProcess.remove(0);
            if (current instanceof ConcurrentCompositeConfiguration) {
                ConcurrentCompositeConfiguration composite = (ConcurrentCompositeConfiguration) current;
                for (String name: composite.getConfigurationNames()) {
                    map.put(name, composite.getConfiguration(name));
                }
                for (Configuration c: composite.getConfigurations()) {
                    toProcess.add(c);
                }
            } else if (current instanceof CombinedConfiguration) {
                CombinedConfiguration combined = (CombinedConfiguration) current;
                for (String name: (Set<String>) combined.getConfigurationNames()) {
                    map.put(name, combined.getConfiguration(name));
                }
                for (int i = 0; i < combined.getNumberOfConfigurations(); i++) {
                    toProcess.add(combined.getConfiguration(i));
                }
            }
        }
        return map;
    }

    /**
     * Utility method to obtain Properties given an instance of AbstractConfiguration.
     * Returns an empty Properties object if the config has no properties or is null.
     * @param config Configuration to get the properties
     * @return properties extracted from the configuration
     */
    public static Properties getProperties(Configuration config) {
 	   Properties p = new Properties();
 	   if (config != null){
	 	   Iterator<String> it = config.getKeys();
	 	   while (it.hasNext()){
	 		   String key = it.next();
	 		   if (key != null) {
	 		      Object value = config.getProperty(key);
                  if (value != null) {
                      p.put(key, value);
                  }	 		   }
	 	   }
 	   }
  	   return p;
    }

    public static void loadProperties(Properties props, Configuration config) {
        for (Entry<Object, Object> entry: props.entrySet()) {
            config.setProperty((String) entry.getKey(), entry.getValue());
        }
    }

    static void loadFromPropertiesFile(AbstractConfiguration config, String baseUrl, Set<String> loaded, String... nextLoadKeys) {
        String nextLoad = getNextLoad(config, nextLoadKeys);
        if (nextLoad == null) {
            return;
        }
        String[] filesToLoad = nextLoad.split(",");
        for (String fileName: filesToLoad) {
            fileName = fileName.trim();
            try {
                URL url = new URL(baseUrl + "/" + fileName);
                // avoid circle
                if (loaded.contains(url.toExternalForm())) {
                    logger.warn(url + " is already loaded");
                    continue;
                }
                loaded.add(url.toExternalForm());
                PropertiesConfiguration nextConfig = new OverridingPropertiesConfiguration(url);
                copyProperties(nextConfig, config);
                logger.info("Loaded properties file " + url);
                loadFromPropertiesFile(config, baseUrl, loaded, nextLoadKeys);
            } catch (Throwable e) {
                logger.warn("Unable to load properties file", e);
            }
        }
    }

    public static AbstractConfiguration getConfigFromPropertiesFile(URL startingUrl, Set<String> loaded, String... nextLoadKeys)
            throws FileNotFoundException {
        if (loaded.contains(startingUrl.toExternalForm())) {
            logger.warn(startingUrl + " is already loaded");
            return null;
        }
        PropertiesConfiguration propConfig = null;
        try {
            propConfig = new OverridingPropertiesConfiguration(startingUrl);
            logger.info("Loaded properties file " + startingUrl);
        } catch (ConfigurationException e) {
            Throwable cause = e.getCause();
            if (cause instanceof FileNotFoundException) {
                throw (FileNotFoundException) cause;
            } else {
                throw new RuntimeException(e);
            }
        }

        if (nextLoadKeys == null) {
            return propConfig;
        }
        String urlString = startingUrl.toExternalForm();
        String base = urlString.substring(0, urlString.lastIndexOf("/"));
        loaded.add(startingUrl.toString());
        loadFromPropertiesFile(propConfig, base, loaded, nextLoadKeys);
        return propConfig;
    }

    public static void copyProperties(Configuration from, Configuration to) {
        for (Iterator<String> i = from.getKeys(); i.hasNext(); ) {
            String key = i.next();
            if (key != null) {
                Object value = from.getProperty(key);
                if (value != null) {
                    to.setProperty(key, value);
                }
            }
        }
    }

    public static Properties getPropertiesFromFile(URL startingUrl, Set<String> loaded, String... nextLoadKeys)
            throws FileNotFoundException {
        AbstractConfiguration config = getConfigFromPropertiesFile(startingUrl, loaded, nextLoadKeys);
        return getProperties(config);
    }

    private static String getNextLoad(Configuration propConfig, String... nextLoadPropertyKeys) {
        String nextLoadKeyToUse = null;
        for (String key: nextLoadPropertyKeys) {
            if (propConfig.getProperty(key) != null) {
                nextLoadKeyToUse = key;
                break;
            }
        }
        // there is no next load for this properties file
        if (nextLoadKeyToUse == null) {
            return null;
        }
        // make a copy of current existing properties
        ConcurrentMapConfiguration config = new ConcurrentMapConfiguration();

        // need to have all the properties to interpolate next load property value
        copyProperties(ConfigurationManager.getConfigInstance(), config);
        copyProperties(propConfig, config);
        // In case this is a list of files to load, always treat the value as a list
        List<Object> list = config.getList(nextLoadKeyToUse);
        StringBuilder sb = new StringBuilder();
        for (Object value: list) {
            sb.append(value).append(",");
        }
        String nextLoad = sb.toString();
        propConfig.clearProperty(nextLoadKeyToUse);
        return nextLoad;
    }

    /**
     * Load properties from InputStream with utf-8 encoding, and it will take care of closing the input stream.
     * @param fin
     * @return
     * @throws IOException
     */
    public static Properties loadPropertiesFromInputStream(InputStream fin) throws IOException {
        Properties props = new Properties();
        InputStreamReader reader = new InputStreamReader(fin, "UTF-8");
        try {
            props.load(reader);
            return props;
        } finally {
            if (reader != null) {
                reader.close();
            }
            if (fin != null) {
                fin.close();
            }
        }
    }
}

class OverridingPropertiesConfiguration extends PropertiesConfiguration {

    public OverridingPropertiesConfiguration() {
        super();
    }

    public OverridingPropertiesConfiguration(File file)
            throws ConfigurationException {
        super(file);
    }

    public OverridingPropertiesConfiguration(String fileName)
            throws ConfigurationException {
        super(fileName);
    }

    public OverridingPropertiesConfiguration(URL url)
            throws ConfigurationException {
        super(url);
    }

    /**
     * Need to override this method for PDCLOUD-1809.
     *
     */
    @Override
    public void addProperty(String name, Object value) {
        if (containsKey(name)) {
            // clear without triggering an event
            clearPropertyDirect(name);
        }
        super.addProperty(name, value);
    }
}


/**
 * 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 com.google.common.base.Splitter;

import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;

import org.apache.commons.configuration.AbstractConfiguration;
import org.apache.commons.configuration.Configuration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.base.Splitter;
import com.netflix.config.validation.ValidationException;

/**
 * Apply the {@link WatchedUpdateResult} to the configuration.

 *
 * If the result is a full result from source, each property in the result is added/set in the configuration. Any
 * property that is in the configuration - but not in the result - is deleted if ignoreDeletesFromSource is false.

 *
 * If the result is incremental, properties will be added and changed from the partial result in the configuration.
 * Deleted properties are deleted from configuration iff ignoreDeletesFromSource is false.
 *
 * This code is shared by both {@link AbstractPollingScheduler} and {@link DynamicWatchedConfiguration}.
 */
public class DynamicPropertyUpdater {
    private static Logger logger = LoggerFactory.getLogger(DynamicPropertyUpdater.class);

    /**
     * Updates the properties in the config param given the contents of the result param.
     *
     * @param result
     *            either an incremental or full set of data
     * @param config
     *            underlying config map
     * @param ignoreDeletesFromSource
     *            if true, deletes will be skipped
     */
    public void updateProperties(final WatchedUpdateResult result, final Configuration config,
            final boolean ignoreDeletesFromSource) {
        if (result == null || !result.hasChanges()) {
            return;
        }

        logger.debug("incremental result? [{}]", result.isIncremental());
        logger.debug("ignored deletes from source? [{}]", ignoreDeletesFromSource);

        if (!result.isIncremental()) {
            Map<String, Object> props = result.getComplete();
            if (props == null) {
                return;
            }
            for (Entry<String, Object> entry : props.entrySet()) {
                addOrChangeProperty(entry.getKey(), entry.getValue(), config);
            }
            Set<String> existingKeys = new HashSet<String>();
            for (Iterator<String> i = config.getKeys(); i.hasNext();) {
                existingKeys.add(i.next());
            }
            if (!ignoreDeletesFromSource) {
                for (String key : existingKeys) {
                    if (!props.containsKey(key)) {
                        deleteProperty(key, config);
                    }
                }
            }
        } else {
            Map<String, Object> props = result.getAdded();
            if (props != null) {
                for (Entry<String, Object> entry : props.entrySet()) {
                    addOrChangeProperty(entry.getKey(), entry.getValue(), config);
                }
            }
            props = result.getChanged();
            if (props != null) {
                for (Entry<String, Object> entry : props.entrySet()) {
                    addOrChangeProperty(entry.getKey(), entry.getValue(), config);
                }
            }
            if (!ignoreDeletesFromSource) {
                props = result.getDeleted();
                if (props != null) {
                    for (String name : props.keySet()) {
                        deleteProperty(name, config);
                    }
                }
            }
        }
    }

    /**
     * Add or update the property in the underlying config depending on if it exists
     *
     * @param name
     * @param newValue
     * @param config
     */
    void addOrChangeProperty(final String name, final Object newValue, final Configuration config) {
        // We do not want to abort the operation due to failed validation on one property
        try {
            if (!config.containsKey(name)) {
                logger.debug("adding property key [{}], value [{}]", name, newValue);

                config.addProperty(name, newValue);
            } else {
                Object oldValue = config.getProperty(name);

                if (newValue != null) {
                    Object newValueArray;
                    if (oldValue instanceof CopyOnWriteArrayList && AbstractConfiguration.getDefaultListDelimiter() != '\0'){
                        newValueArray =
                                        new CopyOnWriteArrayList();

                      Iterable<String> stringiterator = Splitter.on(AbstractConfiguration.getDefaultListDelimiter()).omitEmptyStrings().trimResults().split((String)newValue);
                      for(String s :stringiterator){
                            ((CopyOnWriteArrayList) newValueArray).add(s);
                        }
                      } else {
                          newValueArray = newValue;
                      }

                    if (!newValueArray.equals(oldValue)) {
                        logger.debug("updating property key [{}], value [{}]", name, newValue);

                        config.setProperty(name, newValue);
                    }

                } else if (oldValue != null) {
                    logger.debug("nulling out property key [{}]", name);

                    config.setProperty(name, null);
                }
            }
        } catch (ValidationException e) {
            logger.warn("Validation failed for property " + name, e);
        }
    }

    /**
     * Delete a property in the underlying config
     *
     * @param key
     * @param config
     */
    void deleteProperty(final String key, final Configuration config) {
        if (config.containsKey(key)) {
            logger.debug("deleting property key [" + key + "]");

            config.clearProperty(key);
        }
    }
}


/**
 * 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.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.locks.ReentrantLock;

import org.apache.commons.configuration.AbstractConfiguration;
import org.apache.commons.configuration.Configuration;
import org.apache.commons.configuration.PropertyConverter;
import org.apache.commons.configuration.event.ConfigurationErrorEvent;
import org.apache.commons.configuration.event.ConfigurationErrorListener;
import org.apache.commons.configuration.event.ConfigurationEvent;
import org.apache.commons.configuration.event.ConfigurationListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.netflix.config.validation.ValidationException;

/**
 * This class uses a ConcurrentHashMap for reading/writing a property to achieve high
 * throughput and thread safety. The implementation is lock free for {@link #getProperty(String)}
 * and {@link #setProperty(String, Object)}, but has some synchronization cost for
 * {@link #addProperty(String, Object)} if the object to add is not a String or the key already exists.
 * <p>
 * The methods from AbstractConfiguration related to listeners and event generation are overridden
 * so that adding/deleting listeners and firing events are no longer synchronized.
 * Also, it catches Throwable when it invokes the listeners, making
 * it more robust.
 * <p>
 * This configuration does not allow null as key or value and will throw NullPointerException
 * when trying to add or set properties with empty key or value.
 *
 * @author awang
 *
 */
public class ConcurrentMapConfiguration extends AbstractConfiguration {
    protected Map<String,Object> map;
    private Collection<ConfigurationListener> listeners = new CopyOnWriteArrayList<ConfigurationListener>();
    private Collection<ConfigurationErrorListener> errorListeners = new CopyOnWriteArrayList<ConfigurationErrorListener>();
    private static final Logger logger = LoggerFactory.getLogger(ConcurrentMapConfiguration.class);
    private static final int NUM_LOCKS = 32;
    private ReentrantLock[] locks = new ReentrantLock[NUM_LOCKS];

    /**
     * System property to disable delimiter parsing Apache Commons configurations
     */
    public static final String DISABLE_DELIMITER_PARSING = "archaius.configuration.disableDelimiterParsing";

    /**
     * Create an instance with an empty map.
     */
    public ConcurrentMapConfiguration() {
        map = new ConcurrentHashMap<String,Object>();
        for (int i = 0; i < NUM_LOCKS; i++) {
            locks[i] = new ReentrantLock();
        }
        String disableDelimiterParsing = System.getProperty(DISABLE_DELIMITER_PARSING, "false");
        super.setDelimiterParsingDisabled(Boolean.valueOf(disableDelimiterParsing));
    }

    public ConcurrentMapConfiguration(Map<String, Object> mapToCopy) {
        this();
        map = new ConcurrentHashMap<String, Object>(mapToCopy);
    }

    /**
     * Create an instance by copying the properties from an existing Configuration.
     * Future changes to the Configuration passed in will not be reflected in this
     * object.
     *
     * @param config Configuration to be copied
     */
    public ConcurrentMapConfiguration(Configuration config) {
        this();
        for (Iterator i = config.getKeys(); i.hasNext();) {
            String name = (String) i.next();
            Object value = config.getProperty(name);
            map.put(name, value);
        }
    }

    public Object getProperty(String key)
    {
        return map.get(key);
    }

    protected void addPropertyDirect(String key, Object value)
    {
        ReentrantLock lock = locks[Math.abs(key.hashCode()) % NUM_LOCKS];
        lock.lock();
        try {
            Object previousValue = map.putIfAbsent(key, value);
            if (previousValue == null) {
                return;
            }
            if (previousValue instanceof List)
            {
                // the value is added to the existing list
                ((List) previousValue).add(value);
            }
            else
            {
                // the previous value is replaced by a list containing the previous value and the new value
                List<Object> list = new CopyOnWriteArrayList<Object>();
                list.add(previousValue);
                list.add(value);
                map.put(key, list);
            }
        } finally {
            lock.unlock();
        }
    }

    public boolean isEmpty()
    {
        return map.isEmpty();
    }

    public boolean containsKey(String key)
    {
        return map.containsKey(key);
    }

    protected void clearPropertyDirect(String key)
    {
        map.remove(key);
    }

    public Iterator getKeys()
    {
        return map.keySet().iterator();
    }

    /**
     * Adds the specified value for the given property. This method supports
     * single values and containers (e.g. collections or arrays) as well. In the
     * latter case, {@link #addPropertyDirect(String, Object)} will be called for each
     * element.
     *
     * @param key the property key
     * @param value the value object
     * @param delimiter the list delimiter character
     */
    private void addPropertyValues(String key, Object value, char delimiter)
    {
        Iterator it = PropertyConverter.toIterator(value, delimiter);
        while (it.hasNext())
        {
            addPropertyDirect(key, it.next());
        }
    }

    public void addProperty(String key, Object value) throws ValidationException
    {
        if (value == null) {
            throw new NullPointerException("Value for property " + key + " is null");
        }
        fireEvent(EVENT_ADD_PROPERTY, key, value, true);
        addPropertyImpl(key, value);
        fireEvent(EVENT_ADD_PROPERTY, key, value, false);
    }

    protected void addPropertyImpl(String key, Object value) {
        Object previousValue = null;
        if (isDelimiterParsingDisabled() ||
                ((value instanceof String) && ((String) value).indexOf(getListDelimiter()) < 0)) {
            previousValue = map.putIfAbsent(key, value);
            if (previousValue != null) {
                addPropertyValues(key, value,
                        isDelimiterParsingDisabled() ? '\0'
                                : getListDelimiter());
            }
        } else {
            addPropertyValues(key, value,
                    isDelimiterParsingDisabled() ? '\0'
                            : getListDelimiter());

        }
    }

    /**
     * Override the same method in {@link AbstractConfiguration} to simplify the logic
     * to avoid multiple events being generated. It calls {@link #clearPropertyDirect(String)}
     * followed by logic to add the property including calling {@link #addPropertyDirect(String, Object)}.
     */
    @Override
    public void setProperty(String key, Object value) throws ValidationException
    {
        if (value == null) {
            throw new NullPointerException("Value for property " + key + " is null");
        }
        fireEvent(EVENT_SET_PROPERTY, key, value, true);
        setPropertyImpl(key, value);
        fireEvent(EVENT_SET_PROPERTY, key, value, false);
    }

    protected void setPropertyImpl(String key, Object value) {
        if (isDelimiterParsingDisabled()) {
            map.put(key, value);
        } else if ((value instanceof String) && ((String) value).indexOf(getListDelimiter()) < 0) {
            map.put(key, value);
        } else {
            Iterator it = PropertyConverter.toIterator(value, getListDelimiter());
            List<Object> list = new CopyOnWriteArrayList<Object>();
            while (it.hasNext())
            {
                list.add(it.next());
            }
            if (list.size() == 1) {
                map.put(key, list.get(0));
            } else {
                map.put(key, list);
            }
        }
    }

    /**
     * Load properties into the configuration. This method iterates through
     * the entries of the properties and call {@link #setProperty(String, Object)} for
     * non-null key/value.
     */
    public void loadProperties(Properties props) {
        for (Map.Entry<Object, Object> entry: props.entrySet()) {
            String key = (String) entry.getKey();
            Object value = entry.getValue();
            if (key != null && value != null) {
                setProperty(key, value);
            }
        }
    }

    /**
     * Copy properties of a configuration into this configuration. This method simply
     * iterates the keys of the configuration to be copied and call {@link #setProperty(String, Object)}
     * for non-null key/value.
     */
    @Override
    public void copy(Configuration c)
    {
        if (c != null)
        {
            for (Iterator it = c.getKeys(); it.hasNext();)
            {
                String key = (String) it.next();
                Object value = c.getProperty(key);
                if (key != null && value != null) {
                    setProperty(key, value);
                }
            }
        }
    }

    /**
     * Clear the map and fire corresonding events.
     */
    @Override
    public void clear()
    {
        fireEvent(EVENT_CLEAR, null, null, true);
        map.clear();
        fireEvent(EVENT_CLEAR, null, null, false);
    }

    /**
     * Utility method to get a Properties object from this Configuration
     */
    public Properties getProperties() {
        Properties p = new Properties();
        for (Iterator i = getKeys(); i.hasNext();) {
            String name = (String) i.next();
            String value = getString(name);
            p.put(name, value);
        }
        return p;
    }

    /**
     * Creates an event and calls {@link ConfigurationListener#configurationChanged(ConfigurationEvent)}
     * for all listeners while catching Throwable.
     */
    @Override
    protected void fireEvent(int type, String propName, Object propValue, boolean beforeUpdate) {
        if (listeners == null || listeners.size() == 0) {
            return;
        }
        ConfigurationEvent event = createEvent(type, propName, propValue, beforeUpdate);
        for (ConfigurationListener l: listeners)
        {
            try {
                l.configurationChanged(event);
            } catch (ValidationException e) {
                if (beforeUpdate) {
                    throw e;
                } else {
                    logger.error("Unexpected exception", e);
                }
            } catch (Throwable e) {
                logger.error("Error firing configuration event", e);
            }
        }
    }

    @Override
    public void addConfigurationListener(ConfigurationListener l) {
        if (!listeners.contains(l)) {
            listeners.add(l);
        }
    }

    @Override
    public void addErrorListener(ConfigurationErrorListener l) {
        if (!errorListeners.contains(l)) {
            errorListeners.add(l);
        }
    }

    @Override
    public void clearConfigurationListeners() {
        listeners.clear();
    }

    @Override
    public void clearErrorListeners() {
        errorListeners.clear();
    }

    @Override
    public Collection<ConfigurationListener> getConfigurationListeners() {
        return Collections.unmodifiableCollection(listeners);
    }

    @Override
    public Collection<ConfigurationErrorListener> getErrorListeners() {
        return Collections.unmodifiableCollection(errorListeners);
    }

    @Override
    public boolean removeConfigurationListener(ConfigurationListener l) {
        return listeners.remove(l);
    }

    @Override
    public boolean removeErrorListener(ConfigurationErrorListener l) {
        return errorListeners.remove(l);
    }

    /**
     * Creates an error event and calls {@link ConfigurationErrorListener#configurationError(ConfigurationErrorEvent)}
     * for all listeners while catching Throwable.
     */
    @Override
    protected void fireError(int type, String propName, Object propValue, Throwable ex)
    {
        if (errorListeners == null || errorListeners.size() == 0) {
            return;
        }

        ConfigurationErrorEvent event = createErrorEvent(type, propName, propValue, ex);
        for (ConfigurationErrorListener l: errorListeners) {
            try {
                l.configurationError(event);
            } catch (Throwable e) {
                logger.error("Error firing configuration error event", e);
            }
        }
     }

}


/**
 * 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.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.CopyOnWriteArrayList;

import org.apache.commons.configuration.Configuration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.netflix.config.PollListener.EventType;

/**
 * This class is responsible for scheduling the periodical polling of a configuration source and applying the
 * polling result to a Configuration.
 *

 * A subclass should supply the specific scheduling logic in {@link #schedule(Runnable)} and {@link #stop()}.
 *
 * @author awang
 *
 */
public abstract class AbstractPollingScheduler {
    private volatile boolean ignoreDeletesFromSource;
    private List<PollListener> listeners = new CopyOnWriteArrayList<PollListener>();
    private volatile Object checkPoint;
    private static Logger log = LoggerFactory.getLogger(AbstractPollingScheduler.class);
    private DynamicPropertyUpdater propertyUpdater = new DynamicPropertyUpdater();

    /**
     * @param ignoreDeletesFromSource true if deletes happened in the configuration source should be ignored
     *                  by the Configuration. Warning: If both {@link PollResult#isIncremental()}
     *                  and this parameter are false, any property in the configuration that is missing in the
     *                  polled result will be deleted once the PollResult is applied.
     *
     */
    public AbstractPollingScheduler(boolean ignoreDeletesFromSource) {
        this.ignoreDeletesFromSource = ignoreDeletesFromSource;
    }

    /**
     * Create an instance where ignoreDeletesFromSource is set to false.
     *
     * @see #AbstractPollingScheduler(boolean)
     */
    public AbstractPollingScheduler() {
        this.ignoreDeletesFromSource = false;
    }

    /**
     * Do an initial poll from the source and apply the result to the configuration.
     *
     * @param source source of the configuration
     * @param config Configuration to apply the polling result
     * @throws RuntimeException if any error occurs in polling the configuration source
     */
    protected synchronized void initialLoad(final PolledConfigurationSource source, final Configuration config) {
        PollResult result = null;
        try {
            result = source.poll(true, null);
            checkPoint = result.getCheckPoint();
            fireEvent(EventType.POLL_SUCCESS, result, null);
        } catch (Throwable e) {
            throw new RuntimeException("Unable to load Properties source from " + source, e);
        }
        try {
            populateProperties(result, config);
        } catch (Throwable e) {
            throw new RuntimeException("Unable to load Properties", e);
        }
    }

    /**
     * Apply the polled result to the configuration.
     * If the polled result is full result from source, each property in the result is either added to set
     * to the configuration, and any property that is in the configuration but not in the result is deleted if ignoreDeletesFromSource
     * is false. If the polled result is incremental, properties added and changed in the partial result
     * are set with the configuration, and deleted properties are deleted form configuration if ignoreDeletesFromSource
     * is false.
     *
     * @param result Polled result from source
     */
    protected void populateProperties(final PollResult result, final Configuration config) {
        if (result == null || !result.hasChanges()) {
            return;
        }
        if (!result.isIncremental()) {
            Map<String, Object> props = result.getComplete();
            if (props == null) {
                return;
            }
            for (Entry<String, Object> entry: props.entrySet()) {
                propertyUpdater.addOrChangeProperty(entry.getKey(), entry.getValue(), config);
            }
            HashSet<String> existingKeys = new HashSet<String>();
            for (Iterator<String> i = config.getKeys(); i.hasNext();) {
                existingKeys.add(i.next());
            }
            if (!ignoreDeletesFromSource) {
                for (String key: existingKeys) {
                    if (!props.containsKey(key)) {
                        propertyUpdater.deleteProperty(key, config);
                    }
                }
            }
        } else {
            Map<String, Object> props = result.getAdded();
            if (props != null) {
                for (Entry<String, Object> entry: props.entrySet()) {
                    propertyUpdater.addOrChangeProperty(entry.getKey(), entry.getValue(), config);
                }
            }
            props = result.getChanged();
            if (props != null) {
                for (Entry<String, Object> entry: props.entrySet()) {
                    propertyUpdater.addOrChangeProperty(entry.getKey(), entry.getValue(), config);
                }
            }
            if (!ignoreDeletesFromSource) {
                props = result.getDeleted();
                if (props != null) {
                    for (String name: props.keySet()) {
                        propertyUpdater.deleteProperty(name, config);
                    }
                }
            }
        }
    }

    /**
     * Gets the runnable to be scheduled. The implementation does the following
     *
Gets the next check point
     *
call source.poll(fase, checkpoint)
     *
fire event for poll listeners
     *
If success, update the configuration with the polled result
     *
     * @return Runnable to be scheduled in {@link #schedule(Runnable)}
     */
    protected Runnable getPollingRunnable(final PolledConfigurationSource source, final Configuration config) {
        return new Runnable() {
            public void run() {
                log.debug("Polling started");
                PollResult result = null;
                try {
                    result = source.poll(false, getNextCheckPoint(checkPoint));
                    checkPoint = result.getCheckPoint();
                    fireEvent(EventType.POLL_SUCCESS, result, null);
                } catch (Throwable e) {
                    log.error("Error getting result from polling source", e);
                    fireEvent(EventType.POLL_FAILURE, null, e);
                    return;
                }
                try {
                    populateProperties(result, config);
                } catch (Throwable e) {
                    log.error("Error occured applying properties", e);
                }
            }

        };
    }

    private void fireEvent(PollListener.EventType eventType, PollResult result, Throwable e) {
        for (PollListener l: listeners) {
            try {
                l.handleEvent(eventType, result, e);
            } catch(Throwable ex) {
                log.error("Error in invoking listener", ex);
            }
        }
    }

    /**
     * Initiate the first poll of the configuration source and schedule the runnable. This may start a new thread or
     * thread pool depending on the implementation of {@link #schedule(Runnable)}.
     *
     * @param source Configuration source being polled
     * @param config Configuration where the properties will be updated
     * @throws RuntimeException if any error occurs in the initial polling
     */
    public void startPolling(final PolledConfigurationSource source, final Configuration config) {
        initialLoad(source, config);
        Runnable r = getPollingRunnable(source, config);
        schedule(r);
    }

    /**
     * Add the PollLisetner
     *
     * @param l
     */
    public void addPollListener(PollListener l) {
        if (l!= null) {
            listeners.add(l);
        }
    }

    public void removePollListener(PollListener l) {
        if (l != null) {
            listeners.remove(l);
        }
    }

    /**
     * Get the check point used in next {@link PolledConfigurationSource#poll(boolean, Object)}.
     * The check point can be used by the {@link PolledConfigurationSource} to determine
     * the set of records to return. For example, a check point can be a time stamp and
     * the {@link PolledConfigurationSource} can return the records modified since the time stamp.
     * This method is called before the poll. The
     * default implementation returns the check point received from last poll.
     *
     * @param lastCheckpoint checkPoint from last {@link PollResult#getCheckPoint()}
     * @return the check point to be used for the next poll
     */
    protected Object getNextCheckPoint(Object lastCheckpoint) {
        return lastCheckpoint;
    }

    /**
     * Schedule the runnable for polling the configuration source
     *
     * @param pollingRunnable The runnable to be scheduled.
     */
    protected abstract void schedule(Runnable pollingRunnable);

    /**
     * Stop the scheduler
     */
    public abstract void stop();

    /**
     * @return if the scheduler ignores deletes from source
     */
    public final boolean isIgnoreDeletesFromSource() {
        return ignoreDeletesFromSource;
    }

    /**
     * Set if the scheduler should ignore deletes from source when applying property changes
     */
    public final void setIgnoreDeletesFromSource(boolean ignoreDeletesFromSource) {
        this.ignoreDeletesFromSource = ignoreDeletesFromSource;
    }

}


/**
 * 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 org.apache.commons.configuration.AbstractConfiguration;
import org.apache.commons.configuration.CombinedConfiguration;
import org.apache.commons.configuration.HierarchicalConfiguration;
import org.apache.commons.configuration.event.ConfigurationEvent;
import org.apache.commons.configuration.event.ConfigurationListener;

/**
 *
An ExpandedConfigurationListenerAdapter wraps an instance
 * of {@link PropertyListener}.  When it receives the
 * property modification notification from the Apache Configuration,
 * it translates the {@link ConfigurationEvent} into the corresponding
 * events for {@link PropertyListener}.
 *

 * It also has the capability to pause the event delivery through the {@link #setPauseListener(boolean)} API.
 *
 This class is used as an adapter to attach a {@link PropertyListener} to a Configuration so that
 * methods in the {@link PropertyListener} will be called when there is a change in the configuration.
 *
 */
public class ExpandedConfigurationListenerAdapter implements ConfigurationListener
{
    /** The wrapped PropertyListener. */
    private PropertyListener expandedListener;

    static volatile boolean pauseListener;

    public static boolean isListenerPaused() {
        return pauseListener;
    }

    public static void setPauseListener(boolean pauseListener) {
        ExpandedConfigurationListenerAdapter.pauseListener = pauseListener;
    }

    /**
     *
Create a new ExpandedConfigurationListenerAdapter
     * that wraps the provided
     * {@link PropertyListener}.

     *
     *
     * @param listener to
     *        wrap.
     * @throws NullPointerException if the configuration or listener is
     *         null.
     */
    public ExpandedConfigurationListenerAdapter(PropertyListener listener) {
        if (listener == null) {
            throw new NullPointerException("The listener cannot be null.");
        }
        this.expandedListener = listener;
    }

    /**
     * Returns the wrapped PropertyListener.
     *
     * @return the wrapped PropertyListener.
     */
    public PropertyListener getListener() {
        return expandedListener;
    }

    /**
     * {@inheritDoc}
     * @see ConfigurationListener#configurationChanged(ConfigurationEvent)
     */
    @SuppressWarnings("unchecked")
    @Override
    public void configurationChanged(final ConfigurationEvent event) {

        if (pauseListener){
            return;
        }

        // Grab the event information. Some values may be null.
        final Object source = event.getSource();
        final String name = event.getPropertyName();
        final Object value = event.getPropertyValue();
        final boolean beforeUpdate = event.isBeforeUpdate();

        // Handle the different types.
        switch (event.getType()) {

        // Key identifies node where the Collection of nodes was added, or
        // null if it was at the root.
        case HierarchicalConfiguration.EVENT_ADD_NODES:
        case ConcurrentCompositeConfiguration.EVENT_CONFIGURATION_SOURCE_CHANGED:
            expandedListener.configSourceLoaded(source);
            break;

        // Key identifies the individual property that was added.
        case AbstractConfiguration.EVENT_ADD_PROPERTY:
            expandedListener.addProperty(source, name, value, beforeUpdate);
            break;

        // Entire configuration was cleared.
        case AbstractConfiguration.EVENT_CLEAR:
            expandedListener.clear(source, beforeUpdate);
            break;

        // Key identifies the single property that was cleared.
        case AbstractConfiguration.EVENT_CLEAR_PROPERTY:
            expandedListener.clearProperty(source, name, value, beforeUpdate);
            break;

        // Key identifies the nodes that were removed, along with all its
        // children. Value after update is the List of nodes that were
        // cleared. Before the update, the value is null.
        case HierarchicalConfiguration.EVENT_CLEAR_TREE:
            // Ignore this. We rewrote the clearTree() method below.
            break;

        // The current property set is invalid.
        case CombinedConfiguration.EVENT_COMBINED_INVALIDATE:
            //listener.configSourceLoaded(source);
            break;

        // Key identifies the property that was read.
        case AbstractConfiguration.EVENT_READ_PROPERTY:
            break;

        // Key identifies the property that was set.
        case AbstractConfiguration.EVENT_SET_PROPERTY:
            expandedListener.setProperty(source, name, value, beforeUpdate);
            break;

        default:
            break;
        }
    }

    /**
     * {@inheritDoc}
     * @see Object#equals(Object)
     */
    @Override
    public boolean equals(final Object obj) {
        if (this == obj) return true;
        if (obj instanceof ExpandedConfigurationListenerAdapter ) {
            final ExpandedConfigurationListenerAdapter that = (ExpandedConfigurationListenerAdapter)obj;
            return this.expandedListener.equals(that.expandedListener);
        }
        return false;
    }

    /**
     * {@inheritDoc}
     * @see Object#hashCode()
     */
    @Override
    public int hashCode() {
        return expandedListener.hashCode();
    }
}


/**
 * 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.Map;

/**
 * This class represents the result of a callback from the WatchedConfigurationSource. The result may be the complete
 * content of the configuration source - or an incremental one.
 *
 * @author cfregly
 * @author awang {@link PollResult)
 */
public class WatchedUpdateResult {
    protected final Map<String, Object> complete, added, changed, deleted;
    protected final boolean incremental;

    /**
     * Create a full result that represents the complete content of the configuration source.
     *
     * @param complete
     *            map that contains all the properties
     */
    public static WatchedUpdateResult createFull(Map<String, Object> complete) {
        return new WatchedUpdateResult(complete);
    }

    /**
     * Create a result that represents incremental changes from the configuration source.
     *
     * @param added
     *            properties added
     * @param changed
     *            properties changed
     * @param deleted
     *            properties deleted, in which case the value in the map will be ignored
     */
    public static WatchedUpdateResult createIncremental(Map<String, Object> added, Map<String, Object> changed,
            Map<String, Object> deleted) {
        return new WatchedUpdateResult(added, changed, deleted);
    }

    /**
     * Indicate whether this result has any content. If the result is incremental, this is true if there is any any
     * added, changed or deleted properties. If the result is complete, this is true if {@link #getComplete()} is null.
     */
     public boolean hasChanges() {
        if (incremental) {
            return (added != null && added.size() > 0) || (changed != null && changed.size() > 0)
                    || (deleted != null && deleted.size() > 0);
        } else {
            return complete != null;
        }
    }

    /**
     * Get complete content from configuration source. null if the result is incremental.
     *
     */
    public final Map<String, Object> getComplete() {
        return complete;
    }

    /**
     * @return the added properties in the configuration source as a map
     */
    public final Map<String, Object> getAdded() {
        return added;
    }

    /**
     * @return the changed properties in the configuration source as a map
     */
    public final Map<String, Object> getChanged() {
        return changed;
    }

    /**
     * @return the deleted properties in the configuration source as a map
     */
    public final Map<String, Object> getDeleted() {
        return deleted;
    }

    /**
     * @return whether the result is incremental. false if it is the complete content of the configuration source.
     */
    public final boolean isIncremental() {
        return incremental;
    }

    WatchedUpdateResult(Map<String, Object> complete) {
        this.complete = complete;
        this.added = null;
        this.changed = null;
        this.deleted = null;
        this.incremental = false;
    }

    WatchedUpdateResult(Map<String, Object> added, Map<String, Object> changed, Map<String, Object> deleted) {
        this.complete = null;
        this.added = added;
        this.changed = changed;
        this.deleted = deleted;
        this.incremental = true;
    }
}


/**
 * 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.

 * 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 {

    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 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<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 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);
    }
    /**
     * 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
     * null)
     */
    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 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<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 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<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:
     *

     *
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 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;
    }
}