/**
 * JOnAS: Java(TM) Open Application Server
 * Copyright (C) 1999-2008 Bull S.A.S.
 * Contact: jonas-team@ow2.org
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
 * USA
 *
 * --------------------------------------------------------------------------
 * $Id:RegistryServiceImpl.java 10372 2007-05-14 13:58:42Z sauthieg $
 * --------------------------------------------------------------------------
 */

package org.ow2.jonas.services.bootstrap.osgi;

import java.io.File;
import java.util.ArrayList;
import java.util.Dictionary;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.LinkedList;
import java.util.List;

import org.osgi.framework.BundleContext;
import org.osgi.framework.Constants;
import org.osgi.framework.ServiceReference;
import org.osgi.service.cm.Configuration;
import org.osgi.service.cm.ConfigurationAdmin;
import org.osgi.service.cm.ConfigurationPlugin;
import org.ow2.jonas.configuration.ConfigurationManager;
import org.ow2.jonas.lib.bootstrap.JProp;
import org.ow2.jonas.lib.jmbeans.ServiceManagerUpdater;
import org.ow2.jonas.properties.ServerProperties;
import org.ow2.util.log.Log;
import org.ow2.util.log.LogFactory;

/**
 * JOnAS Configuration component.
 * @author Francois Fornaciari
 */
public class JOnASConfiguration implements ConfigurationManager, ConfigurationPlugin {

    /**
     * Reference to the {@link ConfigurationAdmin} service.
     */
    private ConfigurationAdmin configurationAdmin = null;

    /**
     * Property name for the last modification.
     */
    private static final String DATE = "date";

    /**
     * Property name for the service name.
     */
    private static final String JONAS_SERVICE = "jonas.service";

    /**
     * Property name for the bootstrap mode.
     */
    private static final String JONAS_BOOTSTRAP = "jonas.bootstrap";

    /**
     * Property name for the server configuration.
     */
    private static final String SERVER_CONFIG = "org.ow2.jonas.server";

    /**
     * {@link ServerProperties} exposed as an OGGi service.
     */
    private JProp serverProperties = null;

    /**
     * OSGi {@link BundleContext}.
     */
    private BundleContext bc = null;

    /**
     * Mandatory JOnAS services.
     */
    private List<String> mandatoryServices = null;

    /**
     * JOnAS runnable state. While the optional service configuration updates have not been done, it isn't.
     */
    private boolean isRunnable = false;

    /**
     * Management loggers.
     */
    private Log logger = LogFactory.getLog(JOnASConfiguration.class);

    /**
     * ServiceManager updater.
     */
    private ServiceManagerUpdater serviceManagerUpdater = null;

    /**
     * Constructor used to retrieve the OSGi BundleContext.
     * @param bc OSGi BundleContext
     */
    public JOnASConfiguration(final BundleContext bc) {
        this.bc = bc;

        loadServerProperties();

        // Initialize mandatory services list
        mandatoryServices = new ArrayList<String>();
        mandatoryServices.add("registry");
        mandatoryServices.add("jmx");
    }

    /**
     * Method called when the Configuration Admin Service is bound to the component. <br />
     * This method creates service configurations if they don't already exist or updates it if the JOnAS configuration
     * file has changed since the last modification.
     * @throws Exception Thrown if the configuration creation failed.
     */
    public void configure() throws Exception {
        boolean boostrapMode = Boolean.getBoolean(JONAS_BOOTSTRAP);

        if (boostrapMode) {
            // Delete existant configurations for optional services
            manageServiceConfigurations(getOptionalServices(), true);
        }

        if (doUpdateServiceConfigurations()) {
            // Delete old service configurations
            deleteOldConfigurations();

            // Create only configurations for mandatory services if we are in bootstrap mode
            if (boostrapMode) {
                // Create service configurations for mandatory services only
                manageServiceConfigurations(mandatoryServices, false);
            } else {
                // Create service configurations for all services
                manageServiceConfigurations(getAllServices(), false);
            }
        }

        // The ServerProperties service must be registered after the configuration updates
        // to avoid that services are started before they are configured
        ServiceReference sr = bc.getServiceReference(ServerProperties.class.getName());
        if (sr == null) {
            bc.registerService(ServerProperties.class.getName(), serverProperties, null);
        }
    }

    /**
     * {@inheritDoc}
     */
    public synchronized void updateServiceConfigurations() throws Exception {
        if (doUpdateServiceConfigurations() || !isRunnable) {
            manageServiceConfigurations(getOptionalServices(), false);
            // The server is in a runnable state
            isRunnable = true;
        }
    }

    /**
     * {@inheritDoc}
     */
    public synchronized void deleteServiceConfigurations() throws Exception {
        manageServiceConfigurations(reverseList(getOptionalServices()), true);
        // The server is no more in a runnable state
        isRunnable = false;
    }

    /**
     * {@inheritDoc}
     */
    public void haltServer() throws Exception {
        // Get the server configuration and delete it
        Configuration serverConfiguration = configurationAdmin.getConfiguration(SERVER_CONFIG);
        serverConfiguration.delete();

        // Stop the system bundle
        bc.getBundle(0).stop();
    }

    /**
     * {@inheritDoc}
     */
    public synchronized void updateServiceConfigurations(final List<String> services) throws Exception {
        manageServiceConfigurations(services, false);
    }

    /**
     * {@inheritDoc}
     */
    public synchronized void deleteServiceConfigurations(final List<String> services) throws Exception {
        manageServiceConfigurations(services, true);
    }

    /**
     * Get 'true' if service configurations have to be updated, ie the server configuration don't already exist or the
     * file jonas.properties has not been modified since the last configuration update.
     * @return a <code>boolean</code> value to determine if service configurations have to be updated
     * @throws Exception Thrown either during the configuration creation or the configuration update
     */
    @SuppressWarnings("unchecked")
    private boolean doUpdateServiceConfigurations() throws Exception {
        // Get the server configuration
        Configuration serverConfiguration = configurationAdmin.getConfiguration(SERVER_CONFIG);
        // Get the server properties
        Dictionary<String, Object> serverProps = serverConfiguration.getProperties();

        // Init the server properties
        if (serverProps == null) {
            serverProps = new Hashtable<String, Object>();

            // Update the server configuration with the last modification time
            serverProps.put(DATE, Long.valueOf(getLastModification()));
            serverConfiguration.update(serverProps);

            return true;
        } else {
            // Get time of the last modification of the jonas.properties file stored during the configuration update
            long currentModification = (Long) serverProps.get(DATE);
            // Time of the last modification of the jonas.properties file
            long lastModification = getLastModification();

            // If times are different, JOnAS properties file contains more recent information than the stored
            // OSGi configuration
            if (currentModification != lastModification) {
                loadServerProperties();

                // TODO : find a better place to update the ServiceManager
                if (serviceManagerUpdater != null) {
                    serviceManagerUpdater.updateServiceManager(serverProperties);
                }

                // Update the server configuration with the last modification time
                serverProps.put(DATE, lastModification);
                serverConfiguration.update(serverProps);

                return true;
            }
        }
        return false;
    }

    /**
     * Create, update or delete service configurations for the given services.
     * @param services List of services to configure
     * @param delete 'true' when configurations have to be delete
     * @throws Exception If an exception occurs during configuration updates or deletions
     */
    @SuppressWarnings("unchecked")
    private void manageServiceConfigurations(final List<String> services, final boolean delete) throws Exception {
        for (String service : removeDuplicateServices(services)) {
            boolean mandatory = mandatoryServices.contains(service);
            String serviceClass = JONAS_SERVICE + "." + service + ".class";
            String factoryPID = serverProperties.getValue(serviceClass);

            // Verify that the service match a configuration
            if (factoryPID != null) {
                // Get the service properties
                Dictionary<String, Object> newProperties = null;

                if (!delete) {
                    newProperties = getServiceProperties(service);

                    // Add static properties
                    newProperties.put(JONAS_SERVICE, service);
                    newProperties.put("jonas.service.mandatory", mandatory);
                }

                // Get the list of configurations for a factory
                Configuration[] storedConfigurations = configurationAdmin.listConfigurations("("
                        + ConfigurationAdmin.SERVICE_FACTORYPID + "=" + factoryPID + ")");

                Configuration configuration = null;

                // There is no stored configuration for this factory
                if (!delete && storedConfigurations.length == 0) {
                    // Create a configuration for this factory PID
                    configuration = configurationAdmin.createFactoryConfiguration(factoryPID, null);

                    // Update the configuration
                    configuration.update(newProperties);
                } else if (storedConfigurations.length == 1) {
                    // A configuration already exists
                    configuration = configurationAdmin.getConfiguration(storedConfigurations[0].getPid());

                    if (delete) {
                        configuration.delete();
                    } else {
                        // Look if service properties have been changed
                        Dictionary<String, Object> storedProperties = configuration.getProperties();
                        if (isConfigurationModified(storedProperties, newProperties)) {
                            // Update the configuration
                            configuration.update(newProperties);
                        }
                    }
                }
            } else {
                logger.error("Missing configuration for ''{0}'' service in jonas.properties.", service);
            }
        }
    }

    /**
     * Remove duplicate service names.
     * @param services list of services to analyze
     * @return A list of distinct service names
     */
    private List<String> removeDuplicateServices(final List<String> services) {
        List<String> result = new LinkedList<String>();
        for (String service : services) {
            if (!result.contains(service)) {
                result.add(service);
            } else {
                logger.warn("Service ''{0}'' declared many times in jonas.properties", service);
            }
        }
        return result;
    }

    /**
     * Get all services.
     * @return All services
     */
    private List<String> getAllServices() {
        List<String> allServices = getOptionalServices();

        // Add mandatory services
        for (int i = 0; i < mandatoryServices.size(); i++) {
            allServices.add(i, mandatoryServices.get(i));
        }

        return allServices;
    }

    /**
     * Get optional services.
     * @return Optional services
     */
    private List<String> getOptionalServices() {
        // Get list of all declared services
        String[] declaredServices = serverProperties.getValueAsArray("jonas.services");
        List<String> optionalServices = new ArrayList<String>();

        // Add optional services ignoring mandatory services
        for (String declaredService : declaredServices) {
            if (!mandatoryServices.contains(declaredService)) {
                optionalServices.add(declaredService);
            }
        }

        return optionalServices;
    }

    /**
     * Get 'true' if the two dictionaries don't contain the same properties.
     * @param storedProperties Stored properties
     * @param newProperties New properties for JOnAS properties
     * @return A <code>boolean</code> value representing the fact that the two dictionaries contain or not the same
     *         properties
     */
    private boolean isConfigurationModified(final Dictionary<String, Object> storedProperties,
                                            final Dictionary<String, Object> newProperties) {
        // Remove automatic properties added by the Configuration Admin service
        storedProperties.remove(Constants.SERVICE_PID);
        storedProperties.remove(ConfigurationAdmin.SERVICE_FACTORYPID);
        storedProperties.remove(ConfigurationAdmin.SERVICE_BUNDLELOCATION);

        if (storedProperties.size() != newProperties.size()) {
            return true;
        } else {
            for (Enumeration<String> enProps = storedProperties.keys(); enProps.hasMoreElements();) {
                String key = enProps.nextElement();

                if (newProperties.get(key) == null) {
                    return true;
                } else if (!storedProperties.get(key).equals(newProperties.get(key))) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * Get all properties for a given service name.
     * @param service Service name
     * @return A <code>Dictionary</code> value representing all properties for a given service name
     */
    private Dictionary<String, Object> getServiceProperties(final String service) {
        Dictionary<String, Object> props = new Hashtable<String, Object>();
        for (Object key : serverProperties.getConfigFileEnv().keySet()) {
            String name = (String) key;
            String servicePrefix = JONAS_SERVICE + "." + service + ".";
            if (name.startsWith(servicePrefix)) {
                String value = serverProperties.getValue(name);
                name = name.substring(servicePrefix.length());
                props.put(name, value);
            }
        }
        return props;
    }

    /**
     * Returns the time that the jonas.properties file was last modified.
     * @return A <code>long</code> value representing the time the file was last modified
     */
    private long getLastModification() {
        File conf = new File(JProp.getJonasBase(), "conf");
        File jonasPropertyFile = new File(conf, "jonas.properties");
        return jonasPropertyFile.lastModified();
    }

    /**
     * Returns the reversed list of the give list.
     * @param list the given list
     * @return the reversed list
     */
    private List<String> reverseList(final List<String> list) {
        List<String> reversedList = new ArrayList<String>();
        for (String element : list) {
            reversedList.add(0, element);
        }
        return reversedList;
    }

    /**
     * Delete stored configurations no more present in the jonas.services list.
     * @throws Exception If an exception occurs during configuration deletions
     */
    private void deleteOldConfigurations() throws Exception {
        // Get all configurations
        Configuration[] storedConfigurations = configurationAdmin.listConfigurations(null);
        // Delete configurations
        for (Configuration configuration : storedConfigurations) {
            String serviceName = (String) configuration.getProperties().get(JONAS_SERVICE);
            if (!configuration.getPid().equals(SERVER_CONFIG) && !getAllServices().contains(serviceName)) {
                configuration.delete();
            }
        }
    }

    /**
     * Load server properties.
     */
    private void loadServerProperties() {
        // Read the JOnAS properties
        serverProperties = new JProp();
    }

    /**
     * {@inheritDoc}
     */
    @SuppressWarnings("unchecked")
    public void modifyConfiguration(final ServiceReference serviceReference, final Dictionary storedProperties) {
        try {
            if (doUpdateServiceConfigurations()) {
                String serviceName = (String) storedProperties.get(JONAS_SERVICE);

                // Get the last service properties
                Dictionary<?, ?> newProperties = getServiceProperties(serviceName);

                // Set new properties
                for (Enumeration<?> enProps = newProperties.keys(); enProps.hasMoreElements();) {
                    String key = (String) enProps.nextElement();
                    storedProperties.put(key, newProperties.get(key));
                }

                manageServiceConfigurations(getAllServices(), false);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * @param configurationAdmin the configurationAdmin to set
     */
    public void setConfigurationAdmin(final ConfigurationAdmin configurationAdmin) {
        this.configurationAdmin = configurationAdmin;
    }

    /**
     * @param serviceManagerUpdater the serviceManagerUpdater to set
     */
    public void bindServiceManagerUpdater(final ServiceManagerUpdater serviceManagerUpdater) {
        this.serviceManagerUpdater = serviceManagerUpdater;
    }

    /**
     * @param serviceManagerUpdater the serviceManagerUpdater to unset
     */
    public void unbindServiceManagerUpdater(final ServiceManagerUpdater serviceManagerUpdater) {
        this.serviceManagerUpdater = null;
    }
}
