/*
* Copyright 2012 aquenos GmbH.
* All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v1.0 which accompanies this distribution,
* and is available at http://www.eclipse.org/legal/epl-v10.html.
*/
package com.aquenos.scm.ssh.server;
import java.io.File;
import java.util.LinkedHashSet;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import sonia.scm.ConfigurationException;
import sonia.scm.SCMContext;
import com.google.inject.Singleton;
/**
* Provides load and save operations for the SSH server configuration file.
*
* @author Sebastian Marsching
*/
@Singleton
public class ScmSshServerConfigurationStore {
private final static String PATH = "config" + File.separator
+ "scm-ssh-plugin.xml";
private JAXBContext context;
private File configurationFile;
private long configurationLastModified = -1L;
private ScmSshServerConfiguration configuration = null;
private final Object configLock = new Object();
private LinkedHashSet<ConfigurationChangeListener> listeners = new LinkedHashSet<ConfigurationChangeListener>();
private final Object listenerLock = new Object();
/**
* Interface implemented by configuration change listeners.
*
* @author Sebastian Marsching
*/
public static interface ConfigurationChangeListener {
/**
* Listener method called every time the configuration changes. Please
* note that this method will not be called the first time the
* configuration is loaded (when no configuration has been loaded
* before).
*
* @param newConfiguration
* the new configuration.
*/
void configurationChanged(ScmSshServerConfiguration newConfiguration);
}
/**
* Creates a new configuration store. This object should be managed by Guice
* to ensure that there is only a single shared instance.
*/
public ScmSshServerConfigurationStore() {
try {
this.context = JAXBContext
.newInstance(ScmSshServerConfiguration.class);
this.configurationFile = new File(SCMContext.getContext()
.getBaseDirectory(), PATH);
} catch (JAXBException e) {
throw new RuntimeException(
"Could not create JAXB context for class "
+ ScmSshServerConfiguration.class.getName() + ": "
+ e.getMessage(), e);
}
}
/**
* Loads the configuration from the configuration file. If the configuration
* file does not exist, the default configuration is returned.
*
* @return configuration loaded from file or default configuration if
* configuration file does not exist.
* @throws ConfigurationException
* if configuration file exists but the configuration cannot be
* read from this file.
*/
public ScmSshServerConfiguration load() {
ScmSshServerConfiguration oldConfiguration;
ScmSshServerConfiguration newConfiguration;
synchronized (configLock) {
oldConfiguration = configuration;
if (configuration == null) {
if (!configurationFile.exists()) {
configuration = new ScmSshServerConfiguration();
} else {
doLoad();
}
} else {
if (configurationFile.exists()
&& configurationFile.lastModified() > configurationLastModified) {
doLoad();
}
}
newConfiguration = configuration;
}
if (oldConfiguration != null
&& !oldConfiguration.equals(newConfiguration)) {
notifyListeners(newConfiguration);
}
return configuration.clone();
}
private void doLoad() {
try {
long lastModified = configurationFile.lastModified();
Unmarshaller unmarshaller = context.createUnmarshaller();
ScmSshServerConfiguration loadedConfiguration = (ScmSshServerConfiguration) unmarshaller
.unmarshal(configurationFile);
if (loadedConfiguration != null) {
configuration = loadedConfiguration;
configurationLastModified = lastModified;
} else {
throw new ConfigurationException(
"Got null result from unmarshaller while trying to load ssh-server configuration from "
+ configurationFile);
}
} catch (JAXBException e) {
throw new ConfigurationException(
"Error while trying to load ssh-server configuration from "
+ configurationFile, e);
}
}
/**
* Stores the configuration to the configuration file.
*
* @param configuration
* configuration to be saved.
* @throws ConfigurationException
* if configuration cannot be saved.
*/
public void store(ScmSshServerConfiguration configuration) {
// Make a copy of the object that has been passed in.
configuration = configuration.clone();
ScmSshServerConfiguration oldConfiguration;
// Create parent directory.
if (!configurationFile.getParentFile().exists()) {
configurationFile.getParentFile().mkdirs();
}
synchronized (configLock) {
oldConfiguration = this.configuration;
try {
Marshaller marshaller = context.createMarshaller();
marshaller.marshal(configuration, configurationFile);
} catch (JAXBException e) {
throw new ConfigurationException(
"Error while trying to store ssh-server configuration in "
+ configurationFile, e);
}
this.configuration = configuration;
// There is a slight chance of a race condition here, when the file
// is modified using the store method and directly on the filesystem
// at the same moment. However this case is very unlikely and even
// if it happens the only harm will be, that the changes on the
// filesystem will be ignored.
this.configurationLastModified = configurationFile.lastModified();
}
if (oldConfiguration != null && !oldConfiguration.equals(configuration)) {
notifyListeners(configuration);
}
}
private void notifyListeners(ScmSshServerConfiguration newConfiguration) {
synchronized (listenerLock) {
for (ConfigurationChangeListener listener : listeners) {
listener.configurationChanged(newConfiguration.clone());
}
}
}
/**
* Registers a listener that will be informed of configuration changes. The
* configuration store does not actively monitor the configuration file for
* changes. However, if a change is detected (either because the
* {@link #load()} method is called and the file has changed in between or
* because the {@link #store(ScmSshServerConfiguration)} method is called
* with a changed configuration), the listeners are informed.
*
* @param listener
* listener to register for configuration change events.
* @return <code>true</code> if the listener has not been registered before
* and <code>false</code> if the listener was already registered.
*/
public boolean addConfigurationChangeListener(
ConfigurationChangeListener listener) {
synchronized (listenerLock) {
return this.listeners.add(listener);
}
}
/**
* Removes a listener. The listener will not receive any more change
* notifications after it has been removed.
*
* @param listener
* listener to unregister from configuration change events.
* @return <code>true</code> if the listener has been registered before and
* has been unregistered now, <code>false</code> if the listener is
* not registered.
*/
public boolean removeConfigurationChangeListener(
ConfigurationChangeListener listener) {
// There is a slight risk of a race condition here. It could happen that
// an earlier configuration change somehow got stalled between updating
// the configuration and notifying the listeners. In this case, the
// listeners will receive the older configuration version later.
// However, this situation is so unlikely (the rate of configuration
// changes is expected to be low), that we do not spend the extra
// overhead of maintaining a queue with the change events.
synchronized (listenerLock) {
return this.listeners.remove(listener);
}
}
}