/**
 * JOnAS: Java(TM) Open Application Server
 * Copyright (C) 2006 Bull S.A.S.
 * 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 1any 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.cluster.daemon;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Writer;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import java.util.Vector;

import javax.management.MalformedObjectNameException;
import javax.management.ObjectName;
import javax.management.remote.JMXConnectorFactory;
import javax.management.remote.JMXConnectorServer;
import javax.management.remote.JMXServiceURL;
import javax.management.remote.MBeanServerForwarder;

import org.objectweb.carol.jndi.ns.NameServiceException;
import org.objectweb.carol.jndi.ns.NameServiceManager;
import org.objectweb.carol.util.configuration.CarolDefaultValues;
import org.objectweb.carol.util.configuration.ConfigurationException;
import org.objectweb.carol.util.configuration.ConfigurationRepository;
import org.objectweb.carol.util.configuration.TraceCarol;
import org.objectweb.common.Env;
import org.objectweb.common.WinSysEnv;
import org.objectweb.jonas.common.JProp;
import org.objectweb.jonas.common.Log;
import org.objectweb.jonas.jmx.commons.JMXRemoteException;
import org.objectweb.jonas.jmx.commons.JMXRemoteHelper;
import org.objectweb.jonas.jmx.commons.MBeanServerException;
import org.objectweb.jonas.jmx.commons.MBeanServerHelper;
import org.objectweb.jonas.security.auth.spi.RoleBasedAuthorizationModule;
import org.objectweb.jonas_clusterd.xml.Server;
import org.objectweb.jonas_lib.deployment.xml.JLinkedList;
import org.objectweb.util.monolog.api.BasicLevel;
import org.objectweb.util.monolog.api.Logger;


/**
 *  ClusterDaemon is provided to control JOnAS instances remotely.
 *  The ClusterDaemon is an independent Java program, and runs on every node (machine) in the cluster.
 *  It receives remote commands (JMX), and does something such as starting or stopping JOnAS instances.
 *  @author zhengzz (initial version for PKUAS)
 *  @author Benoit Pelletier (integration in JOnAS)
 *  @author S. Ali Tokmen (JMX security)
 */

public class ClusterDaemon implements ClusterDaemonMBean {

    /**
     * Allow to trace errors/output of a process
     */
    class CmdReader implements Runnable {

        /**
         * Input stream containing information
         */
        private InputStream is;

        /**
         * Should send as error or debug message ?
         */
        private boolean isErrorMessage = false;

        /**
         * Logger to use
         */
        private Logger logger = null;

        /**
         * Cmd associated with the reader
         */
        private String cmd = null;

        /**
         * Constructor
         * @param logger logger
         * @param cmd command
         * @param is given input stream
         * @param isErrorMessage Should send as error or debug message
         */
        public CmdReader(Logger logger, String cmd, InputStream is, boolean isErrorMessage) {
            this.is = is;
            this.isErrorMessage = isErrorMessage;
            this.logger = logger;
            this.cmd = cmd;
        }

        /**
         * Thread execution printing information received
         */
        public void run() {
            try {
                BufferedReader br = new BufferedReader(new InputStreamReader(is));
                String str = null;
                while ((str = br.readLine()) != null) {
                    if (isErrorMessage) {
                        this.logger.log(BasicLevel.ERROR, "<" + cmd + "> " + str);
                    } else {
                        this.logger.log(BasicLevel.DEBUG, "<" + cmd + "> " + str);
                    }
                }
                // close input stream
                is.close();
            } catch (Exception e) {
                this.logger.log(BasicLevel.ERROR, e.getMessage());
                e.printStackTrace();
            }
        }
    }

    /**
     * Allow stop the cmd childs
     */
    class ShutdownHook extends Thread {

        /**
         * Action at the shutdown : destroy the process
         */
        public static final int ACTION_DESTROY = 0;

        /**
         * Action at the shutdown : stop the JOnAS instance
         */
        public static final int ACTION_JONAS_STOP = 1;

        /**
         * Cmdkey
         */
        private String keyCmd = null;

        /**
         * Action
         */
        private int action = -1;

        /**
         * JOnAS instance name
         */
        private String serverName = null;

        /**
         * Constructor
         * @param keyCmd key command
         */
        public ShutdownHook(String serverName, String keyCmd, int action) {
            super();
            this.keyCmd = keyCmd;
            this.action = action;
            this.serverName = serverName;
        }

        /**
         * Thread execution printing information received
         */
        public void run() {
            try {
                if (action == ACTION_DESTROY) {
                    ((Process) ClusterDaemon.this.processMap.get(this.keyCmd)).destroy();
                    ClusterDaemon.this.logger.log(BasicLevel.DEBUG, "destroy cmd" + keyCmd);
                } else if (action == ACTION_JONAS_STOP) {
                    ClusterDaemon.this.logger.log(BasicLevel.DEBUG, "stop JOnAS instance " + serverName);
                    ClusterDaemon.this.doStopJOnAS(serverName);
                    ClusterDaemon.this.logger.log(BasicLevel.DEBUG, "destroy cmd" + keyCmd);
                    ((Process) ClusterDaemon.this.processMap.get(this.keyCmd)).destroy();
                }

            } catch (Exception e) {
                TraceCarol.error("ShutdownHook problem for key=" + this.keyCmd, e);
            }
       }
    }

    /**
     * Monitor is a tool that collects node's performance information regularly if needed
     * (only supporting CPU and memory information now).
     * @author zhengzz
     *
     */
    class Monitor extends Thread {

        /**
         * Logger to use
         */
        private Logger logger = null;

        int cpurate;
        long totalM, availM, percentM;
        BufferedWriter out;
        int count = 0;

        public Monitor(Logger logger) {
            this.logger = logger;
            try {
                out = new BufferedWriter(new FileWriter("monitor.txt", true));
            } catch (Exception e) {
                this.logger.log(BasicLevel.ERROR, "Fail to new FileWrite from monitor.txt" + e);
            }
        }

        public void run() {
            this.logger.log(BasicLevel.INFO, "Start monitoring thread....");
            while (true) {
                try {
                    //cpurate = SystemInformation.getCurrentCpuUsage();
                    //totalM = SystemInformation.getMemoryInfo(1) / 1024;
                    //availM = SystemInformation.getMemoryInfo(2) / 1024;
                    //percentM = SystemInformation.getMemoryInfo(0);
                    //out.write(count + "   " + cpurate + "   " + percentM + "\r\n");
                    //out.flush();
                    Thread.currentThread().sleep(10 * 1000);
                    count = count + 10;
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * Sleep time to wait
     */
    private static final int SLEEP_TIME = 2000;

    /**
     *  JOnAS interaction mode : tighly coupled
     */
    private static final String TIGHLY_COUPLED = "tighly-coupled";

    /**
     *  JOnAS interaction mode : loosely coupled
     */
    private static final String LOOSELY_COUPLED = "loosely-coupled";

    /**
     * jonas admin ping short timeout
     */
    private static final int PING_SHORT_TIMEOUT = 5;

    /**
     * jonas admin ping long timeout
     */
    private static final int PING_LONG_TIMEOUT = 120;

    /**
     * Sleep before exit of JVM
     */
    public static final int SLEEP_DELAY = 2000;

    /**
     * JOnAS command name in windows environmment
     */
    public static final String JONAS_CMD_NAME_WIN_DEF = "jonas.bat";

    /**
     * JOnAS command name in unix environmment
     */
    public static final String JONAS_CMD_NAME_UNIX_DEF = "jonas";

    /**
     * trace.properties file to use instead of the default file.
     */
    private static String csTraceFile = null;

    /**
     * carol.properties file to use instead of the default file.
     */
    private static String carolFile = null;

    /**
     * clusterd.xml filename (by default $JONAS_ROOT/conf/clusterd.xml)
     */
    private static String confFile = null;


    /**
     * J2EE Management domain
     */
    private static String domainName = null;

    /**
     * Logger to use
     */
    private Logger logger = null;

    /**
     * Object name
     */
    private String objectName = null;

    /**
     * Cluster Daemon name
     */
    private String name = null;

    /**
     * Process list
     */
    private Map processMap = new HashMap();

    /**
     * server state
     */
    private boolean isStarted = false;


    /**
     * Constructor
     */
    private ClusterDaemon() {
    }

    /**
     * Execute a JOnAS command in a separate JVM
     * @param server JOnAS server to pass the command
     * @param cmd command
     * @param keyCmd id the of command
     * @return Process associated with the command
     * @throws Exception exception if an error occurs
     */
    private Process execJOnASCmd(Server server, String cmd, String keyCmd) throws Exception {

        // check if the server exists
        if (server == null) {
            throw new ClusterDaemonException("server=null");
        }

        // Check that a same process in not running
       Process previousP =  (Process) processMap.get(keyCmd);
       if (previousP != null) {
           try {
               this.logger.log(BasicLevel.INFO, "Cmd =" + cmd + " Process=" + previousP.exitValue());
           } catch (IllegalThreadStateException exc) {
             // there is already a process running
               this.logger.log(BasicLevel.INFO, "Cmd =" + cmd + " already running (pid=" + previousP.toString() + ").");
               throw new IllegalStateException("Cmd =" + cmd + " already running (pid=" + previousP.toString() + ").");
           }
         }

        // Set the environment
        Vector envv = new Vector();
        envv.addElement("JAVA_HOME=" + server.getJavaHome());
        envv.addElement("JONAS_ROOT=" + server.getJonasRoot());
        envv.addElement("JONAS_BASE=" + server.getJonasBase());

        String catalinaHome = System.getProperty("catalina.home");
        if (catalinaHome != null) {
            envv.addElement("CATALINA_HOME=" + catalinaHome);
            envv.addElement("CATALINA_BASE=" + server.getJonasBase());
        }

        String jettyHome = System.getProperty("jetty.home");
        if (jettyHome != null) {
            envv.addElement("JETTY_HOME=" + jettyHome);
        }

        // if OS is of Windows family, set the SystemRoot variable
        if (Env.isOsWindows()) {
            envv.addElement("SystemRoot=" + WinSysEnv.get("SystemRoot"));
        }

        String[] enva = new String[envv.size()];
        envv.copyInto(enva);

        // set the bin dir according to the platform (only if it's not a user command)
        String binDir = null;
        if (isDefinedUserJonasCmd(server)) {
            // a full path must be set in the configuration
            this.logger.log(BasicLevel.INFO, "User cmd - don't add the jonas-root prefix dir");
            binDir = "";
        } else {
            this.logger.log(BasicLevel.INFO, "Default cmd - add the jonas-root prefix dir");
            binDir = server.getJonasRoot();
            if (File.separatorChar == '/') { // unix system
                binDir += "/bin/unix/";
            } else if (File.separatorChar == '\\') { // windows system
                binDir += "\\bin\\nt\\";
            }
        }

        String[] cmda = null;
        if (cmd != null) {
            cmda = (binDir + cmd).split("\\s+");
        }
         // Set the working directory to the current directory
        File workingDirectory = new File(".");

        // Launch the command
        this.logger.log(BasicLevel.INFO, "Execute the command <" + binDir + cmd + "> with the env " + envv);

        Process  p  = Runtime.getRuntime().exec(cmda, enva, workingDirectory);

        // if synchronous mode, wait for the end of the execution


        processMap.put(keyCmd, p);

        // wait for starting
        Thread.sleep(SLEEP_TIME);

        // trace the start execution
        InputStream cmdError = p.getErrorStream();
        InputStream cmdOut = p.getInputStream();
        Thread err = new Thread(new CmdReader(this.logger, keyCmd, cmdError, true));
        Thread out = new Thread(new CmdReader(this.logger, keyCmd, cmdOut, false));
        out.start();
        err.start();

        // add a shudown hook for this process
        // if loosely coupled mode, only the current commands are destroyed - the JOnAS instances are not stopped
        int action = ShutdownHook.ACTION_DESTROY;
        if (keyCmd.startsWith("start") && isTighlyCoupled()) {
            action = ShutdownHook.ACTION_JONAS_STOP;
        }
        Runtime.getRuntime().addShutdownHook(new ShutdownHook(server.getName(), keyCmd, action));

        this.logger.log(BasicLevel.INFO, "Command " + cmd + " with the env " + envv + " launched");

        return p;
    }

    /**
     * @param server JOnAS server configuration
     * @return true if a user command is defined
     */
    private boolean isDefinedUserJonasCmd(Server server) {
        if (server.getJonasCmd() == null) {
            return false;
        }
        if (server.getJonasCmd().trim().equals("") ){
            return false;
        }
        return true;
    }
    /**
     * @param server JOnAS server configuration
     * @return JOnAS command name according to the os
     */
    private String getJOnASCmdName(Server server) {
        // Test whether a user command is set
        if (isDefinedUserJonasCmd(server)) {
            // user command
            return server.getJonasCmd().trim();
        } else {
            // default command
            if (Env.isOsWindows()) {
                return JONAS_CMD_NAME_WIN_DEF;
            } else {
                return JONAS_CMD_NAME_UNIX_DEF;
            }
        }

    }

    /**
     * Start a JOnAS instance
     * @param name server name to sart
     * @param domainName domain name
     * @param prm extra parameters
     * @param sync wait until the complete starting
     * @throws ClusterDaemonException if an error occurs
     */
    private void doStartJOnAS(String name, String domainName, String prm, boolean sync) throws ClusterDaemonException {

        // Get the Server
        Server server = getServer(name);

        // check if the server exists
        if (server == null) {
            //throw new ClusterDaemonException("JOnAS instance " + name + " doesn't exist");
            throw new ClusterDaemonException("JOnAS server " + name + " is not known by the cluster daemon " + getName());
        }

        // get prm if needed
        String xprm = null;
        if (prm == null) {
            xprm = "";
        } else {
            xprm = prm;
        }

        // add the parameters given in the configuration
        if (server.getXprm() != null) {
            xprm += server.getXprm();
        }

        String d = null;
        if (domainName == null) {
            d = ClusterDaemon.domainName;
        } else {
            d = domainName;
        }

        // Compose the command
        String cmd = getJOnASCmdName(server) + " start -n " + name + " -Ddomain.name=" + d + " " + xprm;

        // If the interaction mode with JOnAS is tighly coupled, the instance is launched in the foreground
        if (isTighlyCoupled()) {
            cmd += "-fg";
        }

        this.logger.log(BasicLevel.INFO, "JOnAS instance " + name + " is starting ...");

        String keyCmd = "start/" + name;

        Process p = null;
        try {
            p = execJOnASCmd(server, cmd, keyCmd);
        } catch (Throwable t) {
            this.logger.log(BasicLevel.ERROR, "Unable to start the JOnAS instance <" + name + ">, cmd=<" + cmd + ">", t);
            throw new ClusterDaemonException("Unable to start the JOnAS instance <" + name + ">, cmd=<" + cmd + ">", t);
        }

        this.logger.log(BasicLevel.INFO, "JOnAS instance " + name + " launched");

        // If sync is required, wait for the end of the starting
        if (sync) {
            if (isLooselyCoupled()) {
                int exitCode = waitEndProcess(p);
            } else {
                if (doPingJOnAS(name, PING_LONG_TIMEOUT) == 1) {
                    throw new ClusterDaemonException("Unable to start the JOnAS instance " + name + " - unreachable server");
                }
            }
            this.logger.log(BasicLevel.INFO, "JOnAS instance " + name + "  started");
        }
    }

    /**
     * Stop a JOnAS instance
     * @param name instance name
     * @throws ClusterDaemonException if an error occurs
    */
    private void doStopJOnAS(String name) throws ClusterDaemonException {
        // Get the Server
        Server server = getServer(name);

        // check if the server exists
        if (server == null) {
            throw new ClusterDaemonException("JOnAS instance " + name + " doesn't exist");
        }

        String cmd = getJOnASCmdName(server) + " stop -n " + server.getName();

        this.logger.log(BasicLevel.INFO, "JOnAS instance " + name + " is stopping ...");

        String keyCmd = "stop/" + server.getName();

        Process p = null;
        try {
            p = execJOnASCmd(server, cmd, keyCmd);
        } catch (Throwable t) {
            this.logger.log(BasicLevel.ERROR, "Unable to stop the JOnAS instance <" + name + ">, cmd=<" + cmd + ">", t);
            throw new ClusterDaemonException("Unable to stop the JOnAS instance <" + name + ">, cmd=<" + cmd + ">", t);
        }
        // Wait until command termination and get the exit code
        int exitCode = waitEndProcess(p);

        this.logger.log(BasicLevel.INFO, "JOnAS instance " + name + " stopped");
    }

    /**
     * Kill a JOnAS instance (only in tighly coupled mode)
     * @param name instance name
     * @throws ClusterDaemonException if an error occurs
    */
    private void doKillJOnAS(String name) throws ClusterDaemonException {
        // Get the Server
        Server server = getServer(name);

        // check if the server exists
        if (server == null) {
            throw new ClusterDaemonException("JOnAS instance " + name + " doesn't exist");
        }

        // check the mode
        if (isLooselyCoupled()) {
            throw new ClusterDaemonException("Command not compatible with the loosely-coupled mode");
        }

        String keyCmd = "start/" + server.getName();

        // get the process
        Process p = (Process) processMap.get(keyCmd);

        // check that the process is alive
        boolean alive = false;
        try {
            this.logger.log(BasicLevel.DEBUG, "Process=" + p.exitValue());

            // Process not alived
            this.logger.log(BasicLevel.DEBUG, "JOnAS instance " + name + " is already dead");

        } catch (IllegalThreadStateException exc) {
          // there is already a process running
            this.logger.log(BasicLevel.DEBUG, "JOnAS instance " + server.getName() + " is going to be killed");
            alive = true;
        }

        if (alive) {
            // kill the process
            p.destroy();
            this.logger.log(BasicLevel.INFO, "JOnAS instance " + name + " killed");
        } else {
            this.logger.log(BasicLevel.INFO, "JOnAS instance " + name + " already killed");
        }

    }

    /**
     * Wait until the end of the execution of the process
     * @param p process
     * @return
     */
    private int waitEndProcess(Process p) {

        if (p == null) {
            return -1;
        }
        boolean isRunning = true;
        int exitCode = -1;
        while (isRunning) {
            this.logger.log(BasicLevel.DEBUG, "check if the process " + p + " is running");
            try {
                exitCode = p.exitValue();
                this.logger.log(BasicLevel.DEBUG, "cmd " + p + " is finished");
                isRunning = false;
            } catch (IllegalThreadStateException exc) {
                // there is already a process running
                this.logger.log(BasicLevel.DEBUG, "cmd " + p + " is still running");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    this.logger.log(BasicLevel.DEBUG, e);
                }
            }
        }
        return exitCode;
    }

    /**
     * Ping a JOnAS instance
     * @param name instance name
     * @param timeout timeout
     * @return exit code of the ping (0 ok, 1 ko)
     * @throws ClusterDaemonException if an error occurs
    */
    private int doPingJOnAS(String name, int timeout) throws ClusterDaemonException {

        // Get the Server
        Server server = getServer(name);

        // check if the server exists
        if (server == null) {
            throw new ClusterDaemonException("JOnAS instance " + name + " doesn't exist");
        }

        String cmd = getJOnASCmdName(server) + " admin -n " + server.getName() + " -ping -timeout " + timeout;

        this.logger.log(BasicLevel.INFO, "Ping JOnAS instance " + name + " ...");

        String keyCmd = "ping/" + server.getName();

        try {
            execJOnASCmd(server, cmd, keyCmd);
        } catch (Throwable t) {
            this.logger.log(BasicLevel.ERROR, "Unable to ping the JOnAS instance <" + name + ">, cmd=<" + cmd + ">", t);
            throw new ClusterDaemonException("Unable to ping the JOnAS instance <" + name + ">, cmd=<" + cmd + ">", t);
        }

        // Wait until command termination and get the exit code
        Process p = (Process) processMap.get(keyCmd);
        int exitCode = waitEndProcess(p);
        this.logger.log(BasicLevel.INFO, "Ping JOnAS instance " + name + " return " + exitCode);
        return exitCode;
    }

    /**
     * Call the Log class to instanciate the cluster daemon logger
     * @throws ClusterDaemonException if an error occurs
     */
    private void initLogger() throws ClusterDaemonException {

        if (csTraceFile != null) {
            File tClient = new File(csTraceFile);

            if (!tClient.exists()) {
                throw new ClusterDaemonException("The file '" + csTraceFile + "' was not found.");
            }

            if (!tClient.isFile()) {
                throw new ClusterDaemonException("The file '" + csTraceFile
                        + "' is not a valid file. Maybe a directory ?");
            }

            // Configure log
            System.setProperty("jonas.client.trace.file", csTraceFile);
            Log.reset();
        } else {
            csTraceFile = "trace";
        }

        // Allow tracing ejb/jms code
        try {
            System.setProperty("jonas.client.trace.file", csTraceFile);
            Log.configure(csTraceFile);
        } catch (NoClassDefFoundError ncdfe) {
            // 'normal exception' thrown outside the JOnAS containers
        }

        // init the logger
        this.logger = Log.getLogger(Log.JONAS_CLUSTER_DAEMON);
    }

    /**
     * Init carol
     * @throws ClusterDaemonException if an error occurs
     */
    private void initCarol() throws ClusterDaemonException {

        System.setProperty("carol.server.mode", "true");
        try {
            // hack
            Writer fw;
            File fConf = null;
            if (carolFile == null) {

                // 1st, try to use the carol.properties in $JONAS_BASE/conf
                URL myCarolFile = Thread.currentThread().getContextClassLoader().getResource(CarolDefaultValues.CAROL_CONFIGURATION_FILE);
                if (myCarolFile == null) {

                    fConf = new File(System.getProperty("java.io.tmpdir") + File.separator + "cs-carol.properties");
                    try {
                        fw = new FileWriter(fConf);
                        fw.write("carol.protocols=irmi\n");
                        fw.write("carol.irmi.url=rmi://localhost:1806\n");
                        //  fw.write("carol.jvm.rmi.local.registry=true");
                        fw.close();
                    } catch (IOException e) {
                        throw new ClusterDaemonException("Cannot initialize carol", e);
                    }
                } else {
                    fConf = new File(myCarolFile.getFile());
                }
            } else {
                fConf = new File(carolFile);
            }
            ConfigurationRepository.init(fConf.toURL());
        } catch (ConfigurationException e) {
            throw new ClusterDaemonException("Cannot initialize registry", e);
        } catch (MalformedURLException e) {
            throw new ClusterDaemonException("Cannot initialize registry", e);
        }
        // start registry
        try {
            NameServiceManager.startNS();
        } catch (NameServiceException e) {
            throw new ClusterDaemonException("Cannot start registry", e);
        }

        this.logger.log(BasicLevel.INFO, "Carol initialized");
    }


    /**
     * Init MBeans
     * @throws ClusterDaemonException if an error occurs
     */
    private void initMBeans() throws ClusterDaemonException {

        try {
            MBeanServerHelper.startMBeanServer(domainName);
        } catch (MBeanServerException e) {
            throw new ClusterDaemonException("Cannot start MBean server", e);
        }

        try {
            Map props = null;
            this.name = ClusterDaemonTools.getCurrentConfiguration().getClusterDaemon().getName();
            String url = ClusterDaemonTools.getJmxUrl(name);
            String objName = ClusterDaemonTools.getObjectName();

            // Set JMX authentication and authorization options
            // We'll test later if JMX has really been secured
            boolean jmxSecured = ClusterDaemonTools.getCurrentConfiguration().getClusterDaemon().isJmxSecured();;
            MBeanServerForwarder forwarder = null;
            if(jmxSecured) {
                props = new HashMap();
                String fileSeparator = System.getProperty("file.separator");

                // Authentication, avoid NullPointerExceptions
                String loginModule = ClusterDaemonTools.getCurrentConfiguration().getClusterDaemon().getJmxAuthenticationMethod();
                String loginParam = ClusterDaemonTools.getCurrentConfiguration().getClusterDaemon().getJmxAuthenticationParameter();
                if(loginModule != null && loginParam != null) {
                    if ("jmx.remote.x.password.file".equals(loginModule)) {
                        // Make sure files get read using JONAS_BASE
                        props.put(loginModule, JProp.getJonasBase() + fileSeparator + loginParam);
                    } else {
                        props.put(loginModule, loginParam);
                    }
                }

                // Authorization, avoid NullPointerExceptions
                String authModule = ClusterDaemonTools.getCurrentConfiguration().getClusterDaemon().getJmxAuthorizationMethod();
                String authParam = ClusterDaemonTools.getCurrentConfiguration().getClusterDaemon().getJmxAuthorizationParameter();
                if(authModule != null && authParam != null) {
                    if (authModule.startsWith("jmx.remote.x.access.rolebased")) {
                        forwarder = RoleBasedAuthorizationModule.newProxyInstance(
                                authModule.substring("jmx.remote.x.access.rolebased".length() + 1), authParam);
                    } else if ("jmx.remote.x.access.file".equals(authModule)) {
                        // Make sure files get read using JONAS_BASE
                        props.put(authModule, JProp.getJonasBase() + fileSeparator + authParam);
                    } else {
                        props.put(authModule, authParam);
                    }
                }
            }

            JMXConnectorServer connectorServer = JMXRemoteHelper.startConnector(url, objName, props);
            if(forwarder != null) {
                connectorServer.setMBeanServerForwarder(forwarder);
            }
            this.logger.log(BasicLevel.DEBUG, "JMX remote connector (" + url + ", " + objName + " started ");

            if(jmxSecured) {
                // Test security: try to connect without credentials
                try {
                    JMXConnectorFactory.connect(new JMXServiceURL(url)).close();
                    logger.log(BasicLevel.WARN, "JMX security is enabled but anonymous logins are still accepted! "
                            + "Please check your JMX security configuration.");
                } catch(SecurityException e) {
                    // It's working: connection was refused
                    logger.log(BasicLevel.INFO, "JMX security is enabled and active");
                } catch (Exception e) {
                    throw new ClusterDaemonException("Cannot test JMX security", e);
                }
            } else {
                logger.log(BasicLevel.WARN, "JMX security is DISABLED");
            }
        } catch (JMXRemoteException e) {
            throw new ClusterDaemonException("Cannot start JMX Remote connector", e);
        }

        // register local MBeans
        StringBuffer sb = new StringBuffer(domainName);
        sb.append(":type=ClusterDaemon");
        ObjectName on = null;
        try {
            on = new ObjectName(sb.toString());
        } catch (MalformedObjectNameException e) {
            throw new ClusterDaemonException("Cannot build ObjectName", e);
        }

        // Set the objectname
        this.setobjectName(on.toString());

        try {
            MBeanServerHelper.getMBeanServer().registerMBean(this, on);
            this.logger.log(BasicLevel.DEBUG, "MBean (" + on + ") registered");

        } catch (Exception e) {
            throw new ClusterDaemonException("Cannot register MBean '" + on + "' in MBeanServer", e);
        }

        this.logger.log(BasicLevel.INFO, "MBeans initialized");
    }

    /**
     *
     * @return true is the loosely-coupled mode is enabled
     */
    private boolean isLooselyCoupled() {

        return ClusterDaemonTools.getCurrentConfiguration().getClusterDaemon().getJonasInteractionMode().equals(ClusterDaemon.LOOSELY_COUPLED);
    }

    /**
    *
    * @return true is the tighly-coupled mode is enabled
    */
   private boolean isTighlyCoupled() {

       return ClusterDaemonTools.getCurrentConfiguration().getClusterDaemon().getJonasInteractionMode().equals(ClusterDaemon.TIGHLY_COUPLED);
   }


      /**
     * @param name server name
     * @return Configuration of the server
     */
    private Server getServer(String name) {
        JLinkedList servers = ClusterDaemonTools.getCurrentConfiguration().getClusterDaemon().getServerList();

        if (servers != null) {
            for (int i = 0; i < servers.size(); i++) {
                Server serverElement = (Server) servers.get(i);
                if (serverElement.getName().equals(name)) {
                    return serverElement;
                }
            }
        }
        return null;
    }

    /**
     * Start all the JOnAS instances configured with auto-reboot
     * @param domainName domain name
     * @param prm extra parameters
     * @param force the starting
     * @param sync synchronous starting
     * @return the nodes list with an indicator started/starting failed
     */
    private String doStartAllJOnAS(String domainName, String prm, boolean force, boolean sync) {

        // This feature is valid only if the jonas-interaction-mode is loosely-coupled
        if (!force) {
            if (isLooselyCoupled()) {
                String result = "JOnAS interaction mode is set to loosely-coupled => don't start the JOnAS instances";
                this.logger.log(BasicLevel.INFO, result);
                return result;
            } else {
                this.logger.log(BasicLevel.INFO, "JOnAS interaction mode is set to tighly-coupled => start the JOnAS instances");
            }
        }

        int nbInst = 0;
        int nbInstStarted = 0;
        String instStarted = "";
        int nbInstStartingError = 0;
        String instStartingError = "";

        JLinkedList servers = ClusterDaemonTools.getCurrentConfiguration().getClusterDaemon().getServerList();

        if (servers != null) {
            for (int i = 0; i < servers.size(); i++) {
                nbInst++;
                Server serverElement = (Server) servers.get(i);
                if (serverElement.isAutoBoot() || force) {
                    try {
                        doStartJOnAS(serverElement.getName(), domainName, prm, sync);
                        nbInstStarted++;
                        instStarted += serverElement.getName() + " ";
                    } catch (ClusterDaemonException e) {
                        this.logger.log(BasicLevel.ERROR, "Error during the launching of JOnAS instance " + serverElement.getName(), e);
                        nbInstStartingError++;
                        instStartingError += serverElement.getName() + " ex=" + e + " ";
                    }
                }
            }
        }
        String result = "Instances started (" +  nbInstStarted + "/" + nbInst + ", " + instStarted + ")";
        result += " Instances starting failed (" +  nbInstStartingError + "/" + nbInst + ", " + instStartingError + ")";

        this.logger.log(BasicLevel.INFO, result);
        return result;
    }

    /**
     * Stop all the JOnAS instances
     * @return the nodes list with an indicator stopped/stopping failed
     */
    private String doStopAllJOnAS() {

        int nbInst = 0;
        int nbInstStopped = 0;
        String instStopped = "";
        int nbInstStoppingError = 0;
        String instStoppingError = "";

        JLinkedList servers = ClusterDaemonTools.getCurrentConfiguration().getClusterDaemon().getServerList();

        if (servers != null) {
            for (int i = 0; i < servers.size(); i++) {
                Server serverElement = (Server) servers.get(i);
                try {
                    doStopJOnAS(serverElement.getName());
                    nbInstStopped++;
                    instStopped += serverElement.getName() + " ";
                } catch (ClusterDaemonException e) {
                    this.logger.log(BasicLevel.ERROR, "Error during the stopping of JOnAS instance " + serverElement.getName(), e);
                    nbInstStoppingError++;
                    instStoppingError += serverElement.getName() + " ex=" + e + " ";
                }
            }
        }
        String result = "Instances stopped (" +  nbInstStopped + "/" + nbInst  + ", " + instStopped + ")";
        result += " Instances stopping failed (" +  nbInstStoppingError + "/" + nbInst  + ", " + instStoppingError + ")";

        this.logger.log(BasicLevel.INFO, result);
        return result;
    }

    /**
     * Kill all the JOnAS instances
     * @return the nodes list with an indicator killed/killing failed
     */
    private String doKillAllJOnAS() {

        int nbInst = 0;
        int nbInstKilled = 0;
        String instKilled = "";
        int nbInstKillingError = 0;
        String instKillingError = "";

        JLinkedList servers = ClusterDaemonTools.getCurrentConfiguration().getClusterDaemon().getServerList();

        if (servers != null) {
            for (int i = 0; i < servers.size(); i++) {
                Server serverElement = (Server) servers.get(i);
                try {
                    doKillJOnAS(serverElement.getName());
                    nbInstKilled++;
                    instKilled += serverElement.getName() + " ";
                } catch (ClusterDaemonException e) {
                    this.logger.log(BasicLevel.ERROR, "Error during the killing of JOnAS instance " + serverElement.getName(), e);
                    nbInstKillingError++;
                    instKillingError += serverElement.getName() + " ex=" + e + " ";
                }
            }
        }
        String result = "Instances killed (" +  nbInstKilled + "/" + nbInst  + ", " + instKilled + ")";
        result += " Instances killing failed (" +  nbInstKillingError + "/" + nbInst  + ", " + instKillingError + ")";

        this.logger.log(BasicLevel.INFO, result);
        return result;
    }

    /**
     * Main function
     * @param args arguments list
     * @throws ClusterDaemonException if an error occurs
     */
    public void start(String[] args) throws ClusterDaemonException {

        boolean useMonitor = false;

        /**
         * Analyse the arguments list
         */
        for (int argn = 0; argn < args.length; argn++) {
            String arg = args[argn];

            try {
                if (arg.equals("-useMonitor")) {
                    useMonitor = true;
                    continue;
                }

                if (arg.equals("-traceFile")) {
                    csTraceFile = args[++argn];
                    continue;
                }

                if (arg.equals("-carolFile")) {
                    carolFile = args[++argn];
                    continue;
                }

                if (arg.equals("-confFile")) {
                    confFile = args[++argn];
                    continue;
                }

                if (arg.equals("--help") || arg.equals("-help") || arg.equals("-h") || arg.equals("-?")) {
                    usage();
                    System.exit(1);
                }

            } catch (ArrayIndexOutOfBoundsException aioobe) {
                // The next argument is not in the array
                throw new ClusterDaemonException("A required parameter was missing after the argument" + arg);
            }
        }


        /**
         * Init the trace logger
         */
        initLogger();

        /**
         * Load clusterd.xml
         */
        ClusterDaemonTools.loadClusterDaemonConfiguration(confFile);

        /**
         * Get the domain name
         */
        domainName = (String) System.getProperty(JProp.DOMAIN_NAME, JProp.JONAS_DEF_NAME);
        String confDomainName =  ClusterDaemonTools.getCurrentConfiguration().getClusterDaemon().getDomainName();

        if (domainName.equals(JProp.JONAS_DEF_NAME)) {
            if (confDomainName == null) {
                this.logger.log(BasicLevel.INFO, "Domain set with default value : " + domainName);
            } else {
                domainName = confDomainName;
                this.logger.log(BasicLevel.INFO, "Domain set with the value defined in the clusterd.xml : " + domainName);
            }
        } else if (domainName.equals(confDomainName)) {
            this.logger.log(BasicLevel.INFO, "Domain set with the value defined in the clusterd.xml : " + domainName);
        } else {
            throw new ClusterDaemonException("Incorrect domain name set in clusterd.xml file and in the property " + confDomainName + "/" + domainName);
        }

        /**
         * Init Carol
         */
        initCarol();

        /**
         * Init MBean server
         */
        initMBeans();

        this.logger.log(BasicLevel.INFO, "JOnAS Cluster Daemon Started!");

        /**
         * start the monitor thread if required
         */
        if (useMonitor) {
            new Monitor(this.logger).start();
        }

        /**
         * Launches the JOnAS instances configured with auto-reboot
         */
        doStartAllJOnAS(domainName, null, false, false);

        /**
         * The server is started now
         */
        isStarted = true;

    }

    /**
     * Usage
     */
    private static void usage() {
        System.out.println("Usage : jclusterd [-DdomainName=<name>] [-useMonitor] [-confFile my_clusterd.xml] [-carolFile=<my_carol.properties>]");
    }

    /**
     * Main method
     * @param args the arguments of the cluster daemon
     */
    public static void main(String[] args) {
        // Retrieve command line parameters
        ClusterDaemon cs = new ClusterDaemon();

        try {
            cs.start(args);
        } catch (Exception e) {
            System.err.println("There was the following exception : " + e.getMessage());
            e.printStackTrace();
            System.exit(-1);
        }
    }

    //////////////////////////////////////////////////////////////////////////////////////////
    ///
    ///         MBEAN INTERFACES
    ///
    //////////////////////////////////////////////////////////////////////////////////////////
    /**
     * @return Object Name
     */
    public String getobjectName() {
        return objectName;
    }

    /**
     * Sets the object name of this mbean
     * @param name the Object Name
     */
    public void setobjectName(String name) {
        this.objectName = name;
    }

    /**
     * @return true if it is an event provider
     */
    public boolean iseventProvider() {
        return false;
    }

    /**
     * @return true if this managed object implements J2EE State Management
     *         Model
     */
    public boolean isstateManageable() {
        return false;
    }

    /**
     * @return true if this managed object implements the J2EE StatisticProvider
     *         Model
     */
    public boolean isstatisticsProvider() {
        return false;
    }

    /**
     * (MBean interface)
     * @return the JAVA_HOME for a specified server name
     * @param name JOnAS instance name
     */
    public String getJavaHome4Server(String name) {
        Server server = getServer(name);
        if (server != null) {
            return server.getJavaHome();
        } else {
            return null;
        }
    }

    /**
     * (MBean interface)
     * @return the JONAS_BASE for a specified server name
     * @param name JOnAS instance name
     */
    public String getJonasBase4Server(String name) {
        Server server = getServer(name);
        if (server != null) {
            return server.getJonasBase();
        } else {
            return null;
        }
    }

    /**
     * (MBean interface)
     * @return the JONAS_ROOT for a specified server name
     * @param name JOnAS instance name
     */
    public String getJonasRoot4Server(String name) {
        Server server = getServer(name);
        if (server != null) {
            return server.getJonasRoot();
        } else {
            return null;
        }
    }

    /**
     * @return the user command for a specified server name
     * @param name JOnAS instance name
     */
    public String getJonasCmd4Server(String name){
        Server server = getServer(name);
        if (server != null) {
            return server.getJonasCmd();
        } else {
            return null;
        }
    }

    /**
     * @return the xprm for a specified server name
     * @param name JOnAS instance name
     */
    public String getXprm4Server(String name){
        Server server = getServer(name);
        if (server != null) {
            return server.getXprm();
        } else {
            return null;
        }
    }

    /**
     * @return the autoboot value for a specified server name
     * @param name JOnAS instance name
     */
    public String getAutoBoot4Server(String name){
        Server server = getServer(name);
        if (server != null) {
            return server.getAutoBoot();
        } else {
            return null;
        }
    }


    /**
     * @return the list of JOnAS instances
     */
    public String getServersList() {
        StringBuffer sb = new StringBuffer();
        JLinkedList servers = ClusterDaemonTools.getCurrentConfiguration().getClusterDaemon().getServerList();

        if (servers != null) {
            for (int i = 0; i < servers.size(); i++) {
                Server serverElement = (Server) servers.get(i);
                if (i != 0) {
                    sb.append(",");
                }
                sb.append(serverElement.getName());
            }
        }
        return sb.toString();
    }

    /**
     * Flush the configuration to the clusterd.xml file
     * @throws ClusterDaemonException if an error occurs
     */
    private void flushConfiguration() throws ClusterDaemonException {
        String buf = ClusterDaemonTools.getCurrentConfiguration().toXML();
        FileWriter fw;
        try {
            fw = new FileWriter(confFile);
            fw.write(buf);
            fw.close();
        } catch (IOException e) {
            throw new ClusterDaemonException(e);
        }

        this.logger.log(BasicLevel.DEBUG, "Configuration flushed in the file " + confFile + "\n" + buf);
    }

    /**
     * Add a server configuration (MBean interface)
     * @param name server name
     * @param description server description
     * @param javaHome JAVA_HOME dir
     * @param jonasBase JONAS_BASE dir
     * @param jonasRoot JONAS_ROOT dir
     * @param xprm extra JVM parameters
     * @param autoBoot automatic start
     * @param jonasCmd user command
     * @throws ClusterDaemonException if an error occurs
     */
    public void addServer(String name, String description, String javaHome, String jonasRoot, String jonasBase, String xprm, String autoBoot, String jonasCmd) throws ClusterDaemonException {

        // check if the server already exist
        if (getServer(name) != null) {
            throw new ClusterDaemonException("JOnAS instance " + name + "already exists");
        }
        JLinkedList servers = ClusterDaemonTools.getCurrentConfiguration().getClusterDaemon().getServerList();

        // add a server
        Server myServer = new Server();
        myServer.setName(name);
        myServer.setDescription(description);
        myServer.setJavaHome(javaHome);
        myServer.setJonasBase(jonasBase);
        myServer.setJonasRoot(jonasRoot);
        myServer.setXprm(xprm);
        myServer.setAutoBoot(autoBoot);
        myServer.setJonasCmd(jonasCmd);
        servers.add(myServer);
        ClusterDaemonTools.getCurrentConfiguration().getClusterDaemon().setServerList(servers);

        // flush the configuration
        flushConfiguration();

        this.logger.log(BasicLevel.DEBUG, "Server " + name + " added");

    }

    /**
     * Remove a server configuration (MBean interface)
     * @param name server name
     * @throws ClusterDaemonException if an error occurs
     */
    public void removeServer(String name) throws ClusterDaemonException {

        // Get the Server
        Server server = getServer(name);

        // check if the server exists
        if (server == null) {
            throw new ClusterDaemonException("JOnAS instance " + name + " doesn't exist");
        }

        // Remove the server
        JLinkedList servers = ClusterDaemonTools.getCurrentConfiguration().getClusterDaemon().getServerList();
        servers.remove(server);
        ClusterDaemonTools.getCurrentConfiguration().getClusterDaemon().setServerList(servers);


        // flush the configuration
        flushConfiguration();

        this.logger.log(BasicLevel.DEBUG, "Server " + name + " removed");

    }

    /**
     * Modify a server configuration (MBean interface)
     * @param name server name
     * @param description server description
     * @param javaHome JAVA_HOME dir
     * @param jonasBase JONAS_BASE dir
     * @param jonasRoot JONAS_ROOT dir
     * @param xprm extra JVM parameters
     * @param autoBoot automatic start
     * @param jonasCmd user command
     * @throws ClusterDaemonException if an error occurs
     */
    public void modifyServer(String name, String description, String javaHome, String jonasRoot, String jonasBase, String xprm, String autoBoot, String jonasCmd) throws ClusterDaemonException {

        // Get the Server
        Server myServer = getServer(name);

        // check if the server exists
        if (myServer == null) {
            throw new ClusterDaemonException("JOnAS instance " + name + " doesn't exist");
        }

        // Update the server
        myServer.setDescription(description);
        myServer.setJavaHome(javaHome);
        myServer.setJonasBase(jonasBase);
        myServer.setJonasRoot(jonasRoot);
        myServer.setXprm(xprm);
        myServer.setAutoBoot(autoBoot);
        myServer.setJonasCmd(jonasCmd);

        // flush the configuration
        flushConfiguration();

        this.logger.log(BasicLevel.DEBUG, "Server " + name + " updated");
    }

    /**
     * Reload the configuration (MBean interface)
     * @throws ClusterDaemonException if an error occurs
     */
    public void reloadConfiguration() throws ClusterDaemonException {

        ClusterDaemonTools.loadClusterDaemonConfiguration(confFile);

    }

    /**
     * Start a JOnAS instance (MBean interface)
     * @param name instance name
     * @param domainName domain name
     * @param prm extra parameters
     * @throws ClusterDaemonException if an error occurs
     */
    public void startJOnAS(String name, String domainName, String prm) throws ClusterDaemonException {
        doStartJOnAS(name, domainName, prm, true);
    }

    /**
     * Stop a JOnAS instance (MBean interface)
     * @param name instance name
     * @throws ClusterDaemonException if an error occurs
     */
    public void stopJOnAS(String name) throws ClusterDaemonException {
        doStopJOnAS(name);
    }

    /**
     * Ping a JOnAS instance (MBean interface)
     * @param name instance name
     * @return exit code of the ping (0 ok, 1 ko)
     * @throws ClusterDaemonException if an error occurs
    */
    public int pingJOnAS(String name) throws ClusterDaemonException {
        return doPingJOnAS(name, PING_SHORT_TIMEOUT);
    }

    /**
     * Start all the JOnAS instances configured with auto-reboot
     * @param domainName domain name
     * @param prm extra parameters
      * @return the nodes list with an indicator started/starting failed
     */
    public String startAllJOnAS(String domainName, String prm) {
        return doStartAllJOnAS(domainName, prm, true, true);
    }

    /**
     * Stop all the JOnAS instances
     * @return the nodes list with an indicator stopped/stopping failed
     */
    public String stopAllJOnAS() {
        return doStopAllJOnAS();
    }

    /**
     * Stop the current instance
     */
    public void stopClusterDaemon() {

        if (isStarted) {
            this.logger.log(BasicLevel.DEBUG, "Server not started");
        }

        isStarted = false;

        // Delay the exit of the JVM so the link client/server is not broken.
        // client call will return before the end of the JVM on server side.
        new Thread(new Runnable() {

            public void run() {
                try {
                    // Wait before exit
                    Thread.sleep(SLEEP_DELAY);
                } catch (InterruptedException ie) {
                    ie.printStackTrace();
                    throw new IllegalStateException("Cannot wait: " + ie.getMessage());
                }
                System.exit(0);
            }
        }).start();

        this.logger.log(BasicLevel.INFO, "Server stopped");
    }

    public String getName() {
        return name;
    }
}
