/**
*
* Copyright 2003-2005 The Apache Software Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.geronimo.system.configuration;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.OutputStream;
import java.net.URI;
import java.net.URL;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.Properties;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import javax.management.MalformedObjectNameException;
import javax.management.ObjectName;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.geronimo.gbean.GBeanData;
import org.apache.geronimo.gbean.GBeanInfo;
import org.apache.geronimo.gbean.GBeanInfoBuilder;
import org.apache.geronimo.gbean.GBeanLifecycle;
import org.apache.geronimo.kernel.Kernel;
import org.apache.geronimo.kernel.config.Configuration;
import org.apache.geronimo.kernel.config.ConfigurationData;
import org.apache.geronimo.kernel.config.ConfigurationInfo;
import org.apache.geronimo.kernel.config.ConfigurationModuleType;
import org.apache.geronimo.kernel.config.ConfigurationStore;
import org.apache.geronimo.kernel.config.InvalidConfigException;
import org.apache.geronimo.kernel.config.NoSuchConfigException;
import org.apache.geronimo.kernel.management.State;
import org.apache.geronimo.system.serverinfo.ServerInfo;
/**
* Implementation of ConfigurationStore using the local filesystem.
*
* @version $Rev$ $Date$
*/
public class LocalConfigStore implements ConfigurationStore, GBeanLifecycle {
private static final String INDEX_NAME = "index.properties";
private static final String BACKUP_NAME = "index.backup";
private static final String DELETE_NAME = "index.delete";
private final int REAPER_INTERVAL = 60 * 1000;
private final Kernel kernel;
private final ObjectName objectName;
private final URI root;
private final ServerInfo serverInfo;
private final Properties index = new Properties();
private final Properties pendingDeletionIndex = new Properties();
private ConfigStoreReaper reaper;
private final Log log;
private File rootDir;
private int maxId;
/**
* Constructor is only used for direct testing with out a kernel.
*/
public LocalConfigStore(File rootDir) {
kernel = null;
objectName = null;
serverInfo = null;
this.root = null;
this.rootDir = rootDir;
log = LogFactory.getLog("LocalConfigStore:"+rootDir.getName());
}
public LocalConfigStore(Kernel kernel, String objectName, URI root, ServerInfo serverInfo) throws MalformedObjectNameException {
this.kernel = kernel;
this.objectName = new ObjectName(objectName);
this.root = root;
this.serverInfo = serverInfo;
log = LogFactory.getLog("LocalConfigStore:"+root.toString());
}
public String getObjectName() {
return objectName.toString();
}
public synchronized void doStart() throws FileNotFoundException, IOException {
// resolve the root dir if not alredy resolved
if (rootDir == null) {
if (serverInfo == null) {
rootDir = new File(root);
} else {
rootDir = new File(serverInfo.resolve(root));
}
if (!rootDir.isDirectory()) {
throw new FileNotFoundException("Store root does not exist or is not a directory: " + rootDir);
}
}
index.clear();
File indexfile = new File(rootDir, INDEX_NAME);
InputStream indexIs = null;
try {
indexIs = new BufferedInputStream(new FileInputStream(indexfile));
index.load(indexIs);
for (Iterator i = index.values().iterator(); i.hasNext();) {
String id = (String) i.next();
maxId = Math.max(maxId, Integer.parseInt(id));
}
} catch (FileNotFoundException e) {
maxId = 0;
} finally {
if (indexIs != null)
indexIs.close();
}
// See if there are old directories which we should clean up...
File pendingDeletionFile = new File(rootDir, DELETE_NAME);
InputStream pendingIs = null;
try {
pendingIs = new BufferedInputStream(new FileInputStream(pendingDeletionFile));
pendingDeletionIndex.load(pendingIs);
} catch (FileNotFoundException e) {
// may not be one...
} finally {
if (pendingIs != null)
pendingIs.close();
}
// Create and start the reaper...
reaper = new ConfigStoreReaper(REAPER_INTERVAL);
Thread t = new Thread(reaper, "Geronimo Config Store Reaper");
t.setDaemon(true);
t.start();
}
public void doStop() {
if (reaper !=null) {
reaper.close();
}
}
public void doFail() {
if (reaper !=null) {
reaper.close();
}
}
private void saveIndex() throws IOException {
// todo provide a backout mechanism
File indexFile = new File(rootDir, INDEX_NAME);
File backupFile = new File(rootDir, BACKUP_NAME);
if (backupFile.exists()) {
backupFile.delete();
}
indexFile.renameTo(backupFile);
FileOutputStream fos = new FileOutputStream(indexFile);
try {
BufferedOutputStream os = new BufferedOutputStream(fos);
index.store(os, null);
os.close();
fos = null;
} catch (IOException e) {
if (fos != null) {
fos.close();
}
indexFile.delete();
backupFile.renameTo(indexFile);
throw e;
}
}
private void saveDeleteIndex() throws IOException {
File deleteFile = new File(rootDir, DELETE_NAME);
FileOutputStream fos = new FileOutputStream(deleteFile);
try {
BufferedOutputStream os = new BufferedOutputStream(fos);
pendingDeletionIndex.store(os, null);
os.close();
fos = null;
} catch (IOException e) {
if (fos != null) {
fos.close();
}
throw e;
}
}
public File createNewConfigurationDir() {
// loop until we find a directory that doesn't alredy exist
// this can happen when a deployment fails (leaving an bad directory)
// and the server reboots without saving out the index.propreties file
// the is rare but we should check for it
File configurationDir;
do {
String newId;
synchronized (this) {
newId = Integer.toString(++maxId);
}
configurationDir = new File(rootDir, newId);
} while (configurationDir.exists());
configurationDir.mkdir();
return configurationDir;
}
public URI install(URL source) throws IOException, InvalidConfigException {
return (URI) install2(source).getAttribute("id");
}
public GBeanData install2(URL source) throws IOException, InvalidConfigException {
File configurationDir = createNewConfigurationDir();
InputStream is = source.openStream();
try {
unpack(configurationDir, is);
} catch (IOException e) {
delete(configurationDir);
throw e;
} finally {
is.close();
}
URI configId;
GBeanData config;
try {
config = loadConfig(configurationDir);
configId = (URI) config.getAttribute("id");
index.setProperty(configId.toString(), configurationDir.getName());
} catch (Exception e) {
delete(configurationDir);
throw new InvalidConfigException("Unable to get ID from downloaded configuration", e);
}
synchronized (this) {
saveIndex();
}
log.debug("Installed configuration (URL) " + configId + " in location " + configurationDir.getName());
return config;
}
public void install(ConfigurationData configurationData, File source) throws IOException, InvalidConfigException {
if (!source.isDirectory()) {
throw new InvalidConfigException("Source must be a directory: source=" + source);
}
if (!source.getParentFile().equals(rootDir)) {
throw new InvalidConfigException("Source must be within the config store: source=" + source + ", configStoreDir=" + rootDir);
}
ExecutableConfigurationUtil.writeConfiguration(configurationData, source);
// update the index
synchronized (this) {
index.setProperty(configurationData.getId().toString(), source.getName());
saveIndex();
}
log.debug("Installed configuration (file) " + configurationData.getId() + " in location " + source.getName());
}
public void uninstall(URI configID) throws NoSuchConfigException, IOException {
String id = configID.toString();
File configDir;
String storeID;
synchronized(this) {
storeID = index.getProperty(id);
if (storeID == null) {
throw new NoSuchConfigException();
}
configDir = new File(rootDir, storeID);
index.remove(id);
saveIndex();
}
delete(configDir);
// On windoze, any open file descriptor (e.g. a MultiParentClassLoader) will prevent
// the directory/files from being deleted. If we're unable to delete, save the directory
// to the pendingDeletionIndex. ConfigStoreReaper will delete when the classloader has been GC'ed.
if (!configDir.exists()) {
log.debug("Uninstalled configuration " + configID);
} else {
log.debug("Uninstalled configuration, but could not delete ConfigStore directory for " + configID);
synchronized (pendingDeletionIndex) {
pendingDeletionIndex.setProperty(configDir.toString(), id);
saveDeleteIndex();
}
}
}
public synchronized ObjectName loadConfiguration(URI configId) throws NoSuchConfigException, IOException, InvalidConfigException {
GBeanData config = loadConfig(getRoot(configId));
ObjectName name;
try {
name = Configuration.getConfigurationObjectName(configId);
} catch (MalformedObjectNameException e) {
throw new InvalidConfigException("Cannot convert id to ObjectName: ", e);
}
config.setName(name);
config.setAttribute("baseURL", getRoot(configId).toURL());
try {
kernel.loadGBean(config, Configuration.class.getClassLoader());
} catch (Exception e) {
throw new InvalidConfigException("Unable to register configuration", e);
}
log.debug("Loaded Configuration " + name);
return name;
}
public List listConfigurations() {
List configs;
synchronized (this) {
configs = new ArrayList(index.size());
for (Iterator i = index.keySet().iterator(); i.hasNext();) {
URI configId = URI.create((String) i.next());
try {
ObjectName configName = Configuration.getConfigurationObjectName(configId);
State state;
if (kernel.isLoaded(configName)) {
try {
state = State.fromInt(kernel.getGBeanState(configName));
} catch (Exception e) {
state = null;
}
} else {
// If the configuration is not loaded by the kernel
// and defined by the store, then it is stopped.
state = State.STOPPED;
}
GBeanData bean = loadConfig(getRoot(configId));
ConfigurationModuleType type = (ConfigurationModuleType) bean.getAttribute("type");
configs.add(new ConfigurationInfo(objectName, configId, state, type));
} catch (Exception e) {
// bad configuration in store - ignored for this purpose
log.warn("Unable get configuration info for configuration " + configId, e);
}
}
}
return configs;
}
public synchronized boolean containsConfiguration(URI configID) {
return index.getProperty(configID.toString()) != null;
}
private synchronized File getRoot(URI configID) throws NoSuchConfigException {
String id = index.getProperty(configID.toString());
if (id == null) {
throw new NoSuchConfigException("No such config: " + configID);
}
return new File(rootDir, id);
}
private GBeanData loadConfig(File configRoot) throws IOException, InvalidConfigException {
File file = new File(configRoot, "META-INF/config.ser");
if (!file.isFile()) {
throw new InvalidConfigException("Configuration does not contain a META-INF/config.ser file");
}
FileInputStream fis = new FileInputStream(file);
try {
ObjectInputStream ois = new ObjectInputStream(new BufferedInputStream(fis));
GBeanData config = new GBeanData();
try {
config.readExternal(ois);
} catch (ClassNotFoundException e) {
//TODO more informative exceptions
throw new InvalidConfigException("Unable to read attribute ", e);
} catch (Exception e) {
throw new InvalidConfigException("Unable to set attribute ", e);
}
return config;
} finally {
fis.close();
}
}
public static void unpack(File to, InputStream from) throws IOException {
ZipInputStream zis = new ZipInputStream(from);
try {
ZipEntry entry;
byte[] buffer = new byte[4096];
while ((entry = zis.getNextEntry()) != null) {
File out = new File(to, entry.getName());
if (entry.isDirectory()) {
out.mkdirs();
} else {
if (!entry.getName().equals("META-INF/startup-jar")) {
out.getParentFile().mkdirs();
OutputStream os = new FileOutputStream(out);
try {
int count;
while ((count = zis.read(buffer)) > 0) {
os.write(buffer, 0, count);
}
} finally {
os.close();
}
zis.closeEntry();
}
}
}
} catch (IOException e) {
delete(to);
throw e;
}
}
private static void delete(File root) throws IOException {
File[] files = root.listFiles();
if ( null == files ) {
return;
}
for (int i = 0; i < files.length; i++) {
File file = files[i];
if (file.isDirectory()) {
delete(file);
}
else {
file.delete();
}
}
root.delete();
}
public static final GBeanInfo GBEAN_INFO;
static {
GBeanInfoBuilder infoFactory = GBeanInfoBuilder.createStatic(LocalConfigStore.class, "ConfigurationStore"); //NameFactory.CONFIGURATION_STORE
infoFactory.addAttribute("kernel", Kernel.class, false);
infoFactory.addAttribute("objectName", String.class, false);
infoFactory.addAttribute("root", URI.class, true);
infoFactory.addReference("ServerInfo", ServerInfo.class, "GBean");
infoFactory.addInterface(ConfigurationStore.class);
infoFactory.setConstructor(new String[]{"kernel", "objectName", "root", "ServerInfo"});
GBEAN_INFO = infoFactory.getBeanInfo();
}
public static GBeanInfo getGBeanInfo() {
return GBEAN_INFO;
}
/**
* Thread to cleanup unused Config Store entries.
* On Windows, open files can't be deleted. Until MultiParentClassLoaders
* are GC'ed, we won't be able to delete Config Store directories/files.
*/
class ConfigStoreReaper implements Runnable {
private final int reaperInterval;
private volatile boolean done = false;
public ConfigStoreReaper(int reaperInterval) {
this.reaperInterval = reaperInterval;
}
public void close() {
this.done = true;
}
public void run() {
log.debug("ConfigStoreReaper started");
while(!done) {
try {
Thread.sleep(reaperInterval);
} catch (InterruptedException e) {
continue;
}
reap();
}
}
/**
* For every directory in the pendingDeletionIndex, attempt to delete all
* sub-directories and files.
*/
public void reap() {
// return, if there's nothing to do
if (pendingDeletionIndex.size() == 0)
return;
// Otherwise, attempt to delete all of the directories
Enumeration list = pendingDeletionIndex.propertyNames();
boolean dirDeleted = false;
while (list.hasMoreElements()) {
String dirName = (String)list.nextElement();
File deleteFile = new File(dirName);
try {
delete(deleteFile);
}
catch (IOException ioe) { // ignore errors
}
if (!deleteFile.exists()) {
String configName = pendingDeletionIndex.getProperty(dirName);
pendingDeletionIndex.remove(dirName);
dirDeleted = true;
log.debug("Reaped configuration " + configName + " in directory " + dirName);
}
}
// If we deleted any directories, persist the list of directories to disk...
if (dirDeleted) {
try {
synchronized (pendingDeletionIndex) {
saveDeleteIndex();
}
}
catch (IOException ioe) {
log.warn("Error saving " + DELETE_NAME + " file.", ioe);
}
}
}
}
}