org.sventon.appl.Application.java Source code

Java tutorial

Introduction

Here is the source code for org.sventon.appl.Application.java

Source

/*
 * ====================================================================
 * 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;
    }
}