// DashboardConfig.java
//
// Created: March 2008
//
// Copyright 2008 Martin Senger
//
// 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.biomoby.service.dashboard;

import org.apache.commons.lang.BooleanUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.ArrayUtils;

import org.apache.commons.configuration.Configuration;
import org.apache.commons.configuration.SystemConfiguration;
import org.apache.commons.configuration.PropertiesConfiguration;
import org.apache.commons.configuration.FileConfiguration;
import org.apache.commons.configuration.CompositeConfiguration;
import org.apache.commons.configuration.ConfigurationException;
import org.apache.commons.configuration.reloading.FileChangedReloadingStrategy;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.Properties;
import java.util.Set;
import java.util.HashSet;
import java.io.File;

/**
 * An abstract class giving access to the global Dashboard
 * configuration. It also allows to add additional configuration files
 * (which may be useful for new panels). <p>
 *
 * The class uses <a
 * href="http://jakarta.apache.org/commons/configuration/">Apache
 * Commons Configuration</a> - the method {@link #get} gives back
 * directly an Apache's <tt>CompositeConfiguration</tt> instance
 * allowing to fine-tune your configuration (if needed, at all). <p>
 *
 * @author <A HREF="mailto:martin.senger@gmail.com">Martin Senger</A>
 * @version $Id: DashboardConfig.java,v 1.2 2008/11/18 06:40:11 senger Exp $
 */

public abstract class DashboardConfig {

    private static org.apache.commons.logging.Log log =
       org.apache.commons.logging.LogFactory.getLog (DashboardConfig.class);

    private static CompositeConfiguration config;
    private static Set<String> configFilenames = new HashSet<String>();

    /**
     * A filename indicating a file containing Dasboard run-time
     * properties. This file name can be changed by setting Java
     * property {@link #PROP_DASHBOARD_CONFIGURATION} to point to a
     * different file name.
     */
    public static final String DASHBOARD_CONFIG_FILENAME = "dashboard.properties";

    /**
     * A property name. Its value contains a filename indicating a
     * file containing Dashboard configuration properties. Default
     * value is {@link #DASHBOARD_CONFIG_FILENAME}.
     */
    public static final String PROP_DASHBOARD_CONFIGURATION = "dashboard.configuration";


    /**************************************************************************
     * The main method returning a configuration object. The returned
     * object is a singleton - but its contents (the properties it
     * carries) can be changed dynamically (the properties are
     * reloaded if their files are modified). <p>
     *
     * The configuration object contains all Java system properties
     * and properties read from a Dashboard configuration file. This
     * file name is given by the system property {@link
     * #PROP_DASHBOARD_CONFIGURATION}, or using a default name {@link
     * #DASHBOARD_CONFIG_FILENAME}. If the filename does not specify
     * an absolute path, the file will be searched automatically in
     * the following locations:
     *
     * <ul>
     *  <li> in the current directory,
     *  <li> in the user home,
     *  <li> directory in the classpath
     * </ul><p>
     *
     * The System properties take precedence over properties read from
     * the Dashboard property file. <p>
     *
     * The configuration object can be anytime extended by properties
     * from other sources by using either methods defined in the
     * <tt>CompositeConfiguration</tt>'s API, or using a convenient
     * method {@link #addConfigPropertyFile} defined here. Properties
     * from these additional files have higher priority than
     * properties added earlier - except System properties, they are
     * always the most prioritized. <p>
     *
     * @return a configuration object
     **************************************************************************/
    public static synchronized CompositeConfiguration get() {
	if (config == null) {

	    // set the Dashboard global configuration
	    CompositeConfiguration cfg = new CompositeConfiguration();

	    // first, add System properties
	    cfg.addConfiguration (new SystemConfiguration());

	    // second, add properties from the 'build.properties' file
	    addPropertiesConfiguration (cfg, "build.properties", false);

	    // third, add Dashboard properties file(s)
	    String[] configFilenames = cfg.getStringArray (PROP_DASHBOARD_CONFIGURATION);
	    if (configFilenames == null || configFilenames.length == 0)
		configFilenames = new String[] { DASHBOARD_CONFIG_FILENAME };
	    for (int i = 0; i < configFilenames.length; i++) {
		log.info ("Using configuration file: " + configFilenames[i]);
		addPropertiesConfiguration (cfg, configFilenames[i], true);
	    }

	    // keep it for other calls
	    config = cfg;
	}
	return config;
    }

    /**************************************************************************
     * It returns all existing configurations from the main composite
     * configuration 'config', while removing them also from this
     * 'config'. This is a step needed before inserting a new
     * configuration,
     **************************************************************************/
    private static ArrayList<Configuration> getAndClearCurrentConfigurations() {
	get();
	ArrayList<Configuration> cfgs = new ArrayList<Configuration>();

	// keep there always the first (System properties) and the
	// last (inMemoryConfiguration) configuration
	while (config.getNumberOfConfigurations() > 2) {
	    Configuration cfg = config.getConfiguration (1);
	    cfgs.add (cfg);
	    config.removeConfiguration (cfg);
	}
	return cfgs;
    }

    /**************************************************************************
     *
     **************************************************************************/
    private static boolean isExistingConfig (FileConfiguration cfg) {
	if (cfg.getFile() == null)
	    return false;
	String filename = cfg.getFile().getAbsolutePath();
	if (configFilenames.contains (filename))
	    return true;
	configFilenames.add (filename);
	return false;
    }

    /**************************************************************************
     * Add given property files as a new configuration to 'cfg'
     * composite configuration. Return true on success. If
     * 'errorsEnabled' it also logs an error if the file cannot be
     * found.
     **************************************************************************/
    private static boolean addPropertiesConfiguration (CompositeConfiguration cfg,
						       String configFilename,
						       boolean errorsEnabled) {
	try {
	    PropertiesConfiguration propsConfig =
		new PropertiesConfiguration (configFilename);
	    if (isExistingConfig (propsConfig))
		return true;
	    propsConfig.setReloadingStrategy (new FileChangedReloadingStrategy());
	    cfg.addConfiguration (propsConfig);
	    return true;
	} catch (ConfigurationException e) {
	    if (errorsEnabled) {
		log.error ("Loading properties configuration from '" +
			   configFilename + "' failed: " +
			   e.getMessage());
	    }
	    return false;
	}
    }

    /**************************************************************************
     * Add new configuration properties from a property file. <p>
     *
     * The newly added properties have higher priority than properties
     * added earlier - except System properties, they are always the
     * most prioritized. <p>
     *
     * @param configFilename is a filename indicating a file
     * (formatted as a Java properties file) with new properties (see
     * {@link #get} explaining where is this file looked for)

     * @return true if 'configFilename' is successfully added, false
     * otherwise (in which case the cause is recorded in the log)
     **************************************************************************/
    public static synchronized boolean addConfigPropertyFile (String configFilename) {

	log.info ("Adding property configuration file: " + configFilename);
	ArrayList<Configuration> cfgs = getAndClearCurrentConfigurations();
	boolean success = addPropertiesConfiguration (get(), configFilename, true);
	for (Configuration cfg: cfgs) {
	    get().addConfiguration (cfg);
	}
	return success;
    }

    /**************************************************************************
     * Get a string associated with the given configuration key, or -
     * if not found - get the given default value (which still may be
     * null). <p>
     *
     * It is a convenient way to say:
     *
     *<pre>
     * String value = Config.get().getString ("my.property", "yes");
     *</pre>
     *
     * @param key is a property name
     * @param defaultValue used if the 'key' cannot be found
     *
     * @return the property value, or 'defaultValue' if such property
     * does not exist
     **************************************************************************/
    public static String getString (String key, String defaultValue) {
	return get().getString (key, defaultValue);
    }

    /**************************************************************************
     * Almost the same functionality as {@link #getString getString}
     * method. The different is the return value: this method allows
     * to return several values of the same property. In the
     * configuration file, a property can be repeated, or can have
     * several comma-separated values. <p>
     *
     * By the way, the importance of a comma in a property value also
     * means that any 'normal' (the one not meant as a value
     * separator) commas in property values, must be escaped by
     * backslashes. <p>
     *
     * @return all values of the given property, or - if such property
     * does not exist - return a one-element array with the
     * 'defaultValue' unless the 'defaultValue' is also null in which
     * case return an empty array
     **************************************************************************/
    public static String[] getStrings (String key,
				       String defaultValue) {
	String[] values = get().getStringArray (key);
	if (values.length > 0) return values;

	if (defaultValue == null)
	    return ArrayUtils.EMPTY_STRING_ARRAY;
	else
	    return new String[] { defaultValue };
    }

    /**************************************************************************
     * Get an integer value associated with the given configuration
     * key, or - if not found or not of integer value - get the given
     * default value. <p>
     *
     * @param key is a property name
     * @param defaultValue used if the 'key' cannot be found, or if it
     * is not an integer
     *
     * @return the property value, or 'defaultValue' if such property
     * does not exist
     **************************************************************************/
    public static int getInt (String key, int defaultValue) {
	String strValue = getString (key, ""+defaultValue);
	try {
	    return Integer.decode (strValue).intValue();
	} catch (NumberFormatException e) {
	    return defaultValue;
	}
    }

    /**************************************************************************
     * Get a boolean value associated with the given configuration
     * key, or - if not found - get the given default value. <p>
     *
     * @param key is a property name
     * @param defaultValue used if the 'key' cannot be found, or if
     * its value is empty
     *
     * @return true for the property values 'true', 'on', 'yes' (case
     * insensitive), and for an existing property with an empty
     * value. For other values, return false. If the property does not
     * exist at all, the 'defaultValue' is returned.
     **************************************************************************/
    public static boolean isEnabled (String key, boolean defaultValue,
				     String serviceName, Object owner) {
	String strValue = getString (key, null);
	if (strValue == null)
	    return defaultValue;
	if (StringUtils.isBlank (strValue))
	    return true;
	return BooleanUtils.toBoolean (strValue);
    }

//     /**************************************************************************
//      * Not needed, at the moment. Uncomment it if needed...
//      **************************************************************************/
//     public static File[] getConfigFiles() {
// 	get();
// 	ArrayList<File> files = new ArrayList<File>();

// 	for (int i = 0; i < config.getNumberOfConfigurations(); i++) {
// 	    Configuration cfg = config.getConfiguration (i);
// 	    if (cfg instanceof FileConfiguration) {
// 		files.add ( ((FileConfiguration)cfg).getFile() );
// 	    }
// 	}
// 	return files.toArray (new File[] {});
//     }

}
