/**
* This file is part of muCommander, http://www.mucommander.com
* Copyright (C) 2002-2016 Maxence Bernard
*
* muCommander is free software; you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* muCommander is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.mucommander.commons.conf;
import java.io.*;
import java.nio.charset.Charset;
import java.util.*;
/**
* Base class for all configuration related tasks.
* <p>
* A <code>Configuration</code> instance's main goal is to act as a configuration data repository.
* Once created, it can be used to {@link #getVariable(String) retrieve}, {@link #removeVariable(String) delete}
* and {@link #setVariable(String,String) set} configuration variables.
* </p>
* <p>
* <h3>Naming conventions</h3>
* Configuration variable names follow the same convention as Java System properties: a serie of strings
* separated by periods. By convention, all but the last string are called configuration sections, while
* the last one is the variable's name. When we refer to a variable's fully qualified name, we're talking
* about the whole period-separated name.<br>
* For example, <code>startup_folder.right.last_folder</code> is interpreted as a variable called
* <code>last_folder</code> contained in a section called <code>right</code>, itself contained in
* another section called <code>startup_folder</code>.<br>
* </p>
* <p>
* <h3>Variable types</h3>
* While the <code>com.mucommander.commons.conf</code> really only handles one type of variables, strings, it offers
* tools to cast them as primitive Java types (int, long, float, double, boolean). This is done through the use
* of the various primitive types' class implementation <code>parseXXX</code> method.<br>
* When a variable hasn't been set but ant attempt is made to cast it, the standard Java default value will
* be returned:
* <ul>
* <li>String: <code>null</code></li>
* <li>Integer: <code>0</code></li>
* <li>Long: <code>0</code></li>
* <li>Float: <code>0</code></li>
* <li>Double: <code>0</code></li>
* <li>Boolean: <code>false</code></li>
* </ul>
* </p>
* <p>
* <h3>Configuration file format</h3>
* By default, configuration data is assumed to be in the standard muCommander file format (described in
* {@link XmlConfigurationReader}). However, application writers can modify that to any format they want
* through the {@link #setReaderFactory(ConfigurationReaderFactory) setReaderFactory} and
* {@link #setWriterFactory(ConfigurationWriterFactory) setWriterFactory} methods.
* </p>
* <p>
* <h3>Configuration data location</h3>
* While <code>Configuration</code> provides read and write methods that accept streams as parameters, it's
* also possible to set the data source once and for all and let the API deal with the details. This can
* be achieved through the {@link #setSource(ConfigurationSource) setSource} method.<br>
* Note that a default implementation, {@link FileConfigurationSource}, is provided. It covers the most
* common case of configuration sources, a local configuration file.<br>
* For application writers who wish to be able to retrieve configuration files through a variety of file systems,
* we suggest creating a source using the <code>com.mucommander.file</code> API.
* </p>
* <p>
* <h3>Monitoring configuration changes</h3>
* Classes that need to monitor the state of the configuration in order, for example, to react to changes
* dynamically rather than wait for an application reboot can implement the {@link ConfigurationListener}
* interface and register themselves through
* {@link #addConfigurationListener(ConfigurationListener) addConfigurationListener}. This guarantees that they
* will receive configuration events whenever a modification occurs.<br>
* Note that LISTENERS are stored as weak references, meaning that application writers must ensure that they keep
* direct references to the listener instances they register if they do not want them to be garbaged collected
* out of existence randomly.
* </p>
* @author Nicolas Rinaudo
*/
public class Configuration {
// - Class variables -----------------------------------------------------------------------------------------------
// -----------------------------------------------------------------------------------------------------------------
/** Used to get access to the configuration source's input and output streams. */
private ConfigurationSource source;
/** Used to create objects that will read from the configuration source. */
private ConfigurationReaderFactory readerFactory;
/** Used to create objects that will write to the configuration source. */
private ConfigurationWriterFactory writerFactory;
/** Holds the content of the configuration file. */
private final ConfigurationSection root = new ConfigurationSection();
/** Contains all registered configuration LISTENERS, stored as weak references. */
private final WeakHashMap<ConfigurationListener, ?> LISTENERS = new WeakHashMap<ConfigurationListener, Object>();
// - Synchronisation locks -----------------------------------------------------------------------------------------
// -----------------------------------------------------------------------------------------------------------------
/** Used to synchronise concurent access of the configuration source. */
private final Object sourceLock = new Object();
/** Used to synchronise concurent access of the reader factory. */
private final Object readerLock = new Object();
/** Used to synchronise concurent access of the writer factory. */
private final Object writerLock = new Object();
// - Initialisation ------------------------------------------------------------------------------------------------
// -----------------------------------------------------------------------------------------------------------------
/**
* Creates a new instance of <code>Configuration</code>.
* <p>
* The resulting instance will use default {@link XmlConfigurationReader readers} and
* {@link XmlConfigurationWriter writers}.
* </p>
* <p>
* Note that until the {@link #setSource(ConfigurationSource) setSource} method has been
* invoked, calls to read or write methods without a stream parameter will fail.
* </p>
*/
public Configuration() {
}
/**
* Creates a new instance of <code>Configuration</code> using the specified source.
* <p>
* The resulting instance will use the default {@link XmlConfigurationReader readers} and
* {@link XmlConfigurationWriter writers}.
* </p>
* @param source where the resulting instance will look for its configuration data.
*/
public Configuration(ConfigurationSource source) {
setSource(source);
}
/**
* Creates a new instance of <code>Configuration</code> using the specified format.
* <p>
* Note that until the {@link #setSource(ConfigurationSource) setSource} method has been
* invoked, calls to read or write methods without a stream parameter will fail.
* </p>
* @param reader factory for configuration readers.
* @param writer factory for configuration writers.
*/
public Configuration(ConfigurationReaderFactory reader, ConfigurationWriterFactory writer) {
setReaderFactory(reader);
setWriterFactory(writer);
}
/**
* Creates a new instance of <code>Configuration</code> using the specified source and format.
* @param source where the resulting instance will look for its configuration data.
* @param reader factory for configuration readers.
* @param writer factory for configuration writers.
*/
public Configuration(ConfigurationSource source, ConfigurationReaderFactory reader, ConfigurationWriterFactory writer) {
setSource(source);
setReaderFactory(reader);
setWriterFactory(writer);
}
// - Configuration source ------------------------------------------------------------------------------------------
// -----------------------------------------------------------------------------------------------------------------
/**
* Sets the source that will be used to read and write configuration information.
* @param s new configuration source.
* @see #getSource()
*/
public void setSource(ConfigurationSource s) {
synchronized(sourceLock) {source = s;}
}
/**
* Returns the current configuration source.
* @return the current configuration source, or <code>null</code> if it hasn't been set.
* @see #setSource(ConfigurationSource)
*/
public ConfigurationSource getSource() {
synchronized(sourceLock) {return source;}
}
// - Reader handling -----------------------------------------------------------------------------------------------
// -----------------------------------------------------------------------------------------------------------------
/**
* Sets the factory that will be used to create {@link ConfigurationReader reader} instances.
* <p>
* In order to reset the configuration to its default reader factory, application writers can call this method
* with a <code>null</code> parameter.
* </p>
* @param f factory that will be used to create reader instances.
* @see #getReaderFactory()
*/
public void setReaderFactory(ConfigurationReaderFactory f) {
synchronized(readerLock) {readerFactory = f;}
}
/**
* Returns the factory that is being used to create {@link ConfigurationReader reader} instances.
* <p>
* By default, this method will return an {@link XmlConfigurationReader XML reader} factory.
* This can be modified by calling {@link #setReaderFactory(ConfigurationReaderFactory) setReaderFactory}.
* </p>
* @return the factory that is being used to create reader instances.
* @see #setReaderFactory(ConfigurationReaderFactory)
*/
public ConfigurationReaderFactory getReaderFactory() {
synchronized(readerLock) {
if(readerFactory == null)
return XmlConfigurationReader.FACTORY;
return readerFactory;
}
}
// - Writer handling -----------------------------------------------------------------------------------------------
// -----------------------------------------------------------------------------------------------------------------
/**
* Sets the factory that will be used to create writer instances.
* <p>
* In order to reset the configuration to its default writer factory, application writers can call
* this method will a <code>null</code> parameter.
* </p>
* @param f factory that will be used to create writer instances.
* @see #getWriterFactory()
*/
public void setWriterFactory(ConfigurationWriterFactory f) {
synchronized(writerLock) {writerFactory = f;}
}
/**
* Returns the factory that is being used to create writer instances.
* <p>
* By default, this method will return an {@link XmlConfigurationWriter} factory. However, this
* can be modified by calling {@link #setWriterFactory(ConfigurationWriterFactory) setWriterFactory}.
* </p>
* @return the factory that is being used to create writer instances.
* @see #setWriterFactory(ConfigurationWriterFactory)
*/
public ConfigurationWriterFactory getWriterFactory() {
synchronized(writerLock) {
if(writerFactory == null)
return XmlConfigurationWriter.FACTORY;
return writerFactory;
}
}
// - Configuration reading -----------------------------------------------------------------------------------------
// -----------------------------------------------------------------------------------------------------------------
/**
* Loads configuration from the specified input stream, using the specified configuration reader.
* @param in where to read the configuration from.
* @param reader reader that will be used to interpret the content of <code>in</code>.
* @throws IOException if an I/O error occurs.
* @throws ConfigurationException if a configuration error occurs.
* @throws ConfigurationFormatException if a syntax error occurs in the configuration data.
* @throws ConfigurationStructureException if the configuration data doesn't describe a valid configuration tree.
* @see #read(InputStream)
* @see #read(ConfigurationReader)
* @see #read()
*/
synchronized void read(Reader in, ConfigurationReader reader) throws IOException, ConfigurationException {
reader.read(in, new ConfigurationLoader(root));
}
/**
* Loads configuration from the specified input stream.
* <p>
* This method will use the configuration reader set by {@link #setReaderFactory(ConfigurationReaderFactory)} if any,
* or an {@link XmlConfigurationReader} instance if not.
* </p>
* @param in where to read the configuration from.
* @throws ConfigurationException if a configuration error occurs.
* @throws ConfigurationFormatException if a syntax error occurs in the configuration data.
* @throws ConfigurationStructureException if the configuration data doesn't describe a valid configuration tree.
* @throws ReaderConfigurationException if the {@link ConfigurationReaderFactory} isn't properly configured.
* @throws IOException if an I/O error occurs.
* @see #read()
* @see #read(ConfigurationReader)
* @see #read(Reader,ConfigurationReader)
* @deprecated Application developers should use {@link #read(Reader)} instead. This method assumes the specified
* {@link InputStream} to be <code>UTF-8</code> encoded.
*/
@Deprecated
public void read(InputStream in) throws ConfigurationException, IOException {
read(new InputStreamReader(in, Charset.forName("utf-8")), getReaderFactory().getReaderInstance());
}
/**
* Loads configuration from the specified reader.
* <p>
* This method will use the configuration reader set by {@link #setReaderFactory(ConfigurationReaderFactory)} if any,
* or an {@link XmlConfigurationReader} instance if not.
* </p>
* @param in where to read the configuration from.
* @throws ConfigurationException if a configuration error occurs.
* @throws ConfigurationFormatException if a syntax error occurs in the configuration data.
* @throws ConfigurationStructureException if the configuration data doesn't describe a valid configuration tree.
* @throws ReaderConfigurationException if the {@link ConfigurationReaderFactory} isn't properly configured.
* @throws IOException if an I/O error occurs.
* @see #read()
* @see #read(ConfigurationReader)
* @see #read(Reader,ConfigurationReader)
*/
public void read(Reader in) throws ConfigurationException, IOException {
read(in, getReaderFactory().getReaderInstance());
}
/**
* Loads configuration using the specified configuration reader.
* <p>
* This method will use the input stream provided by {@link #setSource(ConfigurationSource)} if any, or
* fail otherwise.
* </p>
* @param reader reader that will be used to interpret the content of <code>in</code>.
* @throws IOException if an I/O error occurs.
* @throws ConfigurationException if a configuration error occurs.
* @throws SourceConfigurationException if no {@link ConfigurationSource} has been set.
* @throws ConfigurationFormatException if a syntax error occurs in the configuration data.
* @throws ConfigurationStructureException if the configuration data doesn't describe a valid configuration tree.
* @see #read(InputStream)
* @see #read()
* @see #read(Reader,ConfigurationReader)
*/
public void read(ConfigurationReader reader) throws IOException, ConfigurationException {
Reader in; // Input stream on the configuration source.
ConfigurationSource source; // Configuration source.
in = null;
// Makes sure the configuration source has been properly set.
if((source = getSource()) == null)
throw new SourceConfigurationException("Configuration source hasn't been set.");
// Reads the configuration data.
try {read(in = source.getReader(), reader);}
finally {
if(in != null) {
try {in.close();}
catch(Exception e) {}
}
}
}
/**
* Loads configuration.
* <p>
* If a reader has been specified through {@link #setReaderFactory(ConfigurationReaderFactory)}, it
* will be used to analyse the configuration. Otherwise, an {@link XmlConfigurationReader}
* instance will be used.
* </p>
* <p>
* If a configuration source has been specified through {@link #setSource(ConfigurationSource)}, it will be
* used. Otherwise, this method will fail.
* </p>
* @throws IOException if an I/O error occurs.
* @throws ConfigurationException if a configuration error occurs.
* @throws SourceConfigurationException if no {@link ConfigurationSource} hasn't been set.
* @throws ConfigurationFormatException if a syntax error occurs in the configuration data.
* @throws ConfigurationStructureException if the configuration data doesn't describe a valid configuration tree.
* @throws ReaderConfigurationException if the {@link ConfigurationReaderFactory} isn't properly configured.
* @see #write()
* @see #read(InputStream)
* @see #read(ConfigurationReader)
* @see #read(Reader,ConfigurationReader)
*/
public void read() throws ConfigurationException, IOException {
read(getReaderFactory().getReaderInstance());
}
// - Configuration writing -----------------------------------------------------------------------------------------
// -----------------------------------------------------------------------------------------------------------------
/**
* Writes the configuration to the specified {@link Writer}.
* <p>
* This method will use {@link #getWriterFactory()} to create instances of configuration writer.
* </p>
* @param out where to write the configuration to.
* @throws ConfigurationException if any error occurs.
* @throws ConfigurationFormatException if a syntax error occurs in the configuration data.
* @throws ConfigurationStructureException if the configuration data doesn't describe a valid configuration tree.
* @throws WriterConfigurationException if the {@link ConfigurationWriterFactory} isn't properly configured.
* @see #read(InputStream)
* @see #write()
*/
public void write(Writer out) throws ConfigurationException {
write(getWriterFactory().getWriterInstance(out));
}
/**
* Writes the configuration.
* <p>
* If a configuration source was specified through {@link #setSource(ConfigurationSource)}, it will be used
* to open an output stream. Otherwise, this method will fail.
* </p>
* @throws ConfigurationException if any error occurs.
* @throws SourceConfigurationException if no {@link ConfigurationSource} has been set.
* @throws ConfigurationFormatException if a syntax error occurs in the configuration data.
* @throws ConfigurationStructureException if the configuration data doesn't describe a valid configuration tree.
* @throws IOException if any I/O error occurs.
* @see #read(ConfigurationReader)
* @see #write()
*/
public void write() throws IOException, ConfigurationException {
Writer out; // Where to write the configuration data.
ConfigurationSource source; // Configuration source.
out = null;
// Makes sure the source has been set.
if((source = getSource()) == null)
throw new SourceConfigurationException("No configuration source has been set");
// Writes the configuration data.
try {write(out = source.getWriter());}
finally {
if(out != null) {
try {out.close();}
catch(Exception e) {
// Ignores errors here, nothing we can do about them.
}
}
}
}
/**
* Writes the configuration data to the specified builder.
* @param builder object that will receive configuration building messages.
* @throws ConfigurationException if any error occurs while going through the configuration tree.
*/
public void write(ConfigurationBuilder builder) throws ConfigurationException {
builder.startConfiguration();
build(builder, root);
builder.endConfiguration();
}
// - Configuration building ----------------------------------------------------------------------------------------
// -----------------------------------------------------------------------------------------------------------------
/**
* Recursively explores the specified section and invokes the specified builder's callback methods.
* @param builder object that will receive building events.
* @param root section to explore.
* @throws ConfigurationException if any error occurs.
*/
private synchronized void build(ConfigurationBuilder builder, ConfigurationSection root) throws ConfigurationException {
Iterator<String> enumeration; // Enumeration on the section's variables, then subsections.
String name; // Name of the current variable, then section.
ConfigurationSection section; // Current section.
// Explores the section's variables.
enumeration = root.variableNames();
while(enumeration.hasNext())
builder.addVariable(name = enumeration.next(), root.getVariable(name));
// Explores the section's subsections.
enumeration = root.sectionNames();
while(enumeration.hasNext()) {
name = enumeration.next();
section = root.getSection(name);
// We only go through subsections if contain either variables or subsections of their own.
if(section.hasSections() || section.hasVariables()) {
builder.startSection(name);
build(builder, section);
builder.endSection(name);
}
}
}
// - Variable setting ----------------------------------------------------------------------------------------------
// -----------------------------------------------------------------------------------------------------------------
/**
* Moves the value of <code>fromVar</code> to <code>toVar</code>.
* <p>
* At the end of this call, <code>fromVar</code> will have been deleted. Note that if <code>fromVar</code> doesn't
* exist, but <code>toVar</code> does, <code>toVar</code> will be deleted.
* </p>
* <p>
* This method might trigger as many as two {@link ConfigurationEvent events}:
* <ul>
* <li>One when <code>fromVar</code> is removed.</li>
* <li>One when <code>toVar</code> is set.</li>
* </ul>
* The removal event will always be triggered first.
* </p>
* @param fromVar fully qualified name of the variable to rename.
* @param toVar fully qualified name of the variable that will receive <code>fromVar</code>'s value.
*/
public void renameVariable(String fromVar, String toVar) {
setVariable(toVar, removeVariable(fromVar));
}
/**
* Sets the value of the specified variable.
* <p>
* This method will return <code>false</code> if it didn't modify <code>name</code>'s value. Note that this doesn't
* mean the call failed, but that <code>name</code>'s value was already equal to <code>value</code>.
* </p>
* <p>
* If the value of the specified variable is actually modified, an {@link ConfigurationEvent event} will be passed
* to all LISTENERS.
* </p>
* @param name fully qualified name of the variable to set.
* @param value new value for the variable.
* @return <code>true</code> if this call resulted in a modification of the variable's value,
* <code>false</code> otherwise.
* @see #getVariable(String)
* @see #getVariable(String,String)
*/
public synchronized boolean setVariable(String name, String value) {
ConfigurationExplorer explorer; // Used to navigate to the variable's parent section.
String buffer; // Buffer for the variable's name trimmed of section information.
// Moves to the parent section.
buffer = moveToParent(explorer = new ConfigurationExplorer(root), name, true);
// If the variable's value was actually modified, triggers an event.
if(explorer.getSection().setVariable(buffer, value)) {
triggerEvent(new ConfigurationEvent(this, name, value));
return true;
}
return false;
}
/**
* Sets the value of the specified variable.
* <p>
* This method will return <code>false</code> if it didn't modify <code>name</code>'s value. This, however, is not a
* way of indicating that the call failed: <code>false</code> is only ever returned if the previous value is equal
* to the new value.
* </p>
* <p>
* If the value of the specified variable is actually modified, an {@link ConfigurationEvent event} will be passed
* to all LISTENERS.
* </p>
* @param name fully qualified name of the variable to set.
* @param value new value for the variable.
* @return <code>true</code> if this call resulted in a modification of the variable's value,
* <code>false</code> otherwise.
* @see #getIntegerVariable(String)
* @see #getVariable(String,int)
*/
public boolean setVariable(String name, int value) {
return setVariable(name, ConfigurationSection.getValue(value));
}
/**
* Sets the value of the specified variable.
* <p>
* This method will return <code>false</code> if it didn't modify <code>name</code>'s value. This, however, is not a
* way of indicating that the call failed: <code>false</code> is only ever returned if the previous value is equal
* to the new value.
* </p>
* <p>
* If the value of the specified variable is actually modified, an {@link ConfigurationEvent event} will be passed to all
* LISTENERS.
* </p>
* @param name fully qualified name of the variable to set.
* @param value new value for the variable.
* @param separator string used to separate each element of the list.
* @return <code>true</code> if this call resulted in a modification of the variable's value,
* <code>false</code> otherwise.
* @see #getListVariable(String,String)
* @see #getVariable(String,List,String)
*/
public boolean setVariable(String name, List<String> value, String separator) {
return setVariable(name, ConfigurationSection.getValue(value, separator));
}
/**
* Sets the value of the specified variable.
* <p>
* This method will return <code>false</code> if it didn't modify <code>name</code>'s value. This, however, is not
* a way of indicating that the call failed: <code>false</code> is only ever returned if the previous value is equal
* to the new value.
* </p>
* <p>
* If the value of the specified variable is actually modified, an {@link ConfigurationEvent event} will be passed
* to all LISTENERS.
* </p>
* @param name fully qualified name of the variable to set.
* @param value new value for the variable.
* @return <code>true</code> if this call resulted in a modification of the variable's value,
* <code>false</code> otherwise.
* @see #getFloatVariable(String)
* @see #getVariable(String,float)
*/
public boolean setVariable(String name, float value) {return setVariable(name, ConfigurationSection.getValue(value));}
/**
* Sets the value of the specified variable.
* <p>
* This method will return <code>false</code> if it didn't modify <code>name</code>'s value. This, however, is not a
* way of indicating that the call failed: <code>false</code> is only ever returned if the previous value is equal
* to the new value.
* </p>
* <p>
* If the value of the specified variable is actually modified, an {@link ConfigurationEvent event} will be passed
* to all LISTENERS.
* </p>
* @param name fully qualified name of the variable to set.
* @param value new value for the variable.
* @return <code>true</code> if this call resulted in a modification of the variable's value,
* <code>false</code> otherwise.
* @see #getBooleanVariable(String)
* @see #getVariable(String,boolean)
*/
public boolean setVariable(String name, boolean value) {
return setVariable(name, ConfigurationSection.getValue(value));
}
/**
* Sets the value of the specified variable.
* <p>
* This method will return <code>false</code> if it didn't modify <code>name</code>'s value. This, however, is not a
* way of indicating that the call failed: <code>false</code> is only ever returned if the previous value is equal
* to the new value.
* </p>
* <p>
* If the value of the specified variable is actually modified, an {@link ConfigurationEvent event} will be passed
* to all LISTENERS.
* </p>
* @param name fully qualified name of the variable to set.
* @param value new value for the variable.
* @return <code>true</code> if this call resulted in a modification of the variable's value,
* <code>false</code> otherwise.
* @see #getLongVariable(String)
* @see #getVariable(String,long)
*/
public boolean setVariable(String name, long value) {
return setVariable(name, ConfigurationSection.getValue(value));
}
/**
* Sets the value of the specified variable.
* <p>
* This method will return <code>false</code> if it didn't modify <code>name</code>'s value. This, however, is not a
* way of indicating that the call failed: <code>false</code> is only ever returned if the previous value is equal
* to the new value.
* </p>
* <p>
* If the value of the specified variable is actually modified, an {@link ConfigurationEvent event} will be passed
* to all LISTENERS.
* </p>
* @param name fully qualified name of the variable to set.
* @param value new value for the variable.
* @return <code>true</code> if this call resulted in a modification of the variable's value,
* <code>false</code> otherwise.
* @see #getDoubleVariable(String)
* @see #getVariable(String,double)
*/
public boolean setVariable(String name, double value) {return setVariable(name, ConfigurationSection.getValue(value));}
// - Variable retrieval --------------------------------------------------------------------------------------------
// -----------------------------------------------------------------------------------------------------------------
/**
* Returns the value of the specified variable.
* @param name fully qualified name of the variable whose value should be retrieved.
* @return the variable's value if set, <code>null</code> otherwise.
* @see #setVariable(String,String)
* @see #getVariable(String,String)
*/
public synchronized String getVariable(String name) {
ConfigurationExplorer explorer; // Used to navigate to the variable's parent section.
// If the variable's 'path' doesn't exist, return null.
if((name = moveToParent(explorer = new ConfigurationExplorer(root), name, false)) == null)
return null;
return explorer.getSection().getVariable(name);
}
/**
* Returns the value of the specified variable as a {@link ValueList}.
* @param name fully qualified name of the variable whose value should be retrieved.
* @param separator character used to split the variable's value into a list.
* @return the variable's value if set, <code>null</code> otherwise.
* @see #setVariable(String,List,String)
* @see #getVariable(String,List,String)
*/
public ValueList getListVariable(String name, String separator) {
return ConfigurationSection.getListValue(getVariable(name), separator);
}
/**
* Returns the value of the specified variable as an integer.
* @param name fully qualified name of the variable whose value should be retrieved.
* @return the variable's value if set, <code>0</code> otherwise.
* @throws NumberFormatException if the variable's value cannot be cast to an integer.
* @see #setVariable(String,int)
* @see #getVariable(String,int)
*/
public int getIntegerVariable(String name) {
return ConfigurationSection.getIntegerValue(getVariable(name));
}
/**
* Returns the value of the specified variable as a long.
* @param name fully qualified name of the variable whose value should be retrieved.
* @return the variable's value if set, <code>0</code> otherwise.
* @throws NumberFormatException if the variable's value cannot be cast to a long.
* @see #setVariable(String,long)
* @see #getVariable(String,long)
*/
public long getLongVariable(String name) {
return ConfigurationSection.getLongValue(getVariable(name));
}
/**
* Returns the value of the specified variable as a float.
* @param name fully qualified name of the variable whose value should be retrieved.
* @return the variable's value if set, <code>0</code> otherwise.
* @throws NumberFormatException if the variable's value cannot be cast to a float.
* @see #setVariable(String,float)
* @see #getVariable(String,float)
*/
public float getFloatVariable(String name) {
return ConfigurationSection.getFloatValue(getVariable(name));
}
/**
* Returns the value of the specified variable as a double.
* @param name fully qualified name of the variable whose value should be retrieved.
* @return the variable's value if set, <code>0</code> otherwise.
* @throws NumberFormatException if the variable's value cannot be cast to a double.
* @see #setVariable(String,double)
* @see #getVariable(String,double)
*/
public double getDoubleVariable(String name) {
return ConfigurationSection.getDoubleValue(getVariable(name));
}
/**
* Returns the value of the specified variable as a boolean.
* @param name fully qualified name of the variable whose value should be retrieved.
* @return the variable's value if set, <code>false</code> otherwise.
* @see #setVariable(String,boolean)
* @see #getVariable(String,boolean)
*/
public boolean getBooleanVariable(String name) {
return ConfigurationSection.getBooleanValue(getVariable(name));
}
/**
* Checks whether the specified variable has been set.
* @param name fully qualified name of the variable to check for.
* @return <code>true</code> if the variable is set, <code>false</code> otherwise.
*/
public boolean isVariableSet(String name) {
return getVariable(name) != null;
}
// - Variable removal ----------------------------------------------------------------------------------------------
// -----------------------------------------------------------------------------------------------------------------
/**
* Prunes dead branches from the specified configuration tree.
* @param explorer used to backtrack through the configuration tree.
*/
private void prune(BufferedConfigurationExplorer explorer) {
ConfigurationSection current;
ConfigurationSection parent;
// If we're at the root level, nothing to prune.
if(!explorer.hasSections())
return;
current = explorer.popSection();
// Look for branches to prune until we've either found a non-empty one
// or reached the root of the three.
while(current.isEmpty() && current != root) {
// Gets the current section's parent and prune.
parent = explorer.hasSections() ? explorer.popSection() : root;
parent.removeSection(current);
current = parent;
}
}
/**
* Deletes the specified variable from the configuration.
* <p>
* If the variable was set, a configuration {@link ConfigurationEvent event} will be passed to
* all registered LISTENERS.
* </p>
* @param name name of the variable to remove.
* @return the variable's old value, or <code>null</code> if it wasn't set.
*/
public synchronized String removeVariable(String name) {
BufferedConfigurationExplorer explorer; // Used to navigate to the variable's parent section.
String buffer; // Buffer for the variable's name trimmed of section information.
// If the variable's 'path' doesn't exist, return null.
if((buffer = moveToParent(explorer = new BufferedConfigurationExplorer(root), name, false)) == null)
return null;
// If the variable was actually set, triggers an event.
if((buffer = explorer.getSection().removeVariable(buffer)) != null) {
prune(explorer);
triggerEvent(new ConfigurationEvent(this, name, null));
}
return buffer;
}
/**
* Deletes the specified variable from the configuration.
* <p>
* If the variable was set, a configuration {@link ConfigurationEvent event} will be passed to
* all registered LISTENERS.
* </p>
* @param name name of the variable to remove.
* @param separator character used to split the variable's value into a list.
* @return the variable's old value, or <code>null</code> if it wasn't set.
*/
public ValueList removeListVariable(String name, String separator) {
return ConfigurationSection.getListValue(removeVariable(name), separator);
}
/**
* Deletes the specified variable from the configuration.
* <p>
* If the variable was set, a configuration {@link ConfigurationEvent event} will be passed to
* all registered LISTENERS.
* </p>
* @param name name of the variable to remove.
* @return the variable's old value, or <code>0</code> if it wasn't set.
*/
public int removeIntegerVariable(String name) {
return ConfigurationSection.getIntegerValue(removeVariable(name));
}
/**
* Deletes the specified variable from the configuration.
* <p>
* If the variable was set, a configuration {@link ConfigurationEvent event} will be passed to
* all registered LISTENERS.
* </p>
* @param name name of the variable to remove.
* @return the variable's old value, or <code>0</code> if it wasn't set.
*/
public long removeLongVariable(String name) {
return ConfigurationSection.getLongValue(removeVariable(name));
}
/**
* Deletes the specified variable from the configuration.
* <p>
* If the variable was set, a configuration {@link ConfigurationEvent event} will be passed to
* all registered LISTENERS.
* </p>
* @param name name of the variable to remove.
* @return the variable's old value, or <code>0</code> if it wasn't set.
*/
public float removeFloatVariable(String name) {
return ConfigurationSection.getFloatValue(removeVariable(name));
}
/**
* Deletes the specified variable from the configuration.
* <p>
* If the variable was set, a configuration {@link ConfigurationEvent event} will be passed to
* all registered LISTENERS.
* </p>
* @param name name of the variable to remove.
* @return the variable's old value, or <code>0</code> if it wasn't set.
*/
public double removeDoubleVariable(String name) {
return ConfigurationSection.getDoubleValue(removeVariable(name));
}
/**
* Deletes the specified variable from the configuration.
* <p>
* If the variable was set, a configuration {@link ConfigurationEvent event} will be passed to
* all registered LISTENERS.
* </p>
* @param name name of the variable to remove.
* @return the variable's old value, or <code>false</code> if it wasn't set.
*/
public boolean removeBooleanVariable(String name) {
return ConfigurationSection.getBooleanValue(removeVariable(name));
}
/**
* Remove all variables & sub-sections under the root section
*/
public void clear() {
root.clear();
}
// - Advanced variable retrieval -----------------------------------------------------------------------------------
// -----------------------------------------------------------------------------------------------------------------
/**
* Retrieves the value of the specified variable.
* <p>
* If the variable isn't set, this method will set it to <code>defaultValue</code> before
* returning it. If this happens, a configuration {@link ConfigurationEvent event} will
* be sent to all registered LISTENERS.
* </p>
* @param name name of the variable to retrieve.
* @param defaultValue value to use if <code>name</code> is not set.
* @return the specified variable's value.
* @see #setVariable(String,String)
* @see #getVariable(String)
*/
public synchronized String getVariable(String name, String defaultValue) {
ConfigurationExplorer explorer; // Used to navigate to the variable's parent section.
String value; // Buffer for the variable's value.
String buffer; // Buffer for the variable's name trimmed of section information.
// Navigates to the parent section. We do not have to check for null values here,
// as the section will be created if it doesn't exist.
buffer = moveToParent(explorer = new ConfigurationExplorer(root), name, true);
// If the variable isn't set, set it to defaultValue and triggers an event.
if((value = explorer.getSection().getVariable(buffer)) == null) {
explorer.getSection().setVariable(buffer, defaultValue);
triggerEvent(new ConfigurationEvent(this, name, defaultValue));
return defaultValue;
}
return value;
}
/**
* Retrieves the value of the specified variable as a {@link ValueList}.
* <p>
* If the variable isn't set, this method will set it to <code>defaultValue</code> before
* returning it. If this happens, a configuration {@link ConfigurationEvent event} will
* be sent to all registered LISTENERS.
* </p>
* @param name name of the variable to retrieve.
* @param defaultValue value to use if variable <code>name</code> is not set.
* @param separator separator to use for <code>defaultValue</code> if variable <code>name</code> is not set.
* @return the specified variable's value.
* @see #setVariable(String,List,String)
* @see #getListVariable(String,String)
*/
public ValueList getVariable(String name, List<String> defaultValue, String separator) {
return ConfigurationSection.getListValue(getVariable(name,
ConfigurationSection.getValue(defaultValue, separator)),
separator);
}
/**
* Retrieves the value of the specified variable as an integer.
* <p>
* If the variable isn't set, this method will set it to <code>defaultValue</code> before
* returning it. If this happens, a configuration {@link ConfigurationEvent event} will
* be sent to all registered LISTENERS.
* </p>
* @param name name of the variable to retrieve.
* @param defaultValue value to use if <code>name</code> is not set.
* @return the specified variable's value.
* @throws NumberFormatException if the variable's value cannot be cast to an integer.
* @see #setVariable(String,int)
* @see #getIntegerVariable(String)
*/
public int getVariable(String name, int defaultValue) {
return ConfigurationSection.getIntegerValue(getVariable(name, ConfigurationSection.getValue(defaultValue)));
}
/**
* Retrieves the value of the specified variable as a long.
* <p>
* If the variable isn't set, this method will set it to <code>defaultValue</code> before
* returning it. If this happens, a configuration {@link ConfigurationEvent event} will
* be sent to all registered LISTENERS.
* </p>
* @param name name of the variable to retrieve.
* @param defaultValue value to use if <code>name</code> is not set.
* @return the specified variable's value.
* @throws NumberFormatException if the variable's value cannot be cast to a long.
* @see #setVariable(String,long)
* @see #getLongVariable(String)
*/
public long getVariable(String name, long defaultValue) {
return ConfigurationSection.getLongValue(getVariable(name, ConfigurationSection.getValue(defaultValue)));
}
/**
* Retrieves the value of the specified variable as a float.
* <p>
* If the variable isn't set, this method will set it to <code>defaultValue</code> before
* returning it. If this happens, a configuration {@link ConfigurationEvent event} will
* be sent to all registered LISTENERS.
* </p>
* @param name name of the variable to retrieve.
* @param defaultValue value to use if <code>name</code> is not set.
* @return the specified variable's value.
* @throws NumberFormatException if the variable's value cannot be cast to a float.
* @see #setVariable(String,float)
* @see #getFloatVariable(String)
*/
public float getVariable(String name, float defaultValue) {
return ConfigurationSection.getFloatValue(getVariable(name, ConfigurationSection.getValue(defaultValue)));
}
/**
* Retrieves the value of the specified variable as a boolean.
* <p>
* If the variable isn't set, this method will set it to <code>defaultValue</code> before
* returning it. If this happens, a configuration {@link ConfigurationEvent event} will
* be sent to all registered LISTENERS.
* </p>
* @param name name of the variable to retrieve.
* @param defaultValue value to use if <code>name</code> is not set.
* @return the specified variable's value.
* @see #setVariable(String,boolean)
* @see #getBooleanVariable(String)
*/
public boolean getVariable(String name, boolean defaultValue) {
return ConfigurationSection.getBooleanValue(getVariable(name, ConfigurationSection.getValue(defaultValue)));
}
/**
* Retrieves the value of the specified variable as a double.
* <p>
* If the variable isn't set, this method will set it to <code>defaultValue</code> before
* returning it. If this happens, a configuration {@link ConfigurationEvent event} will
* be sent to all registered LISTENERS.
* </p>
* @param name name of the variable to retrieve.
* @param defaultValue value to use if <code>name</code> is not set.
* @return the specified variable's value.
* @throws NumberFormatException if the variable's value cannot be cast to a double.
* @see #setVariable(String,double)
* @see #getDoubleVariable(String)
*/
public double getVariable(String name, double defaultValue) {
return ConfigurationSection.getDoubleValue(getVariable(name, ConfigurationSection.getValue(defaultValue)));
}
// - Helper methods ------------------------------------------------------------------------------------------------
// -----------------------------------------------------------------------------------------------------------------
/**
* Navigates the specified explorer to the parent section of the specified variable.
* @param root where to start exploring from.
* @param name name of the variable to seek.
* @param create whether or not the path to the variable should be created if it doesn't exist.
* @return the name of the variable trimmed of section information, <code>null</code> if not found.
*/
private String moveToParent(ConfigurationExplorer root, String name, boolean create) {
StringTokenizer parser; // Used to parse the variable's path.
// Goes through each element of the path.
parser = new StringTokenizer(name, ".");
while(parser.hasMoreTokens()) {
// If we've reached the variable's name, return it.
name = parser.nextToken();
if(!parser.hasMoreTokens())
return name;
// If we've reached a dead-end, return null.
if(!root.moveTo(name, create))
return null;
}
return name;
}
// - Configuration listening ---------------------------------------------------------------------------------------
// -----------------------------------------------------------------------------------------------------------------
/**
* Adds the specified object to the list of registered configuration LISTENERS.
* @param listener object to register as a configuration listener.
* @see #removeConfigurationListener(ConfigurationListener)
*/
public void addConfigurationListener(ConfigurationListener listener) {
LISTENERS.put(listener, null);}
/**
* Removes the specified object from the list of registered configuration LISTENERS.
* @param listener object to remove from the list of registered configuration LISTENERS.
* @see #addConfigurationListener(ConfigurationListener)
*/
public void removeConfigurationListener(ConfigurationListener listener) {
LISTENERS.remove(listener);}
/**
* Passes the specified event to all registered configuration LISTENERS.
* @param event event to propagate.
*/
private void triggerEvent(ConfigurationEvent event) {
for(ConfigurationListener listener : LISTENERS.keySet())
listener.configurationChanged(event);
}
// - Misc. ---------------------------------------------------------------------------------------------------------
// -----------------------------------------------------------------------------------------------------------------
/**
* Returns the configuration's root section.
* @return the configuration's root section.
*/
ConfigurationSection getRoot() {return root;}
// - Loading -------------------------------------------------------------------------------------------------------
// -----------------------------------------------------------------------------------------------------------------
/**
* Used to load configuration.
* @author Nicolas Rinaudo
*/
private class ConfigurationLoader implements ConfigurationBuilder {
// - Instance variables ----------------------------------------------------------------------------------------
// -------------------------------------------------------------------------------------------------------------
/** Parents of {@link #currentSection}. */
private Stack<ConfigurationSection> sections;
/** Fully qualified names of {@link #currentSection}. */
private Stack<String> sectionNames;
/** Section that we're currently building. */
private ConfigurationSection currentSection;
// - Initialisation --------------------------------------------------------------------------------------------
// -------------------------------------------------------------------------------------------------------------
/**
* Creates a new configuration loader.
* @param root where to create the configuration in.
*/
public ConfigurationLoader(ConfigurationSection root) {currentSection = root;}
// - Building --------------------------------------------------------------------------------------------------
// -------------------------------------------------------------------------------------------------------------
/**
* Initialises the configuration building.
*/
public void startConfiguration() {
sections = new Stack<ConfigurationSection>();
sectionNames = new Stack<String>();
}
/**
* Ends the configuration building.
* @throws ConfigurationException if not all opened sections have been closed.
*/
public void endConfiguration() throws ConfigurationException {
// Makes sure currentSection is the root section.
if(!sections.empty())
throw new ConfigurationStructureException("Not all sections have been closed.");
sections = null;
sectionNames = null;
}
/**
* Creates a new sub-section to the current section.
* @param name name of the new section.
*/
public void startSection(String name) throws ConfigurationException {
ConfigurationSection buffer;
buffer = currentSection.addSection(name);
sections.push(currentSection);
if(sectionNames.empty())
sectionNames.push(name + '.');
else
sectionNames.push(sectionNames.peek() + name + '.');
currentSection = buffer;
}
/**
* Ends the current section.
* @param name name of the section that's being closed.
* @throws ConfigurationException if we're not closing a legal section.
*/
public void endSection(String name) throws ConfigurationException {
ConfigurationSection buffer;
// Makes sure there is a section to close.
try {
buffer = sections.pop();
sectionNames.pop();
}
catch(EmptyStackException e) {throw new ConfigurationStructureException("Section " + name + " was already closed.");}
// Makes sure we're closing the right section.
if(buffer.getSection(name) != currentSection)
throw new ConfigurationStructureException("Section " + name + " is not the currently opened section.");
currentSection = buffer;
}
/**
* Adds the specified variable to the current section.
* @param name name of the variable.
* @param value value of the variable.
*/
public void addVariable(String name, String value) {
// If the variable's value was modified, trigger an event.
if(currentSection.setVariable(name, value)) {
if(sectionNames.empty())
triggerEvent(new ConfigurationEvent(Configuration.this, name, value));
else
triggerEvent(new ConfigurationEvent(Configuration.this, sectionNames.peek() + name, value));
}
}
}
}