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

com.netflix.config.ConcurrentMapConfiguration 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.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.
 * 

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

* 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 ConcurrentHashMap map; private Collection listeners = new CopyOnWriteArrayList(); private Collection errorListeners = new CopyOnWriteArrayList(); 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(); 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 mapToCopy) { this(); map = new ConcurrentHashMap(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 list = new CopyOnWriteArrayList(); 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 list = new CopyOnWriteArrayList(); 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 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 getConfigurationListeners() { return Collections.unmodifiableCollection(listeners); } @Override public Collection 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); } } } }