/*
Copyright (C) 2003 EBI, GRL
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 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 library; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package org.ensembl.mart.lib.config;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.security.MessageDigest;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.prefs.BackingStoreException;
import java.util.prefs.Preferences;
/**
* Object to cache DatasetConfiguration objects to the file system.
* Uses a combination of the client user Preferences, and a files in
* the home directory of the user under .martj_preferences.
* @author <a href="mailto:dlondon@ebi.ac.uk">Darin London</a>
* @author <a href="mailto:craig@ebi.ac.uk">Craig Melsopp</a>
*/
public class DatasetConfigCache {
private Logger logger = Logger.getLogger(DatasetConfigCache.class.getName());
private final String XMLDIR = System.getProperty("user.home") + File.separator + ".martj_preferences";
private final String NAMESEPARATOR = "__";
private final String NAMEPREFIX = ".";
private final String XMLENDING = ".xml";
private final String DNAMEKEY = "displayName";
private final String DESCKEY = "description";
private final String TYPEKEY = "type";
private final String VISIBLEKEY = "visible";
private final String VISIBLEFILTERPAGEKEY = "visibleFilterPage";
private final String VERSIONKEY = "version";
private final String DIGESTKEY = "MD5";
private final String XMLKEY = "XML";
private Preferences xmlPrefs = null;
private DSConfigAdaptor caller = null;
private String[] keys = null;
private DatasetConfigXMLUtils dscutils = null;
/**
* Constructs a cache for a given DatasetConfigAdaptor. Passing
* a key[] allows the cache to specify a key specific enough for any
* DSConfigAdaptor implementation.
* @param caller - DSConfigAdaptor that needs caching
* @param keys - String[] list of keys to use in creating the cache.
* @param dscutils - DatasetConfigXMLUtils object to read and write XML
*/
public DatasetConfigCache(DSConfigAdaptor caller, String[] keys, DatasetConfigXMLUtils dscutils) {
this.caller = caller;
this.keys = keys;
this.dscutils = dscutils;
initCache();
}
private void initCache() {
if (xmlPrefs == null) {
xmlPrefs = Preferences.userNodeForPackage(caller.getClass());
for (int i = 0, n = keys.length; i < n; i++) {
String key = keys[i];
xmlPrefs = xmlPrefs.node(key);
}
}
}
/**
* Clears the cache for this node, deleting any xml files in $HOME/.martj_preferences
* @throws ConfigurationException for underlying exceptions
*/
public void clearCache() throws ConfigurationException {
initCache();
try {
//find any xml files associated with this cache and delete them
String[] datasets = xmlPrefs.childrenNames();
for (int i = 0, n = datasets.length; i < n; i++) {
String dataset = datasets[i];
String[] inames = xmlPrefs.node(dataset).childrenNames();
for (int j = 0, m = inames.length; j < m; j++) {
String iname = inames[j];
deleteFile(dataset, iname);
xmlPrefs.node(dataset).node(iname).removeNode();
}
xmlPrefs.node(dataset).removeNode();
}
//remove the base directory if it is empty
File baseDir = initCacheDir();
if (baseDir.isDirectory() && baseDir.list().length < 1)
baseDir.delete();
xmlPrefs.flush();
} catch (BackingStoreException e) {
throw new ConfigurationException("Caught BackingStoreException clearing cache: " + e.getMessage(), e);
}
}
private File initCacheDir() {
File baseDir = new File(XMLDIR);
if (!baseDir.isDirectory())
baseDir.mkdir();
return baseDir;
}
private File getFile(String dataset, String iname) {
File baseDir = initCacheDir();
String fileName = NAMEPREFIX;
for (int i = 0, n = keys.length; i < n; i++) {
String key = keys[i];
fileName += key + NAMESEPARATOR;
}
fileName += dataset + NAMESEPARATOR + iname + XMLENDING;
File ret = new File(baseDir, fileName);
return ret;
}
private void deleteFile(String dataset, String internalName) {
String xmlloc = pathTo(dataset, internalName);
if (xmlloc != null) {
File xmlfile = new File(xmlloc);
if (xmlfile.exists())
xmlfile.delete();
}
}
/**
* Adds a DatasetConfig to the cache. This stores a file to $HOME/.martj_prefs/dataset/internalName.xml,
* and stores the identifying values of the DatasetConfig to the preferences object to allow lazy loading, as well
* as its file location and md5sum to check if it is up to date with the original source.
* @param dsc - DatasetConfig to cache
* @throws ConfigurationException for underlying BackingStoreExceptions and OutputStream exceptions
*/
public void addDatasetConfig(DatasetConfig dsc) throws ConfigurationException {
initCache();
String iname = dsc.getInternalName();
String dname = dsc.getDisplayName();
String dataset = dsc.getDataset();
String desc = dsc.getDescription();
byte[] digest = dsc.getMessageDigest();
String type = dsc.getType();
String visible = dsc.getVisible();
String version = dsc.getVersion();
deleteFile(dataset, iname);
File xmlFile = getFile(dataset, iname);
dscutils.writeDatasetConfigToFile(dsc, xmlFile);
//hidden datasets may have null display names
if (dname != null)
xmlPrefs.node(dataset).node(iname).put(DNAMEKEY, dname);
if (desc != null)
xmlPrefs.node(dataset).node(iname).put(DESCKEY, desc);
if (type != null)
xmlPrefs.node(dataset).node(iname).put(TYPEKEY, type);
if (visible != null)
xmlPrefs.node(dataset).node(iname).put(VISIBLEKEY, visible);
if (version != null)
xmlPrefs.node(dataset).node(iname).put(VERSIONKEY, version);
xmlPrefs.node(dataset).node(iname).put(XMLKEY, xmlFile.getAbsolutePath());
xmlPrefs.node(dataset).node(iname).putByteArray(DIGESTKEY, digest);
}
/**
* Removes all information for a DatasetConfig object specified by dataset and internalName from the cache,
* including its associated file in $HOME/.martj_preferences.
* @param dataset - dataset for DatasetConfig to be removed
* @param iname - internalname for the DatasetConfig to be removed
* @throws ConfigurationException for underlying exceptions
*/
public void removeDatasetConfig(String dataset, String iname) throws ConfigurationException {
initCache();
if (cacheExists(dataset, iname)) {
try {
deleteFile(dataset, iname);
xmlPrefs.node(dataset).node(iname).removeNode(); //removes this node entirely
if (xmlPrefs.node(dataset).childrenNames().length == 0)
xmlPrefs.node(dataset).removeNode(); //removes the dataset node, if empty
xmlPrefs.flush();
} catch (BackingStoreException e) {
throw new ConfigurationException(
"Caught BackingStoreException removing DatasetConfig from preferences node "
+ dataset
+ " internalName "
+ iname
+ " "
+ e.getMessage()
+ "\nAssuming it doesnt exist\n");
}
}
}
/**
* Returns a DatasetConfig for the given dataset and internalName.
* @param dataset -- dataset for required DatasetConfig
* @param iname -- internalName for required DatasetConfig
* @param adaptor -- DSConfigAdaptor to set as the underlying DSConfigAdaptor for the returned DatasetConfig object
* Note, in order to satisfy the contract for the DatasetConfig lazyLoad system, if this is passed null,
* the system will fully load the resulting DatasetConfig from the xml file before returning it.
* @return DatasetConfig for given dataset and internalName
* @throws ConfigurationException for underlying exceptions
*/
public DatasetConfig getDatasetConfig(String dataset, String iname, DSConfigAdaptor adaptor)
throws ConfigurationException {
initCache();
DatasetConfig dsv = null;
try {
if (xmlPrefs.nodeExists(dataset)) {
if (xmlPrefs.node(dataset).nodeExists(iname)) {
byte[] digest = xmlPrefs.node(dataset).node(iname).getByteArray(DIGESTKEY, null);
if (adaptor == null) {
dscutils.setFullyLoadMode(true); //temporarily
dsv = dscutils.getDatasetConfigForXMLStream(getXMLStream(dataset, iname));
dscutils.setFullyLoadMode(false);
} else {
String displayName = xmlPrefs.node(dataset).node(iname).get(DNAMEKEY, null);
String description = xmlPrefs.node(dataset).node(iname).get(DESCKEY, null);
String type = xmlPrefs.node(dataset).node(iname).get(TYPEKEY, null);
String visible = xmlPrefs.node(dataset).node(iname).get(VISIBLEKEY, null);
String version = xmlPrefs.node(dataset).node(iname).get(VERSIONKEY, null);
String visibleFilterPage = xmlPrefs.node(dataset).node(iname).get(VISIBLEFILTERPAGEKEY, null);
dsv = new DatasetConfig(iname, displayName, dataset, description, type, visible,visibleFilterPage,version,"","","","","","","","","","","");
dsv.setDSConfigAdaptor(adaptor);
}
if (digest != null)
dsv.setMessageDigest(digest);
}
}
} catch (BackingStoreException e) {
throw new ConfigurationException(
"Caught BackingStoreException getting DatasetConfig from preferences node "
+ dataset
+ " internalName "
+ iname
+ " "
+ e.getMessage());
} catch (ConfigurationException e) {
throw e;
}
return dsv;
}
/**
* lazyLoads a given DatasetConfig object from its cache, if present.
* @param dsv -- DatasetConfig to be lazyLoaded.
* @throws ConfigurationException for all underlying exceptions that prevent the DatasetView from being lazyLoaded.
*/
public void lazyLoadWithCache(DatasetConfig dsv) throws ConfigurationException {
initCache();
String dataset = dsv.getDataset();
String iname = dsv.getInternalName();
if (cacheExists(dataset, iname)) {
try {
InputStream xmlinput = getXMLStream(dataset, iname);
dscutils.loadDatasetConfigWithDocument(
dsv,
dscutils.getDocumentForXMLStream(xmlinput));
xmlinput.close();
} catch (ConfigurationException e) {
throw e;
} catch (IOException e) {
if (logger.isLoggable(Level.FINE))
logger.fine(
"Caught IOException closing Stream: " + e.getMessage() + "\nAssuming datasetview was properly lazyLoaded\n");
}
} else
throw new ConfigurationException("Cache does not exist for " + dataset + " " + iname + "\n");
}
private String pathTo(String dataset, String iname) {
return xmlPrefs.node(dataset).node(iname).get(XMLKEY, null);
}
private InputStream getXMLStream(String dataset, String iname) throws ConfigurationException {
FileInputStream ret = null;
String xmlloc = pathTo(dataset, iname);
if (xmlloc == null) {
throw new ConfigurationException(
"Could not retrieve cache information for dataset "
+ dataset
+ " internalName "
+ iname
+ " does not appear to be cached!\n");
} else {
File xmlfile = new File(xmlloc);
try {
ret = new FileInputStream(xmlfile);
} catch (FileNotFoundException e) {
throw new ConfigurationException(
"Could not retrieve cache information for dataset "
+ dataset
+ " internalName "
+ iname
+ " does not appear to be cached!\n"
+ e.getMessage()
+ "\n",
e);
}
}
return ret;
}
/**
* Determine if cache exists for a specified DatasetConfig, given its dataset and internalName.
* @param dataset -- dataset for required DatasetConfig
* @param iname -- optional internalName for required DatasetConfig. If null, only checks for existence of dataset cache
* @return boolean, true if cache exists for this dataset, and optional internalName, false otherwise
* @throws ConfigurationException for underlying exceptions
*/
public boolean cacheExists(String dataset, String iname) throws ConfigurationException {
initCache();
boolean ret = false;
try {
ret = xmlPrefs.nodeExists(dataset);
if (ret && iname != null)
ret = xmlPrefs.node(dataset).nodeExists(iname);
} catch (BackingStoreException e) {
throw new ConfigurationException(
"Recieved BackingStoreException determining existence of cache for "
+ dataset
+ " "
+ iname
+ "\n"
+ e.getMessage(),
e);
}
return ret;
}
/**
* Determines if the cache for a specified DatasetConfig is up to date for a given source MD5SUM MessageDigest.
* If this returns a false value, then this method actually removes any cache for the given dataset and internalName before returning.
* @param sourceDigest -- byte[] MD5SUM MessageDigest
* @param dataset -- dataset for required DatasetConfig
* @param iname -- internalName for required DatasetConfig
* @return boolean, true if cache exists, and digest in cache matches the given sourceDigest, false otherwise
* This will also return false if, for some reason, the cached digest cannot be retrieved.
* @throws ConfigurationException
*/
public boolean cacheUpToDate(byte[] sourceDigest, String dataset, String iname) throws ConfigurationException {
boolean ret = cacheExists(dataset, iname);
if (ret) {
byte[] cacheDigest = xmlPrefs.node(dataset).node(iname).getByteArray(DIGESTKEY, new byte[0]);
//if the cache cannot return the digest for some reason, it should return an empty byte[]
ret = MessageDigest.isEqual(cacheDigest, sourceDigest);
}
if (!ret) {
removeDatasetConfig(dataset, iname);
}
return ret;
}
}