/*
* ====================================================================
* Copyright (c) 2005-2012 sventon project. All rights reserved.
*
* This software is licensed as described in the file LICENSE, which
* you should have received as part of this distribution. The terms
* are also available at http://www.sventon.org.
* If newer versions of this license are posted there, you may use a
* newer version instead, at your option.
* ====================================================================
*/
package org.sventon.appl;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.Validate;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jmx.export.annotation.ManagedOperation;
import org.springframework.jmx.export.annotation.ManagedResource;
import org.sventon.Version;
import org.sventon.cache.CacheException;
import org.sventon.cache.CacheManager;
import org.sventon.model.RepositoryName;
import javax.annotation.PostConstruct;
import java.io.*;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.*;
import java.util.concurrent.ConcurrentLinkedQueue;
/**
* Represents the sventon application.
* <p/>
* Initializes sventon. It should be instantiated once (and only once), when the application starts.
*
* @author jesper@sventon.org
* @author patrik@sventon.org
*/
@ManagedResource
public class Application {
/**
* The logging instance.
*/
private final Log logger = LogFactory.getLog(getClass());
/**
* Holder of all repository configurations in use.
*/
private final RepositoryConfigurations repositoryConfigurations = new RepositoryConfigurations();
/**
* Will be <code>true</code> if all parameters are ok.
*/
private boolean configured;
/**
* Application configuration directory.
*/
private final File repositoriesDirectory;
/**
* Application configuration file name.
*/
private String configurationFileName;
/**
* Toggles the possibility to edit the config after initial setup.
*/
private boolean editableConfig;
/**
* Password needed to access the config pages.
*/
private String configPassword;
/**
* Toggles the possibility to reload the configuration from disk using a GET request to /config/reload GET.
*/
private boolean configurationReloadSupported;
private final List<CacheManager> cacheManagers = new ArrayList<CacheManager>();
/**
* Map of update markers.
*/
private final ConcurrentLinkedQueue<RepositoryName> updating = new ConcurrentLinkedQueue<RepositoryName>();
/**
* System property, sventon.baseURL, used to set a base property for relative URL:s.
*/
public static final String PROPERTY_KEY_SVENTON_BASE_URL = "sventon.baseURL";
/**
* Scheduler for scheduled jobs.
*/
private Scheduler scheduler;
/**
* Constructor.
*
* @param configDirectory Configuration root directory. Directory will be created if it does not already exist,
* not {@code null} Configuration settings will be stored in this directory.
*/
public Application(final ConfigDirectory configDirectory) {
Validate.notNull(configDirectory, "Config directory cannot be null");
this.repositoriesDirectory = configDirectory.getRepositoriesDirectory();
}
/**
* Sets scheduler instance.
* The scheduler is used to fire cache update job after configuration has been done.
*
* @param scheduler The scheduler
*/
public void setScheduler(final Scheduler scheduler) {
this.scheduler = scheduler;
}
/**
* Initializes the sventon application.
*
* @throws IOException if unable to load the instance configurations.
* @throws CacheException if unable to initialize caches.
*/
@PostConstruct
public void init() throws IOException, CacheException {
logger.info("Initializing sventon version " + Version.getVersion());
final File[] configDirectories = getConfigDirectories();
if (configDirectories.length > 0) {
loadRepositoryConfigurations(configDirectories, repositoryConfigurations);
initCaches();
} else {
logger.debug("No configuration files were found below: " + repositoriesDirectory.getAbsolutePath());
logger.info("No repository has been configured yet. Access sventon web application to start the setup");
}
final URL baseURL = getBaseURL();
if (baseURL != null) {
logger.info("Property [" + PROPERTY_KEY_SVENTON_BASE_URL + "] set to: " + baseURL);
}
}
@ManagedOperation
public synchronized void reinit() throws IOException, CacheException {
logger.info("Starting Application reinitialization.");
logger.debug("Reloading config data.");
RepositoryConfigurations newConfigs = new RepositoryConfigurations();
loadRepositoryConfigurations(getConfigDirectories(), newConfigs);
logger.debug("Calculating diff.");
RepositoryConfigurations.ConfigsDiff diff = repositoryConfigurations.diffByRepositoryName(newConfigs);
//TODO: For extra security store and check timestamp for props file
if (diff.added.isEmpty() && diff.removed.isEmpty()) {
logger.info("New config same as old. Configuration unchanged.");
} else {
logger.info("Reloading configurations:");
logger.info(" added: " + diff.added);
logger.info(" removed: " + diff.removed);
try {
logger.debug("Pausing change monitor jobs");
scheduler.pauseJob("repositoryChangeMonitorUpdateJobDetail", Scheduler.DEFAULT_GROUP);
} catch (SchedulerException sx) {
logger.warn(sx);
}
logger.debug("Applying config diff.");
repositoryConfigurations.apply(diff);
logger.debug("Shutting down unused caches.");
for (RepositoryConfiguration repositoryConfiguration : diff.removed) {
if (repositoryConfiguration.isCacheUsed()) {
logger.info("Shutting down caches for: " + repositoryConfiguration.getName());
shutdownAndDeregisterCaches(repositoryConfiguration);
}
}
logger.debug("(Re)starting caches");
initCaches(); //(re)start caches
try {
logger.debug("Resuming change monitor jobs, firing trigger.");
scheduler.resumeJob("repositoryChangeMonitorUpdateJobDetail", Scheduler.DEFAULT_GROUP);
scheduler.triggerJob("repositoryChangeMonitorUpdateJobDetail", Scheduler.DEFAULT_GROUP);
} catch (SchedulerException sx) {
logger.warn(sx);
}
logger.info("Cleaning up config directories.");
cleanupOldConfigDirectories(getBackupConfigDirectories());
}
logger.info("Application reinitialization completed.");
}
private void shutdownAndDeregisterCaches(RepositoryConfiguration repositoryConfiguration) throws CacheException {
for (CacheManager cacheManager : cacheManagers) {
cacheManager.shutdown(repositoryConfiguration.getName());
cacheManager.removeCache(repositoryConfiguration.getName());
}
}
/**
* @return The configuration root directories for each repository.
*/
public File[] getConfigDirectories() {
return repositoriesDirectory.listFiles(new SventonConfigDirectoryFileFilter(getConfigurationFileName()));
}
/**
* @return The configuration root directories for each removed repository.
*/
public File[] getBackupConfigDirectories() {
return repositoriesDirectory.listFiles(new SventonConfigDirectoryFileFilter(getConfigurationFileBackupName()));
}
/**
* Initializes the caches by registering the cache enabled repositories in the cache managers.
*
* @throws CacheException if unable to register instances in the cache managers.
*/
public void initCaches() throws CacheException {
logger.info("Initializing caches");
for (final RepositoryConfiguration repositoryConfiguration : repositoryConfigurations.getAllConfigurations()) {
final RepositoryName repositoryName = repositoryConfiguration.getName();
if (repositoryConfiguration.isCacheUsed()) {
registerCacheManagers(cacheManagers, repositoryName);
} else {
logger.debug("Caches have not been enabled for repository: " + repositoryName);
}
}
logger.info("Caches initialized ok");
}
private void registerCacheManagers(final List<CacheManager> cacheManagers, final RepositoryName repositoryName) throws CacheException {
for (CacheManager manager : cacheManagers) {
if (!manager.isRegistered(repositoryName)) {
logger.debug("Registering [" + repositoryName.toString() + "] in [" + manager.getClass().getName() + "]");
manager.register(repositoryName);
}
}
}
/**
* Loads the repository configurations from the file at path
* {@code configurationRootDirectory / [repository name] / configurationFilename}
* <p/>
* If a config file is found and configuration is successful this repository will be marked as configured.
* If no file is found initialization will fail silently and the repository will not be marked as configured.
* <p/>
* It is legal to reload an already configured {@link RepositoryConfiguration} instance.
* {@code configurationRootDirectory} and {@code configurationFilename} must be set before calling this method, or bad
* things will most certainly happen...
*
* @param configDirectories Repository configuration directories.
* @param configurations Configurations instance to load configs into. If the instance is not empty, already existing
* repository configs with the same name will be overwritten.
* @throws IOException if IO error occur during file operations.
* @see #isConfigured()
*/
private void loadRepositoryConfigurations(final File[] configDirectories, RepositoryConfigurations configurations)
throws IOException {
for (final File configDir : configDirectories) {
InputStream is = null;
try {
final Properties properties = new Properties();
is = new FileInputStream(new File(configDir, getConfigurationFileName()));
properties.load(is);
final String repositoryName = configDir.getName();
logger.info("Loading repository config: " + repositoryName);
final RepositoryConfiguration configuration = RepositoryConfiguration.create(repositoryName, properties);
configuration.setPersisted();
configurations.add(configuration);
} finally {
IOUtils.closeQuietly(is);
}
}
if (hasConfigurations()) {
logger.info(getRepositoryConfigurationCount() + " repository configuration(s) loaded");
configured = true;
} else {
logger.warn("Configuration property file did exist but did not contain any configuration values");
}
}
/**
* Loads the repository configurations from the file at path
* {@code configurationRootDirectory / [repository name] / configurationFilename}
* <p/>
* If a config file is found and configuration is successful this repository will be marked as configured.
* If no file is found initialization will fail silently and the repository will not be marked as configured.
* <p/>
* It is legal to reload an already configured {@link RepositoryConfiguration} instance.
* {@code configurationRootDirectory} and {@code configurationFilename} must be set before calling this method, or bad
* things will most certainly happen...
*
* @param configDirectories Repository configuration directories.
* @throws IOException if IO error occur during file operations.
* @see #isConfigured()
*/
protected void loadRepositoryConfigurations(final File[] configDirectories) throws IOException {
loadRepositoryConfigurations(configDirectories, repositoryConfigurations);
}
/**
* Store the repository configurations on file at path
* {@code configurationRootDirectory / [repository name] / configurationFilename}.
* <p/>
* Note: Already stored configurations will be untouched.
*
* @throws IOException if IO error occur during file operations.
*/
public void persistRepositoryConfigurations() throws IOException {
for (final RepositoryConfiguration repositoryConfig : repositoryConfigurations.getAllConfigurations()) {
if (!repositoryConfig.isPersisted()) {
final File configDir = getConfigurationDirectoryForRepository(repositoryConfig.getName());
if (!configDir.exists() && !configDir.mkdirs()) {
throw new IOException("Unable to create directory: " + configDir.getAbsolutePath());
}
final File configFile = new File(configDir, getConfigurationFileName());
logger.info("Storing configuration: " + configFile.getAbsolutePath());
FileOutputStream fileOutputStream = null;
try {
fileOutputStream = new FileOutputStream(configFile);
final Properties configProperties = repositoryConfig.getAsProperties();
logger.debug("Storing properties: " + configProperties);
configProperties.store(fileOutputStream, "");
fileOutputStream.flush();
repositoryConfig.setPersisted();
} finally {
IOUtils.closeQuietly(fileOutputStream);
}
}
}
}
/**
* Adds a repository to the application.
*
* @param configuration The repository configuration to add.
*/
public void addConfiguration(final RepositoryConfiguration configuration) {
repositoryConfigurations.add(configuration);
}
/**
* Deletes a repository from the sventon configuration.
* The config file
*
* @param name Name of repository to delete from the sventon configuration.
*/
public void deleteConfiguration(final RepositoryName name) {
if (!repositoryConfigurations.containsConfiguration(name)) {
throw new IllegalArgumentException("Unknown repository name: " + name);
}
final RepositoryConfiguration configuration = repositoryConfigurations.getConfiguration(name);
if (configuration.isPersisted()) {
final File configFile = new File(getConfigurationDirectoryForRepository(name), getConfigurationFileName());
final File configBackupFile = new File(getConfigurationDirectoryForRepository(name), getConfigurationFileBackupName());
logger.info("Disabling repository configuration for [" + name.toString() + "]");
if (configFile.renameTo(configBackupFile)) {
logger.debug("Config file renamed to [" + configBackupFile.getAbsolutePath() + "]");
repositoryConfigurations.remove(name);
} else {
logger.error("Unable to rename config file: " + configFile.getAbsolutePath());
}
} else {
// Repository has not yet been stored and the user wants to delete it. Simply remove reference.
repositoryConfigurations.remove(name);
}
}
private String getConfigurationFileBackupName() {
return getConfigurationFileName() + "_bak";
}
protected void cleanupOldConfigDirectories(File[] configDirsToDelete) {
for (File configDir : configDirsToDelete) {
try {
logger.info("Deleting directory: " + configDir.getAbsolutePath());
FileUtils.deleteDirectory(configDir);
} catch (IOException ioe) {
logger.warn("Config directory delete failed.", ioe);
}
}
}
/**
* @param configurationFileName Path and file name of sventon configuration file, not {@code null}
*/
public void setConfigurationFileName(String configurationFileName) {
Validate.notNull(configurationFileName, "Config filename cannot be null");
this.configurationFileName = configurationFileName;
}
/**
* @return The configuration file name
*/
public String getConfigurationFileName() {
if (configurationFileName == null) {
throw new IllegalStateException("Configuration file name has not been set!");
}
return configurationFileName;
}
/**
* Gets the repository names, sorted alphabetically.
*
* @return Collection of repository names.
*/
public Set<RepositoryName> getRepositoryNames() {
return new TreeSet<RepositoryName>(repositoryConfigurations.getAllConfigurationNames());
}
/**
* Gets the repository configuration for given repository.
*
* @param name Repository name.
* @return Collection of repository names.
*/
public RepositoryConfiguration getConfiguration(final RepositoryName name) {
return repositoryConfigurations.getConfiguration(name);
}
/**
* Returns the number of configured repositories
* .
*
* @return Number of repositories.
*/
public int getRepositoryConfigurationCount() {
return repositoryConfigurations.count();
}
/**
* @return True if at least one repository configuration has been added.
*/
public boolean hasConfigurations() {
return getRepositoryConfigurationCount() > 0;
}
/**
* Gets configuration status of the sventon application.
*
* @return True if sventon is configured ok, false if not.
*/
public boolean isConfigured() {
return configured;
}
/**
* Checks if a repository is being updated.
*
* @param name Repository name.
* @return <tt>true</tt> if repository is being updated.
*/
public synchronized boolean isUpdating(final RepositoryName name) {
return updating.contains(name);
}
/**
* Sets the password for the config pages.
*
* @param configPassword Password.
*/
public void setConfigPassword(final String configPassword) {
this.configPassword = configPassword;
}
/**
* Checks if given password matches the configuration password.
*
* @param configPassword Password to match.
* @return True if password matches, false if not.
*/
public boolean isValidConfigPassword(final String configPassword) {
return this.configPassword != null && this.configPassword.equals(configPassword);
}
/**
* Sets the cache updating status.
* <p/>
* Note: This method is package protected by design.
*
* @param name Repository name.
* @param updating True or false.
*/
public synchronized void setUpdatingCache(final RepositoryName name, final boolean updating) {
if (updating) {
this.updating.add(name);
} else {
this.updating.remove(name);
}
}
/**
* Sets the configuration status of the sventon application.
*
* @param configured True to indicate sventon has been configured.
*/
public void setConfigured(final boolean configured) {
this.configured = configured;
}
/**
* Gets the configuration root directory for given repository name.
*
* @param repositoryName Name of repository to get config dir for.
* @return The config root dir.
*/
public File getConfigurationDirectoryForRepository(final RepositoryName repositoryName) {
return new File(repositoriesDirectory, repositoryName.toString());
}
/**
* Sets the cache managers.
*
* @param cacheManagers List of cache managers.
*/
@Autowired
public void setCacheManagers(final List<CacheManager> cacheManagers) {
this.cacheManagers.addAll(cacheManagers);
}
/**
* Enables or disables the possibility to edit the instance configuration.
*
* @param editableConfig True or false
*/
public void setEditableConfig(final boolean editableConfig) {
this.editableConfig = editableConfig;
}
/**
* @return true if the instance configuration is editable, false if not.
*/
public boolean isEditableConfig() {
return editableConfig;
}
/**
* @return {@code true} if it is possible to reload configurations from disk using a GET request to /config/reload.
*/
public boolean isConfigurationReloadSupported() {
return configurationReloadSupported;
}
/**
* Enables or disables the possibility to reload configurations from disk using a GET request to /config/reload.
*
* @param configurationReloadSupported True to enable reload support.
*/
public void setConfigurationReloadSupported(boolean configurationReloadSupported) {
this.configurationReloadSupported = configurationReloadSupported;
}
/**
* Gets the base URL to use for relative URL:s.
*
* @return The base URL or null if property <tt>sventon.baseURL</tt> was not set.
*/
public URL getBaseURL() {
String baseURL = StringUtils.trimToEmpty(System.getProperty(PROPERTY_KEY_SVENTON_BASE_URL));
if (!baseURL.isEmpty()) {
if (!baseURL.endsWith("/")) {
baseURL = baseURL + "/";
}
try {
return new URL(baseURL);
} catch (MalformedURLException e) {
logger.warn("Value of property '" + PROPERTY_KEY_SVENTON_BASE_URL + "' is not a valid URL: " + e.getMessage());
}
}
return null;
}
}