/**
 * JOnAS: Java(TM) Open Application Server
 * Copyright (C) 1999-2009 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;

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

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.util.ConfigurationConstants;
import org.ow2.jonas.properties.ServerProperties;
import org.ow2.jonas.properties.ServiceProperties;
import org.ow2.util.log.Log;
import org.ow2.util.log.LogFactory;

/**
 * JOnAS Configuration component.
 * @author Francois Fornaciari
 * Contributor(s): Jeremy Cazaux (support to ServiceProperties)
 */
public class JOnASConfiguration implements ConfigurationManager, ConfigurationPlugin {

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

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

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

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

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

    /**
     * JOnAS Server properties.
     */
    private JProp serverProperties = null;

    /**
     * Last modification date of the jonas.properties file.
     */
    private long lastModified;

    /**
     * List of {@link ServiceProperties}
     */
    private List<ServiceProperties> servicePropertiesList;

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

        // Initialize mandatory services list
        mandatoryServices = new ArrayList<String>();

        // Registry is always mandatory and part of the autodeploy bundles
        mandatoryServices.add("registry");

        // Security is always mandatory and part of the autodeploy bundles
        // See JIRA issues JONAS-224 and JONAS-280
        mandatoryServices.add("security");

        // JMX is always mandatory and part of the autodeploy bundles
        mandatoryServices.add("jmx");

        // The discovery service must be added to mandatory services
        if (getDeclaredServices().contains("discovery")) {
            mandatoryServices.add("discovery");
        }

        //Initialize the list of ServiceProperties
        this.servicePropertiesList = new ArrayList<ServiceProperties>();
    }

    /**
     * Method called when the Configuration Admin Service is bound to the component. <br />
     * This method creates service configurations for mandatory JOnAS services (regitry and jmx)
     * @throws Exception Thrown if the configuration failed.
     */
    public void configure() throws Exception {
        // Delete all service configurations
        deleteServiceConfigurations();

        // Create service configurations for mandatory services only
        manageServiceConfigurations(mandatoryServices, 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(), getServerProperties(), null);
        }
    }

    /**
     * {@inheritDoc}
     */
    public void haltServer() throws Exception {
        // Stop the system bundle
        bc.getBundle(0).stop();
    }

    /**
     * {@inheritDoc}
     */
    public synchronized void updateServiceConfiguration(final String service) throws Exception {
        List<String> services = new ArrayList<String>();
        services.add(service);
        manageServiceConfigurations(services, false);
    }

    /**
     * {@inheritDoc}
     */
    public synchronized void deleteServiceConfiguration(final String service) throws Exception {
        List<String> services = new ArrayList<String>();
        services.add(service);
        manageServiceConfigurations(services, true);
    }

    /**
     * {@inheritDoc}
     */
    public boolean matchService(final String service) {
        String factoryPID = getFactoryPID(service);
        return (factoryPID != null);
    }

    /**
     * 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)) {

            // Get the service properties
            Dictionary<String, String> newProperties = null;

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

            // Get the configuration if exist
            Configuration configuration = getConfiguration(service);

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

                // Update the configuration
                configuration.update(newProperties);
            } else if (configuration != null) {
                // A configuration already exists
                configuration = configurationAdmin.getConfiguration(configuration.getPid(), null);

                if (delete) {
                    configuration.delete();
                } else {
                    // Look if service properties have been changed
                    Dictionary<String, String> storedProperties = configuration.getProperties();
                    if (isConfigurationModified(storedProperties, newProperties)) {
                        // Update the configuration
                        configuration.update(newProperties);
                    }
                }
            }
        }
    }

    /**
     * Get mandatory services.
     * @return Mandatory services
     */
    public List<String> getMandatoryServices() {
        return mandatoryServices;
    }

    /**
     * Get all services.
     * @return All services
     */
    public 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
     */
    public List<String> getOptionalServices() {
        List<String> optionalServices = new ArrayList<String>();

        // Add optional services ignoring mandatory services
        for (String declaredService : getDeclaredServices()) {
            if (!mandatoryServices.contains(declaredService)) {

                //only add depmonitor,wc and wm optional services. Other optionnal services should be addons services.
                // They should be started by the addon deployer (not by the ServiceManager or by the JEEServer)
                if (declaredService.equals("depmonitor") || declaredService.equals("wm") || declaredService.equals("wc")) {
                    optionalServices.add(declaredService);
                }
            }
        }

        return optionalServices;
    }

    /**
     * Get list of all declared services.
     * @return Declared services
     */
    private List<String> getDeclaredServices() {
        String[] declaredServices = getServerProperties().getValueAsArray("jonas.services");
        return Arrays.asList(declaredServices);
    }

    /**
     * Get all properties for a given JOnAS service name.
     * @param service The given service name
     * @return A <code>Dictionary</code> value representing all properties for a JOnAS service name
     */
    public Dictionary getServiceProperties(final String service) {
        JProp serverProperties = getServerProperties();
        Dictionary<String, String> props = getServicesProperties(serverProperties.getConfigFileEnv(), service);

        if (props.isEmpty()) {
            int i = 0;
            while (i < servicePropertiesList.size() && props.isEmpty()) {
                ServiceProperties serviceProperties = servicePropertiesList.get(i);
                if (serviceProperties.getService().equals(service)) {
                    props = getServicesProperties(serviceProperties.getProperties(), service);
                }
                i++;
            }
        }

        // Add static properties
        props.put(JONAS_SERVICE, service);
        return props;
    }

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

    /**
     * Return a JOnAS service configuration or null if the configuration doesn't exist.
     * @param service The given service name
     * @return The JOnAS service configuration
     * @throws Exception If the service configuration cannot be retrieved
     */
    private Configuration getConfiguration(final String service) throws Exception {
         // Get the list of configurations for a factory
        Configuration[] storedConfigurations = configurationAdmin.listConfigurations("("
                + ConfigurationAdmin.SERVICE_FACTORYPID + "=" + getFactoryPID(service) + ")");

        if (storedConfigurations != null && storedConfigurations.length == 1) {
            return storedConfigurations[0];
        }

        return null;
    }

    /**
     * Return the JOnAS service configuration factory PID.
     * @param service The given service name
     * @return The JOnAS service configuration factory PID
     */
    private String getFactoryPID(final String service) {
        String serviceClass = JONAS_SERVICE + "." + service + ".class";
        String factoryPID = getServerProperties().getValue(serviceClass);

        if (factoryPID == null) {
            for (ServiceProperties serviceProperties: this.servicePropertiesList) {
                if (serviceProperties.getService().equals(service)) {
                    return serviceProperties.getValue(serviceClass);
                }
            }
        }
        return factoryPID;
    }

    /**
     * {@inheritDoc}
     */
    @SuppressWarnings("unchecked")
    public void modifyConfiguration(final ServiceReference serviceReference, final Dictionary storedProperties) {
        try {
            String serviceName = (String) storedProperties.get(JONAS_SERVICE);
            if (serviceName != null) {
                // Get the last service properties
                Dictionary<String, String> newProperties = getServiceProperties(serviceName);

                // If the configuration for the service has changed
                if (isConfigurationModified(storedProperties, newProperties)) {
                    // Set new properties
                    for (Enumeration<?> enProps = newProperties.keys(); enProps.hasMoreElements();) {
                        String key = (String) enProps.nextElement();
                        storedProperties.put(key, newProperties.get(key));
                    }
                }
            }
        } catch (Exception e) {
            logger.error(e);
        }
    }

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

    /**
     * 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, String> storedProperties,
            final Dictionary<String, String> 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;
    }

    /**
     * Delete service configurations.
     * @throws Exception If an exception occurs during configuration deletions
     */
    private void deleteServiceConfigurations() throws Exception {
        // Get all configurations
        Configuration[] storedConfigurations = configurationAdmin.listConfigurations(null);

        if (storedConfigurations != null) {
            // Delete configurations
            for (Configuration configuration : storedConfigurations) {
                String serviceName = (String) configuration.getProperties().get(JONAS_SERVICE);
                if (serviceName != null) {
                    configuration.delete();
                }
            }
        }
    }

    /**
     * Returns JOnAS Server properties. Read jonas.properties file only if modified.
     * @return Returns JOnAS Server properties.
     */
    private JProp getServerProperties() {
        File conf = new File(JProp.getJonasBase(), ConfigurationConstants.DEFAULT_CONFIG_DIR);
        File jonasPropertyFile = new File(conf, ConfigurationConstants.JONAS_PROPERTIES_PROP);
        long currentLastModified = jonasPropertyFile.lastModified();

        if (currentLastModified != this.lastModified) {
            this.serverProperties = new JProp();
            this.lastModified = currentLastModified;
        }

        return this.serverProperties;
    }

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

    /**
     * @param serviceProperties The {@link ServiceProperties} to add
     */
    public void addServiceProperties(ServiceProperties serviceProperties) {
        this.servicePropertiesList.add(serviceProperties);
    }

    /**
     * @param serviceProperties The {@link ServiceProperties} to remove
     */
    public void removeServiceProperties(ServiceProperties serviceProperties) {
        this.servicePropertiesList.remove(serviceProperties);
    }
}
