package com.redhat.ceylon.common.config;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
/**
* Class to hold, retrieve and set Ceylon configuration values (options).
* This is basically a Map from key strings to value strings.
* Ceylon options are grouped in sections and which can be part of
* yet other sections. Even though this results in a hierarchical
* option "tree" the option names are "fully qualified" to keep the
* API as simple as possible.
*
* This means that a configuration section like this:
*
* [testsection]
* option1=foo
* option2=bar
*
* [testsection.subsection]
* option1=baz
*
* Is actually represented as the following 3 options:
*
* testsection.option1=foo
* testsection.option2=bar
* testsection.subsection.option1=baz
*
* Several static helper methods exists for easy access to the default
* Ceylon configuration.
*
* @author Tako Schotanus (tako@ceylon-lang.org)
*/
public class CeylonConfig {
private HashMap<String, String[]> options;
private HashMap<String, HashSet<String>> sectionNames;
private HashMap<String, HashSet<String>> optionNames;
private static final ThreadLocal<CeylonConfig> localInstance = new InheritableThreadLocal<CeylonConfig>();
/**
* Retrieves the default configuration for the current thread.
* @return Default CeylonConfig object
*/
public static CeylonConfig get() {
CeylonConfig instance = localInstance.get();
if (instance == null) {
instance = createFromLocalDir(new File("."));
localInstance.set(instance);
}
return instance;
}
/**
* Set or override the default configuration
* @param config The CelyonConfig object to use as the default
*/
public static CeylonConfig set(CeylonConfig config) {
CeylonConfig old = localInstance.get();
localInstance.set(config);
return old;
}
/**
* Retrieve the given option value from the default configuration
* @param key The name of the option to retrieve
* @return The value of the option or "null" if it wasn't found
*/
public static String get(String key) {
return get().getOption(key);
}
/**
* Retrieve the given option value from the default configuration
* @param key The name of the option to retrieve
* @param defaultValue The default value to use if the option wasn't found
* @return The value of the option or the default value
*/
public static String get(String key, String defaultValue) {
return get().getOption(key, defaultValue);
}
/**
* Returns a newly created default configuration (see ConfigParser.loadDefaultConfig())
* @param localDir The directory to start looking for local configuration files
* @return Default CeylonConfig object
*/
public static CeylonConfig createFromLocalDir(File localDir) {
return CeylonConfigFinder.loadDefaultConfig(localDir);
}
public CeylonConfig() {
options = new LinkedHashMap<String, String[]>();
sectionNames = new LinkedHashMap<String, HashSet<String>>();
sectionNames.put("", new LinkedHashSet<String>());
optionNames = new LinkedHashMap<String, HashSet<String>>();
}
static class Key {
private String subsectionName;
private String optionName;
private String sectionName;
private String parentSectionName;
public String getSubsectionName() {
return subsectionName;
}
public String getOptionName() {
return optionName;
}
public String getSectionName() {
return sectionName;
}
public String getParentSectionName() {
return parentSectionName;
}
public Key(String key) {
if (key == null) {
throw new IllegalArgumentException("Illegal key");
}
String[] parts = key.split("\\.");
if (parts.length < 2) {
throw new IllegalArgumentException("Illegal key '" + key + "', needs a section name");
}
subsectionName = parts[parts.length - 2];
optionName = parts[parts.length - 1];
parentSectionName = "";
if (parts.length > 2) {
for (int i = 0; i < parts.length - 2; i++) {
if (i > 0) {
parentSectionName += '.';
}
parentSectionName += parts[i];
}
sectionName = parentSectionName + '.' + subsectionName;
} else {
sectionName = subsectionName;
}
}
}
private void initLookupKey(String key) {
Key k = new Key(key);
if (!k.getParentSectionName().isEmpty()) {
initLookupKey(k.getParentSectionName() + ".#");
}
HashSet<String> psn = sectionNames.get(k.getParentSectionName());
psn.add(k.getSubsectionName());
HashSet<String> sn = sectionNames.get(k.getSectionName());
if (sn == null) {
sn = new LinkedHashSet<String>();
sectionNames.put(k.getSectionName(), sn);
}
if (!"#".equals(k.getOptionName())) {
HashSet<String> on = optionNames.get(k.getSectionName());
if (on == null) {
on = new LinkedHashSet<String>();
optionNames.put(k.getSectionName(), on);
}
on.add(k.getOptionName());
}
}
/**
* Returns the "size" of the configuration which is defined
* as the number of unique option names
* @return The size of the configuration
*/
public synchronized int size() {
return options.size();
}
/**
* Determines if an option with the given name exists
* @param key The name of the option to check for
* @return Boolean indicating if the option exists
*/
public synchronized boolean isOptionDefined(String key) {
return options.containsKey(key);
}
/**
* Retrieves the array of values defined for the given option
* @param key The name of the option to retrieve
* @return The array of values or "null" if the option didn't exist
*/
public synchronized String[] getOptionValues(String key) {
return options.get(key);
}
/**
* Defines the array of values for the given option, if passing
* "null" the option will be removed from the configuration
* @param key The name of the option to define
* @param values Array of values to use or "null"
*/
public synchronized void setOptionValues(String key, String[] values) {
if (values != null && values.length > 0) {
for (String val : values) {
if (val == null) {
throw new IllegalArgumentException("Option value cannot be null");
}
}
options.put(key, values);
initLookupKey(key);
} else {
removeOption(key);
}
}
/**
* Retrieves a single value for the given option. If more than one
* value exits only the first one is returned
* @param key The name of the option to retrieve
* @return The (first) value of the option or "null" if the option didn't exist
*/
public String getOption(String key) {
String[] result = getOptionValues(key);
return (result != null) ? result[0] : null;
}
/**
* Retrieves a single value for the given option. If more than one
* value exits only the first one is returned
* @param key The name of the option to retrieve
* @param defaultValue The default value to use if the option wasn't found
* @return The (first) value of the option or the default value
*/
public String getOption(String key, String defaultValue) {
String result = getOption(key);
return (result != null) ? result : defaultValue;
}
/**
* Defines a sinlge value for the given option, if passing
* "null" the option will be removed from the configuration
* @param key The name of the option to define
* @param value The value to use or "null"
*/
public void setOption(String key, String value) {
if (value != null) {
setOptionValues(key, new String[] { value });
} else {
removeOption(key);
}
}
/**
* Retrieves a single numeric value for the given option. If more than one
* value exits only the first one is returned
* @param key The name of the option to retrieve
* @return The (first) value of the option or "null" if the option didn't exist
* or if it wasn't a valid number
*/
public Long getNumberOption(String key) {
String result = getOption(key);
if (result != null) {
try {
return Long.valueOf(result);
} catch (NumberFormatException e) {
// Do nothing (logging would spam in case of a user configuration error)
}
}
return null;
}
/**
* Retrieves a single numeric value for the given option. If more than one
* value exits only the first one is returned
* @param key The name of the option to retrieve
* @param defaultValue The default value to use if the option wasn't found
* @return The (first) value of the option, "null" if it wasn't a valid number
* or the default value if the option didn't exist
*/
public long getNumberOption(String key, long defaultValue) {
String result = getOption(key);
if (result != null) {
try {
return Long.parseLong(result);
} catch (NumberFormatException e) {
// Do nothing (logging would spam in case of a user configuration error)
}
}
return defaultValue;
}
/**
* Defines a single numeric value for the given option
* @param key The name of the option to define
* @param value The numeric value to use
*/
public void setNumberOption(String key, long value) {
setOption(key, Long.toString(value));
}
/**
* Retrieves a single boolean value for the given option. If more than one
* value exits only the first one is returned. The strings "true", "on",
* "yes" and "1" are considered to be "true", everything else is "false".
* @param key The name of the option to retrieve
* @return The (first) value of the option or "null" if the option didn't exist
*/
public Boolean getBoolOption(String key) {
String result = getOption(key);
if (result != null) {
return isTrueish(result);
}
return null;
}
/**
* Returns true if the given value is "true", "on", "yes" or "1", false otherwise.
*/
public static boolean isTrueish(String value) {
return value != null
&& ("true".equals(value) || "on".equals(value) || "yes".equals(value) || "1".equals(value));
}
/**
* Returns true if the given value is "false", "off", "no" or "0", false otherwise.
*/
public static boolean isFalsish(String value) {
return value != null
&& ("false".equals(value) || "off".equals(value) || "no".equals(value) || "0".equals(value));
}
/**
* Retrieves a single boolean value for the given option. If more than one
* value exits only the first one is returned. The strings "true", "on",
* "yes" and "1" are considered to be "true", everything else is "false".
* @param key The name of the option to retrieve
* @param defaultValue The default value to use if the option wasn't found
* @return The (first) value of the option or the default value if the option
* didn't exist
*/
public boolean getBoolOption(String key, boolean defaultValue) {
Boolean result = getBoolOption(key);
if (result != null) {
return result.booleanValue();
}
return defaultValue;
}
/**
* Defines a single boolean value for the given option
* @param key The name of the option to define
* @param value The boolean value to use
*/
public void setBoolOption(String key, boolean value) {
setOption(key, Boolean.toString(value));
}
/**
* Removes the given option (does nothing if it doesn't exist)
* @param key The name of the option to remove
*/
public synchronized void removeOption(String key) {
Key k = new Key(key);
options.remove(key);
HashSet<String> on = optionNames.get(k.getSectionName());
if (on != null) {
on.remove(k.getOptionName());
if (on.isEmpty()) {
cleanupSection(k.getSectionName());
}
}
}
private void cleanupSection(String sectionName) {
HashSet<String> on = optionNames.get(sectionName);
if (on != null && on.isEmpty()) {
optionNames.remove(sectionName);
}
HashSet<String> sn = sectionNames.get(sectionName);
if (sn != null && sn.isEmpty()) {
sectionNames.remove(sectionName);
}
if (!optionNames.containsKey(sectionName) && !sectionNames.containsKey(sectionName)) {
Key k = new Key(sectionName + ".dummy");
HashSet<String> psn = sectionNames.get(k.getParentSectionName());
psn.remove(k.getSubsectionName());
if (!k.getParentSectionName().isEmpty()) {
cleanupSection(k.getParentSectionName());
}
}
}
/**
* Determines if a section with the given name exists
* @param section The name of the section to check for
* @return Boolean indicating if the section exists
*/
public synchronized boolean isSectionDefined(String section) {
return sectionNames.containsKey(section);
}
/**
* Removes the given section and all its options
* (does nothing if it doesn't exist)
* @param key The name of the option to remove
*/
public synchronized void removeSection(String section) {
String sectionDot = section + ".";
if (isSectionDefined(section)) {
LinkedHashSet<String> keys = new LinkedHashSet<String>(options.keySet());
for (String key : keys) {
if (key.startsWith(sectionDot)) {
removeOption(key);
}
}
}
}
/**
* Returns the list of all section names, the root section names
* or the sub section names of the given section depending on the
* argument being passed
* @param section Returns the subsections of the section being passed.
* Will return the root section names if being passed an empty string.
* And will return all section names if being passed null.
* @return An array of the requested section names
*/
public synchronized String[] getSectionNames(String section) {
HashSet<String> sn;
if (section != null) {
sn = sectionNames.get(section);
} else {
sn = new LinkedHashSet<String>(sectionNames.keySet());
sn.remove("");
}
String[] res = new String[sn.size()];
return sn.toArray(res);
}
/**
* Returns an array of option names in a given section or the list
* of all option names if "null" is passed
* @param section The name of the section or "null"
* @return An array of option names or "null" if the section doesn't exist
*/
public synchronized String[] getOptionNames(String section) {
if (section == null) {
String[] res = new String[options.keySet().size()];
return options.keySet().toArray(res);
} else {
if (isSectionDefined(section)) {
HashSet<String> on = optionNames.get(section);
if (on != null) {
String[] res = new String[on.size()];
return on.toArray(res);
} else {
// A section can have only subsections and
// no options of its own
return new String[0];
}
} else {
return null;
}
}
}
/**
* Merges the options from the given configuration with the current
* one where duplicate options that exist locally will be overwritten
* by the ones encountered in the given configuration.
* @param local
* @return
*/
public synchronized CeylonConfig merge(CeylonConfig other) {
for (String key : other.getOptionNames(null)) {
String[] values = other.getOptionValues(key);
setOptionValues(key, values);
}
return this;
}
/**
* Returns an exact and safe copy of the current configuration
* @return A clone of the current configuration
*/
public CeylonConfig copy() {
CeylonConfig cfg = new CeylonConfig();
cfg.merge(this);
return cfg;
}
@Override
public String toString() {
try {
ByteArrayOutputStream out = new ByteArrayOutputStream();
ConfigWriter.write(this, out);
return out.toString("UTF-8");
} catch (IOException e) {
return super.toString();
}
}
}