// This file is part of PleoCommand:
// Interactively control Pleo with psychobiological parameters
//
// Copyright (C) 2010 Oliver Hoffmann - Hoffmann_Oliver@gmx.de
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Boston, USA.
package pleocmd.cfg;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import pleocmd.Log;
import pleocmd.exc.ConfigurationException;
/**
* The global, persistent configuration of the application.
* <p>
* A class which needs persistent configurable data, must implement the
* {@link ConfigurationInterface} and invoke
* {@link #registerConfigurableObject(ConfigurationInterface, Set)} or
* {@link #registerConfigurableObject(ConfigurationInterface, String)} during
* its construction.<br>
* If its a singleton class, there's no need to unregister, otherwise during
* "destruction" (means in Java: at the time a class's instance is no longer
* used) {@link #unregisterConfigurableObject(ConfigurationInterface)} should be
* invoked.<br>
* If there are more than one instances of the class at the same time, you
* should consider creating a singleton class which handles the configuration
* like:<br>
* <code><pre>
* class Foo {
* private final static ConfigString cfg0 = new ConfigString(...);
* private final static ConfigString cfg1 = new ConfigString(...);
* ...
* static {
* // must be *after* declaration of all static fields !!!
* new FooConfig();
* }
* static class FooConfig implements ConfigurationInterface {
* FooConfig() {
* try {
* Configuration.the().registerConfigurableObject(this,
* getClass().getSimpleName());
* } catch (final ConfigurationException e) {
* Log.error(e);
* }
* }
* public Group getSkeleton(final String groupName) {
* return new Group(groupName).add(cfg0).add(cfg1);
* }
* public void configurationAboutToBeChanged() {
* }
* public void configurationChanged(final Group group) {
* }
* public List<Group> configurationWriteback() {
* return Configuration.asList(getSkeleton(getClass().getSimpleName()));
* }
* }
* };
* </pre></code>
*
* @author oliver
*/
public final class Configuration {
private static final File DEFAULT_CONFIG_FILE;
static {
DEFAULT_CONFIG_FILE = new File(System.getProperty("user.home")
+ File.separator + ".pleocommand.cfg");
}
private static Configuration mainConfig;
private final List<Group> groupsUnassigned;
private final Map<String, ConfigurationInterface> groupsRegistered;
private final Set<ConfigurationInterface> configObjects;
public Configuration() {
// No Log call here - it would recursively create a new Configuration!
groupsUnassigned = new ArrayList<Group>();
groupsRegistered = new HashMap<String, ConfigurationInterface>();
configObjects = new HashSet<ConfigurationInterface>();
}
public static synchronized Configuration getMain() {
if (mainConfig == null) {
mainConfig = new Configuration();
try {
mainConfig.readFromDefaultFile();
} catch (final ConfigurationException e) {
Log.error(e);
}
}
return mainConfig;
}
/**
* Registers an external object to this {@link Configuration}.<br>
* For every {@link Group} named in <i>groupNames</i> which appears during
* loading in the configuration file,
* {@link ConfigurationInterface#configurationChanged(Group)} will be
* called.<br>
* During saving, every {@link Group} listed in <i>groupNames</i> will be
* removed, before {@link ConfigurationInterface#configurationWriteback()}
* will be called, so only these {@link Group}s will be written to disk.
*
* @param co
* a configurable object
* @param groupNames
* a {@link Set} of {@link Group} names which will be handled by
* the configurable object if they exist in the
* {@link Configuration}.
* @throws ConfigurationException
* if reading or writing to default file fails
*/
public synchronized void registerConfigurableObject(
final ConfigurationInterface co, final Set<String> groupNames)
throws ConfigurationException {
Log.detail("Register '%s' with '%s' to '%s'", co, groupNames,
super.toString());
if (configObjects.contains(co))
throw new IllegalStateException("Already registered");
for (final String groupName : groupNames)
if (groupsRegistered.containsKey(groupName))
throw new IllegalArgumentException(String.format(
"The group-name '%s' is already registered for '%s'",
groupName, groupsRegistered.get(groupName)));
// register the new object and assign group-names to it
for (final String groupName : groupNames)
groupsRegistered.put(groupName, co);
configObjects.add(co);
// load the external object from the currently unassigned groups
// only keep groups which are not registered by the new external object
//
// concurrent modification is possible if one of co's methods call
// registerConfigurableObject() again, so we have to split into feed and
// keep lists first
final List<Group> groupsKeep = new ArrayList<Group>(
groupsUnassigned.size());
final List<Group> groupsFeed = new ArrayList<Group>(
groupsUnassigned.size());
for (final Group group : groupsUnassigned)
if (groupNames.contains(group.getName()))
groupsFeed.add(group);
else
groupsKeep.add(group);
groupsUnassigned.clear();
groupsUnassigned.addAll(groupsKeep);
co.configurationAboutToBeChanged();
for (final Group group : groupsFeed) {
final Group skelGroup = co.getSkeleton(group.getName());
if (skelGroup == null)
// no skeleton? just feed co with the unassigned group
co.configurationChanged(group);
else {
// we have to copy data from unassigned to skeleton group
skelGroup.assign(group);
co.configurationChanged(skelGroup);
}
}
co.configurationRead();
}
/**
* Registers an external object to this {@link Configuration}.<br>
* For every {@link Group} of name <i>groupName</i> which appears during
* loading in the configuration file,
* {@link ConfigurationInterface#configurationChanged(Group)} will be
* called.<br>
* During saving, every {@link Group} listed in <i>groupNames</i> will be
* removed, before {@link ConfigurationInterface#configurationWriteback()}
* will be called, so only these {@link Group}s will be written to disk.
*
* @param co
* a configurable object
* @param groupName
* a {@link Group} name which will be handled by the configurable
* object if it exists in the {@link Configuration}.
* @throws ConfigurationException
* if reading or writing to default file fails
*/
public synchronized void registerConfigurableObject(
final ConfigurationInterface co, final String groupName)
throws ConfigurationException {
final Set<String> set = new HashSet<String>(1);
set.add(groupName);
registerConfigurableObject(co, set);
}
/**
* Removes an external object from this {@link Configuration}.<br>
* All registered groups will be removed and
* {@link ConfigurationInterface#configurationWriteback()} will be called to
* write the last changes.
*
* @param co
* a configurable object
* @throws ConfigurationException
* if writing back configuration changes to default file fails
*/
public synchronized void unregisterConfigurableObject(
final ConfigurationInterface co) throws ConfigurationException {
Log.detail("Unregister '%s' from '%s'", co, super.toString());
if (!configObjects.contains(co))
throw new IllegalStateException("Not registered");
// write back configuration for all groups of the object
final List<Group> groups = co.configurationWriteback();
// put all groups of the object into the list of unassigned groups
for (final Group group : groups)
groupsUnassigned.add(group);
// remove registration of this object
final Iterator<Entry<String, ConfigurationInterface>> it = groupsRegistered
.entrySet().iterator();
while (it.hasNext())
if (it.next().getValue() == co) it.remove();
configObjects.remove(co);
}
public synchronized void readFromDefaultFile()
throws ConfigurationException {
readFromFile(DEFAULT_CONFIG_FILE);
}
public synchronized void readFromFile(final File file)
throws ConfigurationException {
readFromFile(file, null);
}
public synchronized void readFromFile(final File file,
final ConfigurationInterface coOnly) throws ConfigurationException {
try {
final BufferedReader in = new BufferedReader(new FileReader(file));
try {
readFromReader(in, coOnly);
} finally {
in.close();
}
} catch (final IOException e) {
throw new ConfigurationException(e, "Cannot read from '%s'", file);
}
}
public synchronized void readFromReader(final BufferedReader in,
final ConfigurationInterface coOnly) throws ConfigurationException,
IOException {
Log.detail("Reading configuration");
if (coOnly == null) {
for (final ConfigurationInterface co : configObjects)
co.configurationAboutToBeChanged();
groupsUnassigned.clear();
} else
coOnly.configurationAboutToBeChanged();
final int[] nr = new int[1];
String line;
// read lines before the first group
while ((line = in.readLine()) != null) {
++nr[0];
line = line.trim();
if (line.isEmpty() || line.charAt(0) == '#') continue;
break;
}
// read all the groups
while (true) {
if (line == null) break;
if (line.charAt(0) != '[' || line.charAt(line.length() - 1) != ']')
throw new ConfigurationException(nr[0], line,
"Expected a group name between '[' and ']'");
line = readGroup(in, nr, line.substring(1, line.length() - 1)
.trim(), coOnly);
}
if (coOnly == null)
for (final ConfigurationInterface co : configObjects)
co.configurationRead();
Log.detail("Done reading %d configuration line(s)", nr[0]);
}
private String readGroup(final BufferedReader in, final int[] nr,
final String groupName, final ConfigurationInterface coOnly)
throws ConfigurationException, IOException {
Log.detail("Reading config group '%s' at line %d", groupName, nr[0]);
final ConfigurationInterface co = groupsRegistered.get(groupName);
if (coOnly != null && co != coOnly) {
Log.warn("Ignoring group '%s' because it doesn't belong to '%s'",
groupName, coOnly);
return fastSkipGroup(in, nr);
}
Group group = co == null ? null : co.getSkeleton(groupName);
final boolean hasSkeleton = group != null;
if (group == null)
group = new Group(groupName);
else if (!group.getName().equals(groupName))
throw new ConfigurationException(nr[0], "???",
"Skeleton Group-Name '%s' mismatches current "
+ "Group-Name '%s'", group.getName(), groupName);
String line;
while ((line = in.readLine()) != null) {
++nr[0];
line = line.trim();
if (line.isEmpty() || line.charAt(0) == '#') continue;
if (line.charAt(0) == '[') break; // done with this group
readValue(in, nr, group, hasSkeleton, line);
}
if (co == null) {
assert coOnly == null;
groupsUnassigned.add(group);
} else
co.configurationChanged(group);
Log.detail("Done reading config group '%s' at line %d "
+ "registered by '%s'", group, nr[0], co);
return line;
}
private String fastSkipGroup(final BufferedReader in, final int[] nr)
throws IOException {
String line;
while ((line = in.readLine()) != null) {
++nr[0];
line = line.trim();
if (line.isEmpty() || line.charAt(0) == '#') continue;
if (line.charAt(0) == '[') break; // done with this group
}
return line;
}
private void readValue(final BufferedReader in, final int[] nr,
final Group group, final boolean hasSkeleton, final String line)
throws ConfigurationException, IOException {
final int colIdx = line.indexOf(':');
if (colIdx == -1)
throw new ConfigurationException(nr[0], line,
"Expected a label name followed by ':' or "
+ "a group name between '[' and ']'");
String label = line.substring(0, colIdx).trim();
final String content = line.substring(colIdx + 1).trim();
String identifier = null;
if (label.isEmpty())
throw new ConfigurationException(nr[0], line,
"Expected a non-empty label name followed by ':'");
if (label.charAt(label.length() - 1) == '>') {
final int brkIdx = label.indexOf('<');
if (brkIdx == -1)
throw new ConfigurationException(nr[0], line,
"Missing '<' for type in label name");
identifier = label.substring(brkIdx + 1, label.length() - 1).trim();
label = label.substring(0, brkIdx).trim();
}
Log.detail("Parsed label '%s', identifier '%s', content '%s'", label,
identifier, content);
ConfigValue value = null;
final boolean singleLined = !"{".equals(content);
boolean isUnknown = false;
if (hasSkeleton) {
value = group.get(label);
if (value == null) isUnknown = true;
}
if (value == null)
value = ConfigValue.createValue(identifier, label, singleLined);
try {
if (singleLined)
value.setFromString(content);
else
value.setFromStrings(readList(in, nr));
} catch (final ConfigurationException e) {
Log.warn("Failed to read value '%s' from '%s': '%s'", label,
group.getName(), e.getMessage());
}
if (isUnknown)
Log.error("Ignoring unknown value '%s' of group '%s'", value,
group.getName());
else
group.set(value);
}
private List<String> readList(final BufferedReader in, final int[] nr)
throws IOException, ConfigurationException {
final List<String> items = new ArrayList<String>();
String line;
while ((line = in.readLine()) != null) {
++nr[0];
line = line.trim();
if (!line.isEmpty() && line.charAt(0) == '#') continue;
if ("}".equals(line)) return items;
items.add(line);
}
++nr[0];
throw new ConfigurationException(nr[0], "",
"Missing line with '}' for end of value list");
}
public synchronized void writeToDefaultFile() throws ConfigurationException {
writeToFile(DEFAULT_CONFIG_FILE);
}
public synchronized void writeToFile(final File file)
throws ConfigurationException {
writeToFile(file, null);
}
public synchronized void writeToFile(final File file,
final ConfigurationInterface coOnly) throws ConfigurationException {
try {
final FileWriter out = new FileWriter(file);
try {
writeToWriter(out, coOnly);
} finally {
out.close();
}
} catch (final IOException e) {
throw new ConfigurationException(e, "Cannot write to '%s'", file);
}
}
public synchronized void writeToWriter(final Writer out,
final ConfigurationInterface coOnly) throws IOException {
Log.detail("Writing configuration");
if (coOnly == null)
writeAll(out);
else
writeConfigurationInterface(out, coOnly);
out.flush();
Log.detail("Done writing configuration");
}
private void writeAll(final Writer out) throws IOException {
for (final ConfigurationInterface co : configObjects)
writeConfigurationInterface(out, co);
Log.detail("Writing unassigned groups");
for (final Group group : groupsUnassigned)
writeGroup(out, group);
}
private void writeConfigurationInterface(final Writer out,
final ConfigurationInterface co) throws IOException {
try {
final List<Group> list = co.configurationWriteback();
Log.detail("Got groups by '%s': %s", co, list);
for (final Group group : list)
writeGroup(out, group);
} catch (final ConfigurationException e) {
Log.error(e, "Part of configuration could not be saved:");
}
}
private void writeGroup(final Writer out, final Group group)
throws IOException {
Log.detail("Writing group '%s'", group);
out.write('[');
out.write(group.getName());
out.write(']');
out.write('\n');
for (final ConfigValue value : group.getValueMap().values()) {
out.write(value.getLabel());
final String id = value.getIdentifier();
if (id != null) {
out.write('<');
out.write(id);
out.write('>');
}
out.write(": ");
if (value.isSingleLined()) {
out.write(value.asString());
out.write('\n');
} else {
out.write('{');
out.write('\n');
for (final String line : value.asStrings()) {
out.write('\t');
out.write(line);
out.write('\n');
}
out.write('}');
out.write('\n');
}
}
out.write('\n');
}
@Override
public synchronized String toString() {
final StringBuilder sb = new StringBuilder("Configuration");
sb.append(" Registered configurable objects: ");
sb.append(configObjects.toString());
sb.append(" Registered groups: ");
sb.append(groupsRegistered.toString());
sb.append(" Unassigned groups: ");
sb.append(groupsUnassigned.toString());
return sb.toString();
}
public static List<Group> asList(final Group group) {
final List<Group> res = new ArrayList<Group>(1);
res.add(group);
return res;
}
public synchronized List<Group> getGroupsUnassigned() {
return Collections.unmodifiableList(groupsUnassigned);
}
public synchronized Group getGroupUnassigned(final String name) {
for (final Group g : groupsUnassigned)
if (g.getName().equals(name)) return g;
return null;
}
public synchronized Group getGroupUnassignedSafe(final String name) {
for (final Group g : groupsUnassigned)
if (g.getName().equals(name)) return g;
return new Group(name);
}
public synchronized Map<String, ConfigurationInterface> getGroupsRegistered() {
return Collections.unmodifiableMap(groupsRegistered);
}
public synchronized Set<ConfigurationInterface> getConfigObjects() {
return Collections.unmodifiableSet(configObjects);
}
public synchronized boolean removeUnassignedGroup(final String name) {
for (final Group g : groupsUnassigned)
if (g.getName().equals(name)) {
groupsUnassigned.remove(g);
return true;
}
return false;
}
public synchronized boolean removeUnassignedGroup(final Group g) {
return groupsUnassigned.remove(g);
}
}