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

import java.rmi.RMISecurityManager;
import java.util.List;

import javax.management.MBeanServer;
import javax.management.MBeanServerFactory;
import javax.management.ObjectName;
import javax.management.modelmbean.ModelMBean;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.rmi.PortableRemoteObject;
import javax.security.jacc.PolicyContext;
import javax.security.jacc.PolicyContextException;
import javax.security.jacc.PolicyContextHandler;

import org.apache.commons.modeler.ManagedBean;
import org.apache.commons.modeler.Registry;
import org.objectweb.carol.util.configuration.ConfigurationRepository;
import org.objectweb.carol.util.configuration.ProtocolConfiguration;
import org.objectweb.jonas.adm.Adm;
import org.objectweb.jonas.adm.AdmInterface;
import org.objectweb.jonas.common.JProp;
import org.objectweb.jonas.common.Log;
import org.objectweb.jonas.common.LogManagement;
import org.objectweb.jonas.jmx.J2eeObjectName;
import org.objectweb.jonas.jmx.JonasObjectName;
import org.objectweb.jonas.management.JonasMBeanTools;
import org.objectweb.jonas.management.ReconfigManager;
import org.objectweb.jonas.management.j2eemanagement.J2EEDomain;
import org.objectweb.jonas.security.PolicyProvider;
import org.objectweb.jonas.security.interceptors.jrmp.ctxcheck.Initializer;
import org.objectweb.jonas.security.jacc.JPolicyContextHandler;
import org.objectweb.jonas.service.JonasAlreadyStartedException;
import org.objectweb.jonas.service.ServiceException;
import org.objectweb.jonas.service.ServiceManager;
import org.objectweb.jonas_lib.I18n;
import org.objectweb.jonas_lib.deployment.work.WorkCleaner;
import org.objectweb.jonas_lib.version.Version;
import org.objectweb.jotm.Current;
import org.objectweb.util.monolog.api.BasicLevel;
import org.objectweb.util.monolog.api.Logger;
import org.objectweb.util.monolog.wrapper.javaLog.MonologFormatter;

/**
 * This class implements an JOnAS Server.
 * @author Philippe Coq
 * @author Philippe Durieux
 * @author Jeff Mesnil
 * @author Regis Le Brettevillois - Libelis / JOnAS team - evidian Service as
 *         org.objectweb.jonas.service.Service 02/04/25
 * @author Sebastien Chassande-Barrioz sebastien.chassande@inrialpes.fr Monolog
 *         for traces
 * @author Adriana Danes 03/01/20: -Highlight configuration properties 03/03/..:
 *         -Force jmx service startup -Create and register the JonAS
 *         configuration MBean. This MBean manages JOnAS persistent
 * @author S. Ali Tokmen
           - Local JMX access don't create a remote connection anymore
 */
public class Server {


    /**
     * Property value for the default file
     */
    private static final String DEF_LOG_CONFIGFILE = "trace";

    /**
     * Internationalization
     */
    private static I18n i18n = I18n.getInstance(Server.class);

    /**
     * Property for the configuration log file
     */
    private static final String LOG_CONFIGFILE = "jonas.log.configfile";

    /**
     * Logger
     */
    private static Logger logger = null;

    /**
     * Property for the security propagation
     */
    private static final String SECURITY_PROPAGATION = "jonas.security.propagation";

    /**
     * Property for the Csiv2 propagation
     */
    private static final String CSIV2_PROPAGATION = "jonas.csiv2.propagation";

    /**
     * Property for the security manager
     */
    private static final String SECURITY_MANAGER = "jonas.security.manager";

    /**
     * Property for the security context check.
     */
    private static final String SEC_CHECK = "jonas.security.context.check";


    /**
     * Property for the master
     */
    private static final String IS_MASTER = "jonas.service.discovery.master";

    /**
     * J2EE Application Server Name
     */
    private static final String SERVERNAME = "JOnAS";

    /**
     * Default sleep value
     */
    private static final int SLEEP_VALUE = 10000;

    /**
     * Property for the transaction propagation
     */
    private static final String TRANSACTION_PROPAGATION = "jonas.transaction.propagation";

    /**
     * The Server singleton value
     */
    private static Server unique = null;

    /**
     * ID of the MBean server
     */
    private String idMbeanServer = null;

    /**
     * MBean Server
     */
    private MBeanServer mbeanServer = null;

    /**
     * Server start exception
     */
    private Exception startException = null;

    /**
     * True if I am the master on the domain
     */
    private boolean ismaster;

    /**
     * Get the unique instance. Create it at first call.
     * @return unique instance.
     */
    public static Server getInstance() {
        if (unique == null) {
            unique = new Server();
        }
        return unique;
    }

    /**
     * Server main routine. No args.
     * @param args the arguments of the server
     * @exception Exception can not start server
     */
    public static void main(String[] args) throws Exception {
        Server server = null;

        // Add Mx4J logger as wrapper (allow to put the right class for debug in
        // the log)
        // Also add carol class which print traces
        System.setProperty(MonologFormatter.WRAPPERS_PROPERTY, "mx4j.log.CommonsLogger,mx4j.log.Logger,org.ow2.util.log.JDKLogger, org.apache.juli.logging.DirectJDKLog,org.objectweb.carol.util.configuration.TraceCarol,org.objectweb.carol.cmi.configuration.TraceCmi,org.ow2.carol.util.configuration.TraceCarol");
        try {
            server = Server.getInstance();
            server.start();
        } catch (JonasAlreadyStartedException e) {
            // if we get this exception this means that
            // we are trying to restart the same JOnAS
            System.err.println(i18n.getMessage("Server.main.alreadyStarted", SERVERNAME));
            // rethrow JonasAlreadyStartedException
            throw e;
        } catch (Exception e) {
            // We don't use here the trace system.
            System.err.println("JOnAS error: " + e);
            e.printStackTrace(System.err);
            System.err.println(SERVERNAME + " halting");
            try {
                if (server != null) {
                    server.stop();
                }
            } catch (Exception he) {
                System.err.println("Exception starting JOnAS: " + e);
                e.printStackTrace(System.err);
                System.err.println("Exception during server.stop: " + he.getMessage());
                throw he;
            }
            throw e;
        }
    }

    /**
     * Associated Adm object
     */
    private static Adm adm = null;

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

    /**
     * J2EEServer associate
     */
    private J2EEServer j2EEServer = null;

    /**
     * J2EEDomain associated
     */
    private J2EEDomain j2eeDomain = null;

    /**
     * File for log configuration
     */
    private String logConfigFile;

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

    /**
     * Current server name (the server instance name)
     */
    private String serverName = null;

    /**
     * Current server's JONAS_BASE
     */
    private String jonasBase = null;
    /**
     * The associated ServiceManager for JOnAS
     */
    private ServiceManager sm = null;

    /**
     * Protocols list
     */
    private String srvProtocols= "";
    /**
     * Private constructor to force only 1 instance.
     * @throws Exception (for example if JProp can't read properties file)
     */
    private Server() {
        // read the JOnAS properties
        props = JProp.getInstance();

        // set configuration info into the Log System (monolog)
        logConfigFile = props.getValue(LOG_CONFIGFILE, DEF_LOG_CONFIGFILE);
        Log.configure(logConfigFile);

        // get a logger for server traces
        logger = Log.getLogger(Log.JONAS_SERVER_PREFIX);

        // create the service manager
        sm = ServiceManager.getInstance();

        // Check if I am the master
        ismaster = new Boolean(props.getValue(IS_MASTER, "false").trim()).booleanValue();

        // get JONAS_BASE
        jonasBase = props.getValue(JProp.JONAS_BASE);

        // prints JVM infos
        logger.log(BasicLevel.INFO, jvmInfos());

        // called only once for a JVM
        PolicyProvider.init();

    }

    /**
     * Gets J2EEDomain
     * @return J2EEDomain
     */
    public J2EEDomain getJ2EEDomain() {
        return j2eeDomain;
    }

    /**
     * Gets J2EEServer.
     * @return J2EEServer
     */
    public J2EEServer getJ2EEServer() {
        return j2EEServer;
    }

    /**
     * Initialize the MBean server
     * @throws Exception if MBean server cannot be found/created
     */
    private void initMBeanServer() throws Exception {

        // Set domain name for creating MBeans
        JonasObjectName.setDomain(domainName);

        boolean existingMbeanServer = false;
        // First, try to find an MBean Server
        List mbeanServers = MBeanServerFactory.findMBeanServer(null);
        if (mbeanServers.size() > 0) {
            mbeanServer = (MBeanServer) mbeanServers.get(0);
            existingMbeanServer = true;
        } else {
            // Need to create an MBean server
            mbeanServer =  MBeanServerFactory.createMBeanServer(domainName);
        }

        ObjectName mbeanServerDelegate = new ObjectName("JMImplementation:type=MBeanServerDelegate");
        idMbeanServer = (String) mbeanServer.getAttribute(mbeanServerDelegate, "MBeanServerId");
        if (existingMbeanServer) {
            logger.log(BasicLevel.INFO, "Use an existing MBean server with id '" + idMbeanServer + "'.");
        }
    }


    /**
     * Initialize interception and carol
     * @param props the JOnAS properties
     * @param serverName the name of the server for creating mbeans
     * @return list of protocols
     * @throws Exception if carol init failed
     */
    public String initCarol(JProp props, String serverName) throws Exception {

        String old = System.getProperty(Context.INITIAL_CONTEXT_FACTORY);

        // Intialize Carol
        ConfigurationRepository.init(domainName, serverName);

        // Initialize Interceptors
        String sec = props.getValue(SECURITY_PROPAGATION, "false").trim();
        String trans = props.getValue(TRANSACTION_PROPAGATION, "false").trim();
        String csiv2 = props.getValue(CSIV2_PROPAGATION, "true").trim();
        String secCheck = props.getValue(SEC_CHECK, "false").trim();

        if (sec.equals("true")) {
            // jrmp interceptors
            ConfigurationRepository.addInterceptors("jrmp",
                    "org.objectweb.jonas.security.interceptors.jrmp.SecurityInitializer");
            // cmi interceptors
            ConfigurationRepository.addInterceptors("cmi",
                    "org.objectweb.jonas.security.interceptors.jrmp.SecurityInitializer");
            // iiop interceptors flag
            ConfigurationRepository.addInterceptors("iiop",
                    "org.objectweb.jonas.security.interceptors.iiop.SecurityInitializer");

            // Assert security context ?
            if ("true".equals(secCheck)) {
                ConfigurationRepository.addInterceptors("jrmp", Initializer.class.getName());
                ConfigurationRepository.addInterceptors("cmi", Initializer.class.getName());
            }

        }

        if (csiv2.equals("true")) {
            ConfigurationRepository.addInterceptors("iiop", "org.objectweb.jonas.security.iiop.Csiv2Initializer");
        }


        if (trans.equals("true")) {
            // jrmp interceptors flag
            ConfigurationRepository.addInterceptors("jrmp", "org.objectweb.jotm.jta.rmi.JTAInterceptorInitializer");
            // jrmp interceptors flag
            ConfigurationRepository.addInterceptors("cmi", "org.objectweb.jotm.jta.rmi.JTAInterceptorInitializer");
            // iiop interceptors flag
            ConfigurationRepository.addInterceptors("iiop", "org.objectweb.jotm.ots.OTSORBInitializer");
        }

        // Activate HA interceptors if CMI is enabled (used both for ha service and load-balancing)
        // HA context embedds the update views.
        ConfigurationRepository.addInterceptors("cmi", "org.objectweb.jonas.ha.interceptor.HAInterceptorInitializer");

        // Set carol mode in server mode (will use fixed port if set)
        System.setProperty("carol.server.mode", "true");

        // get Carol configuration
        // Should go as Carol MBean
        String protocols = "";
        String prefix = "rmi/";
        ProtocolConfiguration[] protocolConfigurations = ConfigurationRepository.getConfigurations();
        for (int c = 0; c < protocolConfigurations.length; c++) {
            if (protocols == "") {
                protocols = prefix + protocolConfigurations[c].getName();
            } else {
                protocols += "," +  prefix + protocolConfigurations[c].getName();
            }
        }

        if (null != old) {
            System.setProperty(Context.INITIAL_CONTEXT_FACTORY, old);
        }

        return protocols;
    }

    /**
     * Initialize the context handlers (JACC)
     * @throws PolicyContextException if handlers can't be registered
     */
    private void initJACCPolicyContextHandlers() throws PolicyContextException {
        PolicyContextHandler policyContextHandler = new JPolicyContextHandler();
        String[] keys = policyContextHandler.getKeys();
        for (int k = 0; k < keys.length; k++) {
            PolicyContext.registerHandler(keys[k], policyContextHandler, true);
        }
    }

    /**
     * Stop the server and stop the JVM (MBean method)
     * @throws Exception if the server can't be destroyed
     */
    public void kill() throws Exception {
        if (adm != null) {
            adm.killServer();
        }
    }

    /**
     * Start the server
     * @throws Exception if server can't be started
     */
    public void start() throws Exception {

        // Server name
        serverName = props.getValue(JProp.JONAS_NAME, JProp.JONAS_DEF_NAME);

        // Domain name
        domainName = props.getValue(JProp.DOMAIN_NAME, JProp.JONAS_DEF_NAME);

       Thread srvThread = new Thread() {
              public void run() {
                try {

                    initMBeanServer();

                    // Initialize Context Interceptors and Carol
                    srvProtocols = initCarol(props, serverName);

                    // RMI Security Manager
                    boolean useSecurityManager = new Boolean(props.getValue(SECURITY_MANAGER, "true").trim()).booleanValue();

                    if (useSecurityManager) {
                        if (System.getSecurityManager() == null) {
                            System.setSecurityManager(new RMISecurityManager());
                        }
                    } else {
                        logger.log(BasicLevel.INFO, "Security manager is not set.");
                    }

                    // Initialize Policy Context handlers (JACC)
                    try {
                        initJACCPolicyContextHandlers();
                    } catch (PolicyContextException pce) {
                        logger.log(BasicLevel.ERROR, "Could not register JOnAS Policy Context Handlers");
                        throw pce;
                    }

                    // First of all, start registry service
                    sm.startRegistry();

                    // then, start JMX service (start an MBean server and create RMI
                    // connector for it registered in JNDI)
                    sm.startJmx(idMbeanServer);

                    // start the cleaner thread (deleting unused working files)
                    WorkCleaner.getInstance().start();

                    // Get the MBean register (and load mbeans-descriptors)
                    Registry oRegistry = JonasMBeanTools.getRegistry();

                    // Create and register a J2EE MBean for the domain management
                    ObjectName domainOn = null;
                    try {
                        // Create a JMX object name for the Domain MBean
                        domainOn = J2eeObjectName.J2EEDomain(domainName);
                        j2eeDomain = new J2EEDomain(domainOn.toString());
                        ManagedBean oManaged = oRegistry.findManagedBean("J2EEDomain");
                        ModelMBean oMBean = oManaged.createMBean(j2eeDomain);
                        mbeanServer.registerMBean(oMBean, domainOn);
                    } catch (javax.management.InstanceAlreadyExistsException ae) {
                        // should never occur
                        logger.log(BasicLevel.ERROR, ae);
                    } catch (Exception e) {
                        logger.log(BasicLevel.WARN, "Could not register J2EEDomain MBean");
                    }

                    // If this server is a master, the Domain MBean
                    // is set master, which enables domain monitoring
                    if (checkDiscoveryEnabled() && ismaster) {
                        j2eeDomain.setMaster();
                    }

                    // Create and register necessary MBeans for the server management
                    ObjectName serverOn = null;
                    try {
                        // Create and register a MBean allowing server reconfiguration
                        ReconfigManager reconfigManagerMBean = new ReconfigManager(props.getConfigFileEnv(), mbeanServer);
                        mbeanServer.registerMBean(reconfigManagerMBean, JonasObjectName.serverConfig());

                        // Create and register a MBean allowing the JOnAS server management
                        serverOn = J2eeObjectName.J2EEServer(domainName, serverName);
                        // Get the VERSIONS file content
                        String versions = JProp.getInstance("VERSIONS").getVersionFile();
                        j2EEServer = new J2EEServer(serverOn.toString(), unique, serverName, Version.getNumber(), Version.VENDOR,
                                srvProtocols, versions);
                        j2EEServer.setJonasBase(jonasBase);
                        ManagedBean oManaged = oRegistry.findManagedBean("J2EEServer");
                        ModelMBean oMBean = oManaged.createMBean(j2EEServer);
                        mbeanServer.registerMBean(oMBean, serverOn);

                        // Add local server to the J2EEDomain
                        j2eeDomain.addLocalServer(j2EEServer);

                        // Create and register a MBean for the logging service management
                        LogManagement logMBean = LogManagement.getInstance();
                        mbeanServer.registerMBean(logMBean, JonasObjectName.logService(logConfigFile));
                    } catch (Exception e) {
                        logger.log(BasicLevel.WARN, "Could not create J2EEServer MBean : " + e.getMessage(), e);
                    }

                    // Create the MBean for the corresponding JVM
                    ObjectName onJvm = null;
                    try {
                        onJvm = J2eeObjectName.JVM(domainName, serverName, serverName);
                        ManagedBean oManaged = oRegistry.findManagedBean("JVM");
                        JavaVm oJvm = new JavaVm(onJvm.toString(), props);
                        ModelMBean oMBean = oManaged.createMBean(oJvm);
                        mbeanServer.registerMBean(oMBean, onJvm);
                        // Update J2EEServer MBean
                        j2EEServer.addJavaVM(onJvm.toString());
                    } catch (Exception e) {
                        logger.log(BasicLevel.WARN, "Could not create JVM MBean : " + e.getMessage(), e);
                    }

                    // Creates the Adm instance for remote administration
                    try {
                        adm = new Adm(props);
                    } catch (Exception e) {
                        e.printStackTrace();
                        throw new Exception("Cannot create Adm : " + e);
                    }

                    // start all other services
                    sm.startServices();

                    // Add local server to the J2EEDomain
                    j2eeDomain.setLocalServerRunning();

                    // TM Server or EJB Server ?
                    boolean isEJB = true;
                    try {
                        sm.getEjbService();
                    } catch (ServiceException se) {
                        isEJB = false;
                    }
                    adm.serverReady(isEJB);

                    /* Do recovery right before saying JOnAS has started, if the transaction
                     * service has been configured.  This is done at the end of all the service
                     * startups, since by then the resource adapters loaded by the Resource
                     * service have been registered with JOTM and JOTM recovery can be done
                     * in a performant manner against all the resource adapters at once.
                     */
                    boolean isTM = true;
                    try {
                        sm.getTransactionService();
                    } catch (ServiceException se) {
                        isTM = false;
                    }
                    if (isTM) {
                        // Start recovery
                        try {
                            Current.getTransactionRecovery().startResourceManagerRecovery();
                        } catch (Exception ex) {
                            if (logger.isLoggable(BasicLevel.DEBUG)) {
                                logger.log(BasicLevel.DEBUG, "JOTM startResourceManagerRecovery failed: ", ex);
                            }
                            throw new Exception("Cannot start JOTM resource manager recovery", ex);
                        }
                     }
                } catch (Throwable e) {
                    startException = new Exception(e);
                     return;
                }

                System.out.println(i18n.getMessage("Server.start.serverIsReady", serverName, Version.getNumber()));
                logger.log(BasicLevel.INFO, i18n.getMessage("Server.start.serverStarted", serverName, srvProtocols));

                // Don't stop this thread to keep the server alive.

                while (true) {
                    try {
                        Thread.sleep(SLEEP_VALUE);
                    } catch (Exception e) {
                        return;
                    }
                }
            }
        };
        logger.log(BasicLevel.DEBUG, "Current Thread classloader: " + Thread.currentThread().getContextClassLoader());
        srvThread.setName(serverName + " Main");
        srvThread.setContextClassLoader(Thread.currentThread().getContextClassLoader());
        logger.log(BasicLevel.DEBUG, "New Thread classloader: " + srvThread.getContextClassLoader());
        srvThread.start();

        // Only exit when JOnAS has completely started
        while (adm == null && srvThread.isAlive()) {
            Thread.sleep(50);
        }

        if (adm != null && startException == null) {
            while (startException == null && adm.getServerState() != Adm.READY) {
                Thread.sleep(250);
            }
        }
        if (startException != null) {
            logger.log(BasicLevel.INFO, i18n.getMessage("Server.start.errorStarting", serverName, startException));
            throw new Exception("Error in Server start", startException);
        }
    }

    /**
     * @return information on JVM currently used
     */
    public static String jvmInfos() {
        StringBuffer sb = new StringBuffer();
        sb.append("JVM used is ");
        sb.append(System.getProperty("java.version"));
        sb.append(" version of ");
        sb.append(System.getProperty("java.vm.name"));
        sb.append("-");
        sb.append(System.getProperty("java.vm.version"));
        sb.append("/");
        sb.append(System.getProperty("java.vendor"));
        sb.append(" vendor on ");
        sb.append(System.getProperty("os.name"));
        sb.append(" ");
        sb.append(System.getProperty("os.version"));
        sb.append("/");
        sb.append(System.getProperty("os.arch"));
        sb.append(" OS.");
        return sb.toString();
    }

    /**
     * Stop the server without stopping the JVM
     * @throws Exception if the server can't be stopped
     */
    public void stop() throws Exception {
        if (adm != null) {
            System.out.println(i18n.getMessage("Server.stop.serverIsStopped", serverName));
            logger.log(BasicLevel.INFO, i18n.getMessage("Server.stop.serverStopped", serverName));
            adm.stopServer();
        }
    }

    /**
     * @return Returns the status of the server started or not.
     */
    public static boolean isStarted() {
        return unique != null;
    }

    /**
     * check the server state
     * @param jonasName JOnAS instance name
     * @return true if reachable
     */
    private boolean checkServer(String jonasName) {

        // Gets reference on Adm object
        boolean reachable = false;
        String admName = jonasName + Adm.ADMNAME_SUFFIX;
        try {
            InitialContext initialContext = new InitialContext();
            AdmInterface admI = (AdmInterface) PortableRemoteObject.narrow(initialContext.lookup(admName), AdmInterface.class);
            int state = admI.getServerState();
            reachable = (state == Adm.READY);
        } catch (Exception e) {
            reachable = false;
        }
        return reachable;
    }

    /**
     * @return true if the discovery service is present in the services list
     */
    private boolean checkDiscoveryEnabled() {
        boolean discoveryEnabled = false;
        String[] services = JProp.getInstance().getValueAsArray("jonas.services");
        for (int i = 0; i < services.length; i++) {
            if ("discovery".equals(services[i])) {
                discoveryEnabled = true;
            }
        }
        return discoveryEnabled;
    }
}
