package org.stagemonitor.configuration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.stagemonitor.configuration.source.ConfigurationSource;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.ServiceLoader;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
public class ConfigurationRegistry {
private final Logger logger = LoggerFactory.getLogger(getClass());
private final String updateConfigPasswordKey;
private final List<ConfigurationSource> configurationSources = new CopyOnWriteArrayList<ConfigurationSource>();
private final boolean failOnMissingRequiredValues;
private Map<Class<? extends ConfigurationOptionProvider>, ConfigurationOptionProvider> optionProvidersByClass = new HashMap<Class<? extends ConfigurationOptionProvider>, ConfigurationOptionProvider>();
private Map<String, ConfigurationOption<?>> configurationOptionsByKey = new LinkedHashMap<String, ConfigurationOption<?>>();
private Map<String, List<ConfigurationOption<?>>> configurationOptionsByCategory = new LinkedHashMap<String, List<ConfigurationOption<?>>>();
private ScheduledExecutorService configurationReloader = Executors.newScheduledThreadPool(1, new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
thread.setDaemon(true);
thread.setName("configuration-reloader");
return thread;
}
});
/**
* @param updateConfigPasswordKey the key of the password to update configuration settings.
* The actual password is loaded from the configuration sources. Set to null to disable dynamic updates.
*/
public ConfigurationRegistry(String updateConfigPasswordKey) {
this(ConfigurationOptionProvider.class, updateConfigPasswordKey);
}
/**
* @param optionProviderClass the class that should be used to lookup instances of
* {@link ConfigurationOptionProvider} via {@link ServiceLoader#load(Class)}
*/
public ConfigurationRegistry(Class<? extends ConfigurationOptionProvider> optionProviderClass) {
this(optionProviderClass, null);
}
/**
* @param optionProviderClass the class that should be used to lookup instances of
* {@link ConfigurationOptionProvider} via {@link ServiceLoader#load(Class)}
* @param updateConfigPasswordKey the key of the password to update configuration settings.
* The actual password is loaded from the configuration sources. Set to null to disable dynamic updates.
*/
public ConfigurationRegistry(Class<? extends ConfigurationOptionProvider> optionProviderClass, String updateConfigPasswordKey) {
this(optionProviderClass, Collections.<ConfigurationSource>emptyList(), updateConfigPasswordKey);
}
/**
* @param optionProviderClass the class that should be used to lookup instances of
* {@link ConfigurationOptionProvider} via {@link ServiceLoader#load(Class)}
* @param configSources the configuration sources
* @param updateConfigPasswordKey the key of the password to update configuration settings.
* The actual password is loaded from the configuration sources. Set to null to disable dynamic updates.
*/
public ConfigurationRegistry(Class<? extends ConfigurationOptionProvider> optionProviderClass,
List<ConfigurationSource> configSources, String updateConfigPasswordKey) {
this(ServiceLoader.load(optionProviderClass, ConfigurationRegistry.class.getClassLoader()), configSources, updateConfigPasswordKey);
}
/**
* @param optionProviders the option providers
* @param configSources the configuration sources
* @param updateConfigPasswordKey the key of the password to update configuration settings.
* The actual password is loaded from the configuration sources. Set to null to disable dynamic updates.
*/
public ConfigurationRegistry(Iterable<? extends ConfigurationOptionProvider> optionProviders,
List<ConfigurationSource> configSources, String updateConfigPasswordKey) {
this(optionProviders, configSources, updateConfigPasswordKey, false);
}
/**
* @param optionProviders the option providers
* @param configSources the configuration sources
* @param updateConfigPasswordKey the key of the password to update configuration settings.
* The actual password is loaded from the configuration sources. Set to null to disable dynamic updates.
*/
public ConfigurationRegistry(Iterable<? extends ConfigurationOptionProvider> optionProviders,
List<ConfigurationSource> configSources, String updateConfigPasswordKey, boolean failOnMissingRequiredValues) {
this.updateConfigPasswordKey = updateConfigPasswordKey;
this.failOnMissingRequiredValues = failOnMissingRequiredValues;
configurationSources.addAll(configSources);
registerConfigurationOptions(optionProviders);
}
private void registerConfigurationOptions(Iterable<? extends ConfigurationOptionProvider> optionProviders) {
for (ConfigurationOptionProvider configurationOptionProvider : optionProviders) {
registerOptionProvider(configurationOptionProvider);
}
}
private void registerOptionProvider(ConfigurationOptionProvider configurationOptionProvider) {
optionProvidersByClass.put(configurationOptionProvider.getClass(), configurationOptionProvider);
for (ConfigurationOption<?> configurationOption : configurationOptionProvider.getConfigurationOptions()) {
add(configurationOption);
}
}
/**
* Returns a {@link ConfigurationOptionProvider} whose {@link ConfigurationOption}s are populated
*
* @param configClass the {@link ConfigurationOptionProvider} class
* @param <T> the type
* @return a {@link ConfigurationOptionProvider} whose {@link ConfigurationOption}s are populated
*/
public <T extends ConfigurationOptionProvider> T getConfig(Class<T> configClass) {
final T config = (T) optionProvidersByClass.get(configClass);
if (config != null) {
return config;
} else {
for (Class<? extends ConfigurationOptionProvider> storedConfigClass : optionProvidersByClass.keySet()) {
if (configClass.isAssignableFrom(storedConfigClass)) {
return (T) optionProvidersByClass.get(storedConfigClass);
}
}
return null;
}
}
public Collection<ConfigurationOptionProvider> getConfigurationOptionProviders() {
return Collections.unmodifiableCollection(optionProvidersByClass.values());
}
private void add(final ConfigurationOption<?> configurationOption) {
configurationOption.setConfiguration(this);
configurationOption.setConfigurationSources(configurationSources);
final String key = configurationOption.getKey();
addConfigurationOptionByKey(configurationOption, key);
for (String alternateKey : configurationOption.getAliasKeys()) {
addConfigurationOptionByKey(configurationOption, alternateKey);
}
addConfigurationOptionByCategory(configurationOption.getConfigurationCategory(), configurationOption);
}
private void addConfigurationOptionByKey(ConfigurationOption<?> configurationOption, String key) {
if (configurationOptionsByKey.containsKey(key)) {
throw new IllegalArgumentException(String.format("The configuration key %s is registered twice. Once for %s and once for %s.",
key, configurationOptionsByKey.get(key).getLabel(), configurationOption.getLabel()));
}
configurationOptionsByKey.put(key, configurationOption);
}
private void addConfigurationOptionByCategory(String configurationCategory, final ConfigurationOption<?> configurationOption) {
if (configurationOptionsByCategory.containsKey(configurationCategory)) {
configurationOptionsByCategory.get(configurationCategory).add(configurationOption);
} else {
configurationOptionsByCategory.put(configurationCategory, new ArrayList<ConfigurationOption<?>>() {{
add(configurationOption);
}});
}
}
/**
* Returns all Configuration options grouped by {@link ConfigurationOption#configurationCategory}
*
* @return all Configuration options grouped by {@link ConfigurationOption#configurationCategory}
*/
public Map<String, List<ConfigurationOption<?>>> getConfigurationOptionsByCategory() {
return Collections.unmodifiableMap(configurationOptionsByCategory);
}
/**
* Returns all Configuration options grouped by the {@link ConfigurationOption#key}
*
* @return all Configuration options grouped by the {@link ConfigurationOption#key}
*/
public Map<String, ConfigurationOption<?>> getConfigurationOptionsByKey() {
return Collections.unmodifiableMap(configurationOptionsByKey);
}
/**
* Returns a map with the names of all configuration sources as key and a boolean indicating whether the configuration
* source supports saving as value
*
* @return the names of all configuration sources
*/
public Map<String, Boolean> getNamesOfConfigurationSources() {
final Map<String, Boolean> result = new LinkedHashMap<String, Boolean>();
for (ConfigurationSource configurationSource : configurationSources) {
result.put(configurationSource.getName(), configurationSource.isSavingPossible());
}
return result;
}
/**
* Returns a configuration option by its {@link ConfigurationOption#key}
*
* @param key the configuration key
* @return the configuration option with a specific key
*/
public ConfigurationOption<?> getConfigurationOptionByKey(String key) {
return configurationOptionsByKey.get(key);
}
/**
* Schedules {@link #reloadDynamicConfigurationOptions()} at a fixed rate
*
* @param rate the period between reloads
* @param timeUnit the time unit of rate
*/
public void scheduleReloadAtRate(final long rate, TimeUnit timeUnit) {
configurationReloader.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
logger.debug("Beginning scheduled configuration reload (interval is {} sec)...", rate);
reloadDynamicConfigurationOptions();
logger.debug("Finished scheduled configuration reload");
}
}, rate, rate, timeUnit);
}
/**
* Reloads a specific dynamic configuration option.
*
* @param key the key of the configuration option
*/
public void reload(String key) {
if (configurationOptionsByKey.containsKey(key)) {
configurationOptionsByKey.get(key).reload(false);
}
}
/**
* This method reloads all configuration options - even non {@link ConfigurationOption#dynamic} ones.
* <p/>
* Use this method judiciously, because you have to make sure that no one already read from a non dynamic
* {@link ConfigurationOption} before calling this method.
*/
public void reloadAllConfigurationOptions() {
reload(true);
}
/**
* Reloads all {@link ConfigurationOption}s where {@link ConfigurationOption#dynamic} is true
*/
public void reloadDynamicConfigurationOptions() {
reload(false);
}
private void reload(final boolean reloadNonDynamicValues) {
for (ConfigurationSource configurationSource : configurationSources) {
try {
configurationSource.reload();
} catch (Exception e) {
logger.warn(e.getMessage() + " (this exception is ignored)", e);
}
}
for (ConfigurationOption<?> configurationOption : configurationOptionsByKey.values()) {
configurationOption.reload(reloadNonDynamicValues);
}
}
/**
* Adds a configuration source as first priority to the configuration.
* <p/>
* Don't forget to call {@link #reloadAllConfigurationOptions()} or {@link #reloadDynamicConfigurationOptions()}
* after adding all configuration sources.
*
* @param configurationSource the configuration source to add
*/
public void addConfigurationSource(ConfigurationSource configurationSource) {
addConfigurationSource(configurationSource, true);
}
/**
* Adds a configuration source to the configuration.
* <p/>
* Don't forget to call {@link #reloadAllConfigurationOptions()} or {@link #reloadDynamicConfigurationOptions()}
* after adding all configuration sources.
*
* @param configurationSource the configuration source to add
* @param firstPrio whether the configuration source should be first or last priority
*/
public void addConfigurationSource(ConfigurationSource configurationSource, boolean firstPrio) {
if (configurationSource == null) {
return;
}
if (firstPrio) {
configurationSources.add(0, configurationSource);
} else {
configurationSources.add(configurationSource);
}
}
/**
* Returns <code>true</code>, if the password that is required to {@link #save(String, String, String, String)} settings is
* set (not <code>null</code>), <code>false</code> otherwise
*
* @return <code>true</code>, if the update configuration password is set, <code>false</code> otherwise
*/
public boolean isPasswordSet() {
return getString(updateConfigPasswordKey) != null;
}
/**
* Dynamically updates a configuration key.
* <p/>
* Performs a password check.
*
* @param key the configuration key
* @param value the configuration value
* @param configurationSourceName the {@link ConfigurationSource#getName()}
* of the configuration source the value should be stored to
* @param configurationUpdatePassword the password (must not be null)
* @throws IOException if there was an error saving the key to the source
* @throws IllegalStateException if the update configuration password did not match
* @throws IllegalArgumentException if there was a error processing the configuration key or value or the
* configurationSourceName did not match any of the available configuration
* sources
* @throws UnsupportedOperationException if saving values is not possible with this configuration source
*/
public void save(String key, String value, String configurationSourceName, String configurationUpdatePassword) throws IOException,
IllegalArgumentException, IllegalStateException, UnsupportedOperationException {
assertPasswordCorrect(configurationUpdatePassword);
final ConfigurationOption<?> configurationOption = validateConfigurationOption(key, value);
saveToConfigurationSource(key, value, configurationSourceName, configurationOption);
}
/**
* Validates a password.
*
* @param password the provided password to validate
* @return <code>true</code>, if the password is correct, <code>false</code> otherwise
*/
public boolean isPasswordCorrect(String password) {
final String actualPassword = getString(updateConfigPasswordKey);
return "".equals(actualPassword) || actualPassword != null && actualPassword.equals(password);
}
/**
* Validates a password. If not valid, throws a {@link IllegalStateException}.
*
* @param password the provided password to validate
* @throws IllegalStateException if the password did not match
*/
public void assertPasswordCorrect(String password) {
if (!isPasswordSet()) {
throw new IllegalStateException("'" + updateConfigPasswordKey + "' is not set.");
}
if (!isPasswordCorrect(password)) {
throw new IllegalStateException("Wrong password for '" + updateConfigPasswordKey + "'.");
}
}
/**
* Dynamically updates a configuration key.
* <p/>
* Does not perform a password check.
*
* @param key the configuration key
* @param value the configuration value
* @param configurationSourceName the {@link ConfigurationSource#getName()}
* of the configuration source the value should be stored to
* @throws IOException if there was an error saving the key to the source
* @throws IllegalArgumentException if there was a error processing the configuration key or value or the
* configurationSourceName did not match any of the available configuration
* sources
* @throws UnsupportedOperationException if saving values is not possible with this configuration source
*/
public void save(String key, String value, String configurationSourceName) throws IOException {
final ConfigurationOption<?> configurationOption = validateConfigurationOption(key, value);
saveToConfigurationSource(key, value, configurationSourceName, configurationOption);
}
private ConfigurationOption<?> validateConfigurationOption(String key, String value) throws IllegalArgumentException {
final ConfigurationOption configurationOption = getConfigurationOptionByKey(key);
if (configurationOption != null) {
configurationOption.assertValid(value);
return configurationOption;
} else {
throw new IllegalArgumentException("Config key '" + key + "' does not exist.");
}
}
private void saveToConfigurationSource(String key, String value, String configurationSourceName, ConfigurationOption<?> configurationOption) throws IOException {
for (ConfigurationSource configurationSource : configurationSources) {
if (configurationSourceName != null && configurationSourceName.equals(configurationSource.getName())) {
validateConfigurationSource(configurationSource, configurationOption);
configurationSource.save(key, value);
reload(key);
logger.info("Updated configuration: {}={} ({})", key, value, configurationSourceName);
return;
}
}
throw new IllegalArgumentException("Configuration source '" + configurationSourceName + "' does not exist.");
}
private void validateConfigurationSource(ConfigurationSource configurationSource, ConfigurationOption<?> configurationOption) {
if (!configurationOption.isDynamic() && !configurationSource.isSavingPersistent()) {
throw new IllegalArgumentException("Non dynamic options can't be saved to a transient configuration source.");
}
}
private String getString(String key) {
if (key == null || key.isEmpty()) {
return null;
}
String property = null;
for (ConfigurationSource configurationSource : configurationSources) {
property = configurationSource.getValue(key);
if (property != null) {
break;
}
}
return property;
}
/**
* Shuts down the internal thread pool
*/
public void close() {
configurationReloader.shutdown();
}
boolean isFailOnMissingRequiredValues() {
return failOnMissingRequiredValues;
}
}