/**
 * JOnAS: Java(TM) Open Application Server
 * Copyright (C) 1999-2004 Bull S.A.
 * Contact: jonas-team@objectweb.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$
 * --------------------------------------------------------------------------
 */


package org.objectweb.jonas.service;

import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;

import javax.naming.Context;
import javax.naming.NamingException;

import org.objectweb.jonas.common.JProp;
import org.objectweb.jonas.common.Log;
import org.objectweb.jonas.naming.CompNamingContext;

import org.objectweb.util.monolog.api.BasicLevel;
import org.objectweb.util.monolog.api.Logger;

/**
 * This class implements a manager of org.objectweb.jonas.service.Service for JOnAS.
 *
 * The services are defined in the jonas.properties files
 *  - 'jonas.services' property defines the list of the services,
 *  - for each service
 *     - 'jonas.service.<service-name>.class' property defines the class of the
 *       implementation of the service,
 *     - 'jonas.service.<service-name>.XXXX' properties define the configuration
 *       of the service
 *
 * @author Helene Joanin
 * Contributor(s):
 * 01/06/15: Regis Le Brettevillois - Libelis
 * 02/06   : Florent Benoit & Ludovic Bert : Ear service/Web Container service
 * Philippe Durieux
 * 03/01/14 Adriana Danes : pass the entire property name (jonas.service.<service-name>.XXXX)
 *     in the configuration context at service creation time
 */

public class ServiceManager {

    /**
     * Services of JOnAS
     */
    static final String SERVICES_PROP_NAME = JProp.JONASPREFIX + ".services";

    /**
     * A JOnAS service
     */
    static final String PREFIX_SERVICE_PROP_NAME = JProp.JONASPREFIX + ".service";

    /**
     * The logger used.
     */
    private static Logger logger = null;

    /**
     * The ServerManager singleton value
     */
    private static ServiceManager unique = null;

    /**
     * JOnAS properties
    */
    private JProp props = null;

    /**
     * The list of the managed services
     */
    private static List services = null;

    /**
     * The managed services and their names
     */
    private Map servicesByName = null;

    /**
     * The Configuration context for each managed service
     */
    private Map contextsByService = null;

    /**
     * Private constructor to force only 1 instance.
     * @throws Exception if the ServiceManager can't be built.
     */
    private ServiceManager() {
        logger = Log.getLogger(Log.JONAS_SERVER_PREFIX);
        // read properties from the different jonas.properties files
        props = JProp.getInstance();
        // read all services from properties
        readServices();
    }

    /**
     * Get the unique instance.
     * Create it at first call.
     * @return ServiceManager the unique instance
     * @throws Exception if the ServiceManager can't be built.
     */
    public static ServiceManager getInstance() {
        if (unique == null) {
            unique = new ServiceManager();
        }
        return unique;
    }

    /**
     * Get the service which have the given name
     * @param name the given name
     * @return the service which have the given name
     * @throws ServiceException if the service can't be retrieved.
     */
    public Service getService(String name) throws ServiceException {
        if (servicesByName == null) {
            throw new ServiceException("No service has been initialized yet");
        }
        Service s = (Service) servicesByName.get(name);
        if (s == null) {
            throw new ServiceException("Unknown service '" + name + "'");
        }
        return s;
    }

    /**
     * Return the EJB service (service's name: 'ejb')
     * @return the EJB service.
     * @throws ServiceException if can not get the ejb service.
     */
    public Service getEjbService() throws ServiceException {
        return getService("ejb");
    }

    /**
     * Return the Ear service (service's name: 'ear')
     * @return the EAR service.
     * @throws ServiceException if can not get the ear service.
     */
    public Service getEarService() throws ServiceException {
        return getService("ear");
    }

    /**
     * Return the Rar service (service's name: 'resource')
     * @return the RAR service.
     * @throws ServiceException if can not get the rar service.
     */
    public Service getRarService() throws ServiceException {
        return getService("resource");
    }

    /**
     * Return the web service (service's name: 'web').
     * @return the web service.
     * @throws ServiceException if can not get the web service.
     */
    public Service getWebContainerService() throws ServiceException {
        return getService("web");
    }

    /**
     * Return the WebServices service (service's name: 'ws').
     * @return the WebServices service.
     * @throws ServiceException if can not get the WebServices service.
     */
    public Service getWebServicesService() throws ServiceException {
        return getService("ws");
    }

    /**
     * Return the mail service (service's name: 'mail').
     * @return the mail service.
     * @throws ServiceException if can not get the mail service.
     */
    public Service getMailService() throws ServiceException {
        return getService("mail");
    }

    /**
     * Return the DataBase service (service's name: 'dbm')
     * @return the DBM service.
     * @throws ServiceException if can not get the dbm service.
     */
    public Service getDataBaseService() throws ServiceException {
        return getService("dbm");
    }

    /**
     * Return the Transaction service (service's name: 'jtm')
     * @return the JTM service.
     * @throws ServiceException if can not get the jtm service.
     */
    public Service getTransactionService() throws ServiceException {
        return getService("jtm");
    }

    /**
     * Return the JMS service (service's name: 'jms')
     * @return the JMS service.
     * @throws ServiceException if can not get the jms service.
     */
    public Service getJmsService() throws ServiceException {
        return getService("jms");
    }

    /**
     * Return the Security service (service's name: 'security')
     * @return the Security service.
     * @throws ServiceException if can not get the security service.
     */
    public Service getSecurityService() throws ServiceException {
        return getService("security");
    }

    /**
     * Return the JMX service (service's name: 'jmx')
     * @return the JMX service.
     * @throws ServiceException if can not get the jmx service.
     */
    public Service getJmxService() throws ServiceException {
        return getService("jmx");
    }

    /**
     * Return the discovery service (service's name: 'discovery')
     * @return the discovery service.
     * @throws ServiceException if can not get the discovery service.
     */
    public Service getDiscoveryService() throws ServiceException {
        return getService("discovery");
    }

    /**
     * Return the Registry  service (service's name: 'registry')
     * @return the Registry service.
     * @throws ServiceException if can not get the registry service.
     */
    public Service getRegistryService() throws ServiceException {
        return getService("registry");
    }

    /**
     * Return the Resource  service (service's name: 'resource')
     * @return the Resource service.
     * @throws ServiceException if can not get the resource service.
     */
    public Service getResourceService() throws ServiceException {
        return getService("resource");
    }

    /**
     * Start the mandatory "registry" service
     * @throws ServiceException if the "registry" service can't be started.
     */
    public void startRegistry() throws ServiceException {
        if (logger.isLoggable(BasicLevel.DEBUG)) {
            logger.log(BasicLevel.DEBUG, "");
        }
        Service reg = getRegistryService();
        try {
            reg.init((Context) contextsByService.get(reg));
            reg.start();
            logger.log(BasicLevel.INFO, "registry service started");
        } catch (ServiceException e) {
            throw new ServiceException("Cannot init/start registry" + e);
        }
    }

    /**
     * Start the jmx service
     * @throws ServiceException if the jmx service can't be started.
     */
    public void startJmx(String idMBeanServer) throws ServiceException {
        if (logger.isLoggable(BasicLevel.DEBUG)) {
            logger.log(BasicLevel.DEBUG, "");
        }
        Service jmxService = getJmxService();
        try {
            Context paramCtx = (Context) contextsByService.get(jmxService);
            if (idMBeanServer != null) {
                try {
                paramCtx.bind("idMBeanServer", idMBeanServer);
                } catch (NamingException ne) {
                    throw new ServiceException("Cannot bind idMBeanServer = '" + idMBeanServer + "' for JMX service.", ne);
                }
            }
            jmxService.init(paramCtx);
            jmxService.start();
            logger.log(BasicLevel.INFO, "jmx service started");
        } catch (ServiceException e) {
            throw e;
        }
    }

    /**
     * Starts the managed services.
     * @throws ServiceException if the services can't be started.
     */
    public void startServices() throws ServiceException {
        if (logger.isLoggable(BasicLevel.DEBUG)) {
            logger.log(BasicLevel.DEBUG, "");
        }
        Service[] services = getServices();
        Context serviceContext = null;
        // registry = service 0, already started.
        // jmx = service 1, already started
        for (int i = 2; i < services.length; i++) {
            Service service = services[i];
            try {
                serviceContext = (Context) contextsByService.get(service);
                service.init(serviceContext);
                service.start();
                logger.log(BasicLevel.INFO, service.getName() + " service started");
            } catch (ServiceException e) {
                throw new ServiceException("Cannot init/start service '" + service.getName() + "': " + e.getMessage(), e);
            }
        }
    }

    /**
     * Returns the list of the managed services
     * @return String[] the name of the managed services.
     * @throws ServiceException if the list can't be retrieved.
     */
    public Service[] getServices() throws ServiceException {
        Service[] ss = new Service[services.size()];
        for (int i = 0; i < services.size(); i++) {
            ss[i] = (Service) services.get(i);
        }
        return ss;
    }

    /**
     * return the list of services + registry if needed.
     * @return String[] the name of the services.
     */
    public String[] getServiceNames() {
        if (logger.isLoggable(BasicLevel.DEBUG)) {
            logger.log(BasicLevel.DEBUG, "");
        }
        String [] serviceNames;
        serviceNames = props.getValueAsArray(SERVICES_PROP_NAME);
        if (serviceNames == null) {
            return null;
        }
        if (serviceNames[0].equals("registry") && serviceNames[1].equals("jmx")) {
            return serviceNames;
        }
        ArrayList services = new ArrayList();
        services.add(0, "registry");
        services.add(1, "jmx");
        int nbServices = 2;
        for (int i = 0; i < serviceNames.length; i++) {
            if (!serviceNames[i].equals("registry") && !serviceNames[i].equals("jmx")) {
                services.add(serviceNames[i]);
                nbServices++;
            }
        }
        serviceNames = new String[nbServices];
        if (logger.isLoggable(BasicLevel.DEBUG)) {
            logger.log(BasicLevel.DEBUG, "Created new array of String of size " + services.size());
        }
        for (int i = 0; i < nbServices; i++) {
            serviceNames[i] = (String) services.get(i);
        }
        return serviceNames;
    }

    /**
     * For each service, create it and its associated configuration context.
     * (creates services, contextsByService and servicesByName)
     * @throws ServiceException if the services can't be read.
     */
    protected void readServices() throws ServiceException {
        if (logger.isLoggable(BasicLevel.DEBUG)) {
            logger.log(BasicLevel.DEBUG, "");
        }
        services = new ArrayList();
        contextsByService = new Hashtable();
        servicesByName = new Hashtable();
        String [] serviceNames;
        serviceNames = getServiceNames();
        if (serviceNames == null) {
            throw new ServiceException("Property '" + SERVICES_PROP_NAME + "' is missing in '"
                                       + JProp.JONASPREFIX + "' properties file");
        }
        for (int i = 0; i < serviceNames.length; i++) {
            String serviceName = serviceNames[i];
            Service serviceObj = createServiceFrom(serviceName);
            if (serviceObj != null) {
                try {
                    Context ctx = createServiceContextFor(serviceName);
                    services.add(serviceObj);
                    contextsByService.put(serviceObj, ctx);
                    servicesByName.put(serviceName, serviceObj);
                } catch (NamingException e) {
                    throw new ServiceException("cannot create the context name of the service '"
                                               + serviceName + "'",
                                               e);
                }
            }
        }
    }

    /**
     * Creates and returns the service which have the given name.
     * (Uses the 'jonas.service.<serviceName>.class' property for that)
     * @param serviceName the name of the service to instanciate.
     * @return Service the instance of the service.
     * @throws ServiceException if the service can't be created.
     */
    protected Service createServiceFrom(String serviceName) throws ServiceException {
        if (logger.isLoggable(BasicLevel.DEBUG)) {
            logger.log(BasicLevel.DEBUG, serviceName);
        }
        String prefixPropName = PREFIX_SERVICE_PROP_NAME + "." + serviceName;
        String serviceClassName = props.getValue(prefixPropName + ".class");
        if (serviceClassName == null) {
            throw new ServiceException("Property '" + prefixPropName + ".class' missing in '"
                                       + JProp.JONASPREFIX + "' properties file");
        }
        try {
            ClassLoader classLoader = this.getClass().getClassLoader();
            if (classLoader == null) {
                classLoader = Thread.currentThread().getContextClassLoader();
            }
            Class serviceClass = classLoader.loadClass(serviceClassName);
            Service service = (Service) serviceClass.newInstance();
            service.setName(serviceName);
            if (logger.isLoggable(BasicLevel.DEBUG)) {
                logger.log(BasicLevel.DEBUG, "class used: " + serviceClassName);
            }
            return (service);
        } catch (NoClassDefFoundError ncdfe) {
            logger.log(BasicLevel.WARN, "WARNING : The service '" + serviceName
                       + "' is disabled because a class for this service is missing."
                       + " Check your services in jonas.properties file and your environment variables. Missing class : '"
                       + ncdfe.getMessage() + "'", ncdfe);
            // Return null so the service won't be added.
            return null;
        } catch (ClassNotFoundException cnfe) {
            logger.log(BasicLevel.WARN, "WARNING : The service '" + serviceName
                       + "' is disabled because a class for this service is missing."
                       + " Check your services in jonas.properties file and your environment variables. Missing class : '"
                       + cnfe.getMessage() + "'", cnfe);
            // Return null so the service won't be added.
            return null;
        } catch (Exception e) {
            throw new ServiceException("Error when creating the service '" + serviceName + "'", e);
        }
    }

    /**
     * Creates and returns the context for the configuration of the service
     * which have the given name.
     * (Uses the 'jonas.service.<serviceName>.XXXX' properties for that)
     * @param serviceName the name of the service to obtain the configuration.
     * @return Context the context for the service.
     * @throws NamingException if the context can't be built.
     */
    protected Context createServiceContextFor(String serviceName) throws NamingException {
        if (logger.isLoggable(BasicLevel.DEBUG)) {
            logger.log(BasicLevel.DEBUG, serviceName);
        }
        String prefixPropName = PREFIX_SERVICE_PROP_NAME + "." + serviceName;
        CompNamingContext ctx = new CompNamingContext(serviceName);
        for (Enumeration e = props.getEnv().propertyNames(); e.hasMoreElements();) {
            String propName = (String) e.nextElement();
            if (propName.startsWith(prefixPropName + ".")) {
                ctx.rebind(propName, props.getValue(propName));
                // Also, bind the short property value (without the Prefix)
                ctx.rebind(propName.substring((prefixPropName + ".").length()), props.getValue(propName));
            }
        }
        // set Jonas name properties to all ctx
        ctx.rebind(JProp.JONAS_NAME, props.getValue(JProp.JONAS_NAME));
        ctx.rebind(JProp.DOMAIN_NAME, props.getValue(JProp.DOMAIN_NAME));
        // Test JMX Remote
        // Get specific JMX props
        if (serviceName.equals("jmx")) {
            String prop = props.getValue("host.name");
            if (prop != null) {
                ctx.rebind("host.name", prop);
            }
            prop = props.getValue("jmxconnector.port");
            if (prop != null) {
                ctx.rebind("jmxconnector.port", prop);
            }
        }
        return ctx;
    }

    /**
     * Management Method: Stop all services.
     * The services are stopped in the reverse order of their starting.
     * (Continue the processing of stopping for the others services, even if there is a problem
     * for one service.)
     * @throws ServiceException if the service can't be stopped.
     */
    public void stopServices() throws ServiceException  {
        String msgError = new String();
        String sepError = "";
        Service[] services = getServices();
        for (int i = services.length - 1; i >= 0; i--) {
            Service service = services[i];
            if (service.isStarted()) {
                try {
                    service.stop();
                    if (logger.isLoggable(BasicLevel.DEBUG)) {
                        logger.log(BasicLevel.DEBUG, service.getName() + " service stopped");
                    }
                } catch (ServiceException e) {
                    msgError = msgError.concat(sepError + "Cannot stop the service '"
                                               + service.getName() + "': " + e);
                    sepError = "\n";
                }
            }
        }
        if (msgError.length() != 0) {
            throw new ServiceException(msgError);
        }
    }
}
