/*
Copyright (C) 2006 EBI
This library 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 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the itmplied 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 library; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package org.biomart.common.resources;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import org.biomart.common.utils.FileUtils;
import org.biomart.common.view.gui.dialogs.StackTrace;
/**
* Manages the on-disk cache of user settings.
* <p>
* Settings are contained in a folder called <tt>.biomart</tt> in the user's
* home directory, inside which there is a second folder for each of the BioMart
* applications. In there are two files - one called <tt>properties</tt> which
* contains general configuration settings such as look and feel, and the other
* called <tt>cache</tt> which is a directory containing history settings for
* various classes.
* <p>
* You should only ever need to modify the <tt>properties</tt> file, and
* <tt>cache</tt> should be left alone.
*
* @author Richard Holland <holland@ebi.ac.uk>
* @version $Revision: 1.16 $, $Date: 2007-11-01 13:22:22 $, modified by
* $Author: rh4 $
* @since 0.5
*/
public class Settings {
/**
* App reference for MartBuilder.
*/
public static final String MARTBUILDER = "martbuilder";
/**
* App reference for MartRunner.
*/
public static final String MARTRUNNER = "martrunner";
private static String application;
// Insert more app references as more apps are built.
private static final File homeDir = new File(System
.getProperty("user.home"), ".biomart");
private static File appDir;
private static final Map classCache = new HashMap();
private static File classCacheDir;
private static int classCacheSize = 10;
private static boolean initialising = true;
private static final Properties properties = new Properties();
private static final File propertiesFile = new File(Settings.homeDir,
"properties");
private static final Object SAVE_LOCK = new String("__SAVE__LOCK");
private static final String manifestName = "__MANIFEST.txt";
// Create the bits we need on start-up.
static {
try {
if (!Settings.homeDir.exists())
Settings.homeDir.mkdir();
if (!Settings.propertiesFile.exists())
Settings.propertiesFile.createNewFile();
} catch (final Throwable t) {
Log.error("Failed to initialise settings cache");
}
}
/**
* Set the current application.
*
* @param app
* the current application.
*/
public static void setApplication(final String app) {
Settings.application = app;
// Make the home directory.
Settings.appDir = new File(Settings.homeDir, app);
if (!Settings.appDir.exists())
Settings.appDir.mkdir();
// Set up the logger.
Log.configure(app, Settings.appDir);
// Use it to log application startup.
Log.info("Started " + app);
// Make the class cache directory.
Settings.classCacheDir = new File(Settings.appDir, "objectCache");
final File classCacheVersion = new File(Settings.appDir,
"objectCacheVersion.txt");
// Remove it if pre-current version.
if (Settings.classCacheDir.exists()) {
// Load and check class cache version.
String oldVersion = null;
if (classCacheVersion.exists())
try {
final FileInputStream fis = new FileInputStream(
classCacheVersion);
final byte[] oldVersionBytes = new byte[Resources.BIOMART_VERSION
.length()];
final int readLength = fis.read(oldVersionBytes);
if (readLength >= 0)
oldVersion = new String(oldVersionBytes, 0, readLength);
fis.close();
} catch (final IOException e) {
// Assume if failed that it is an out-of-date version.
oldVersion = null;
}
// If differs, delete cache.
if (!Resources.BIOMART_VERSION.equals(oldVersion))
try {
FileUtils.delete(Settings.classCacheDir);
} catch (final IOException e) {
// Not much we can do apart from warn.
StackTrace.showStackTrace(e);
}
}
// If doesn't exist, or has been removed, (re)create it.
if (!Settings.classCacheDir.exists())
try {
Settings.classCacheDir.mkdir();
// Create/Overwrite version file.
final FileOutputStream fos = new FileOutputStream(
classCacheVersion);
fos.write(Resources.BIOMART_VERSION.getBytes());
fos.close();
} catch (final IOException e) {
// Not much we can do apart from warn.
StackTrace.showStackTrace(e);
}
// Remove pre-0.7 cache if present.
final File oldCache = new File(Settings.appDir, "cache");
if (oldCache.exists())
try {
FileUtils.delete(oldCache);
} catch (final IOException e) {
// Ignore.
}
}
/**
* Gets the current application
*
* @return the current application.
*/
public static String getApplication() {
return Settings.application;
}
/**
* Obtain the current application's storage directory.
*
* @return the directory.
*/
public static File getStorageDirectory() {
return Settings.appDir;
}
/**
* Saves the current cache of settings to disk as a set of files at
* <tt>~/.biomart/<appname></tt>.
*/
private static void save() {
// Don't save if we're still loading.
if (Settings.initialising) {
Log.debug("Still loading settings, so won't save settings yet");
return;
}
synchronized (Settings.SAVE_LOCK) {
try {
Log.debug("Saving settings to "
+ Settings.propertiesFile.getPath());
Settings.properties.store(new FileOutputStream(
Settings.propertiesFile), Resources
.get("settingsCacheHeader"));
// Save the class-by-class properties.
Log.debug("Saving class caches");
for (final Iterator i = Settings.classCache.entrySet()
.iterator(); i.hasNext();) {
final Map.Entry classCacheEntry = (Map.Entry) i.next();
final Class clazz = (Class) classCacheEntry.getKey();
final File classDir = new File(Settings.classCacheDir,
clazz.getName());
Log.debug("Creating class cache directory for "
+ clazz.getName());
classDir.mkdir();
// Remove existing files.
Log.debug("Clearing existing class cache files");
final File[] files = classDir.listFiles();
for (int j = 0; j < files.length; j++)
files[j].delete();
// Save current set. Must use Map.Entry else each
// call for map keys and values will change the
// structure of the LRU cache map, and hence cause
// ConcurrentModificationExceptions.
final List manifestList = new ArrayList();
for (final Iterator j = ((Map) classCacheEntry.getValue())
.entrySet().iterator(); j.hasNext();) {
final Map.Entry entry = (Map.Entry) j.next();
final String name = (String) entry.getKey();
manifestList.add(name);
final Properties props = (Properties) entry.getValue();
final File propsFile = new File(classDir, name);
Log
.debug("Saving properties to "
+ propsFile.getPath());
props.store(new FileOutputStream(propsFile), Resources
.get("settingsCacheHeader"));
}
// Write manifest speciying order of keys.
final File manifest = new File(classDir,
Settings.manifestName);
final FileOutputStream fos = new FileOutputStream(manifest);
final ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(manifestList);
oos.flush();
fos.close();
}
} catch (final Throwable t) {
Log.error("Failed to save settings", t);
}
Log.info("Done saving settings");
}
}
/**
* Given a class, return the set of names of properties from the history map
* that correspond to that class.
*
* @param clazz
* the class to look up.
* @return the names of the properties sets in the history that match that
* class. May be empty but never <tt>null</tt>.
*/
public static List getHistoryNamesForClass(final Class clazz) {
final Map map = (Map) Settings.classCache.get(clazz);
// Use copy of map keys in order to prevent concurrent modifications.
return map == null ? Collections.EMPTY_LIST : new ArrayList(map
.keySet());
}
/**
* Given a class and a group name, return the set of properties from history
* that match.
*
* @param clazz
* the class to look up.
* @param name
* the name of the property set in the history.
* @return the properties that match. <tt>null</tt> if there is no match.
*/
public static Properties getHistoryProperties(final Class clazz,
final String name) {
final Map map = (Map) Settings.classCache.get(clazz);
return map == null ? null : (Properties) map.get(name);
}
/**
* Given a property name, return that property based on the contents of the
* cache file <tt>properties</tt>.
*
* @param property
* the property name to look up.
* @return the value, or <tt>null</tt> if not found.
*/
public static String getProperty(final String property) {
String value = (String)Settings.properties.getProperty(property);
if (value==null) {
Settings.properties.setProperty(property, "");
return null;
}
else if ("".equals(value))
return null;
else
return value;
}
/**
* Loads the current cache of settings from disk, from the files in
* <tt>~/.biomart/<appname></tt>.
*/
public static synchronized void load() {
Settings.initialising = true;
// Clear the existing settings.
Log.debug("Clearing existing settings");
Settings.properties.clear();
// Load the settings.
try {
Log.debug("Loading settings from "
+ Settings.propertiesFile.getPath());
Settings.properties.load(new FileInputStream(
Settings.propertiesFile));
} catch (final Throwable t) {
Log.error("Failed to load settings", t);
}
// Set up the cache.
final String newClassCacheSize = Settings.properties
.getProperty("classCacheSize");
try {
Log.debug("Setting class cache size to " + newClassCacheSize);
Settings.classCacheSize = Integer.parseInt(newClassCacheSize);
} catch (final NumberFormatException e) {
// Ignore and use the default.
Settings.classCacheSize = 10;
Log.debug("Using default class cache size of "
+ Settings.classCacheSize);
Settings
.setProperty("classCacheSize", "" + Settings.classCacheSize);
}
// Loop over classCacheDir to find classes.
Log.debug("Loading class caches");
final String[] classes = Settings.classCacheDir.list();
if (classes != null)
for (int i = 0; i < classes.length; i++)
try {
final Class clazz = Class.forName(classes[i]);
Log.debug("Loading class cache for " + clazz.getName());
final File classDir = new File(Settings.classCacheDir,
classes[i]);
// Write manifest speciying reverse order of keys.
List manifestList;
try {
final File manifest = new File(classDir,
Settings.manifestName);
final FileInputStream fis = new FileInputStream(
manifest);
final ObjectInputStream ois = new ObjectInputStream(fis);
manifestList = (List) ois.readObject();
fis.close();
} catch (final Exception e) {
// No manifest? Use natural order instead.
manifestList = Arrays.asList(classDir.list());
}
// Load files in order specified in manifest.
for (final Iterator j = manifestList.iterator(); j
.hasNext();) {
final String entry = (String) j.next();
final Properties props = new Properties();
final File propsFile = new File(classDir, entry);
Log.debug("Loading properties from "
+ propsFile.getPath());
props.load(new FileInputStream(propsFile));
Settings.saveHistoryProperties(clazz, entry, props);
}
} catch (final ClassNotFoundException e) {
// Ignore. We don't care as these settings are
// now irrelevant if the class no longer exists.
} catch (final Throwable t) {
Log.error("Failed to load settings", t);
}
Settings.initialising = false;
Log.info("Done loading settings");
}
/**
* Given a bunch of properties, save them in the history of the given class
* with the given name. If the history contains very old stuff, age it out.
*
* @param clazz
* the class of the history properties to store.
* @param name
* the name to give the history entry.
* @param properties
* the properties to store.
*/
public static void saveHistoryProperties(final Class clazz,
final String name, final Properties properties) {
Log.debug("Adding history entry for " + clazz.getName() + ":" + name);
if (!Settings.classCache.containsKey(clazz)) {
Log.debug("Creating new cache for class " + clazz.getName());
final LinkedHashMap history = new LinkedHashMap(
Settings.classCacheSize, 0.75f, true) {
private static final long serialVersionUID = 1;
protected boolean removeEldestEntry(Map.Entry eldest) {
return this.size() > Settings.classCacheSize;
}
};
Settings.classCache.put(clazz, history);
}
Log.debug("History properties are: " + properties);
((Map) Settings.classCache.get(clazz)).put(name, properties);
Settings.save();
}
/**
* Given a property name, sets that property.
*
* @param property
* the property name to set.
* @param value
* the value to give it.
*/
public static void setProperty(final String property, final String value) {
Log.debug("Setting property " + property + "=" + value);
Settings.properties.setProperty(property, value);
Settings.save();
}
// Private means that this class is a static singleton.
private Settings() {
}
}