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

package org.ow2.jonas.jmx.internal;

import java.lang.reflect.Method;
import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Properties;

import javax.management.ObjectName;
import javax.management.remote.JMXConnectorFactory;
import javax.management.remote.JMXConnectorServer;
import javax.management.remote.JMXConnectorServerFactory;
import javax.management.remote.JMXServiceURL;
import javax.management.remote.MBeanServerForwarder;
import javax.naming.Context;

import org.objectweb.util.monolog.api.BasicLevel;
import org.objectweb.util.monolog.api.Logger;
import org.ow2.jonas.lib.bootstrap.JProp;
import org.ow2.jonas.lib.execution.ExecutionResult;
import org.ow2.jonas.lib.execution.IExecution;
import org.ow2.jonas.lib.execution.RunnableHelper;
import org.ow2.jonas.lib.util.JonasObjectName;
import org.ow2.jonas.lib.util.Log;
import org.ow2.jonas.naming.JNamingManager;
import org.ow2.jonas.registry.RegistryService;
import org.ow2.jonas.security.auth.spi.RoleBasedAuthorizationModule;
import org.ow2.jonas.service.JonasAlreadyStartedException;
import org.ow2.jonas.service.ServiceException;

/**
 * JMX Service implementation. Provides specific doStart() and doStop()
 * methods to start /stop JOnAS JMX Service.
 *
 * @author Adriana Danes
 * @author S. Ali Tokmen
 */
public class JOnASJMXService extends BaseJMXService {

    /**
     * Management loggers.
     */
    private static Logger logger = Log.getLogger(Log.JONAS_JMX_PREFIX);

    /**
     * MX4J CommonsLogger fully qualified Classname.
     */
    private static final String MX4J_COMMONS_LOGGER_CLASSNAME = "mx4j.log.CommonsLogger";

    /**
     * MX4J Log class.
     */
    private static final String MX4J_LOG_CLASSNAME = "mx4j.log.Log";

    /**
     * MX4J Logger class.
     */
    private static final String MX4J_LOGGER_CLASS = "mx4j.log.Logger";


    /**
     * Connector servers attached to the MBean server. We may have several
     * connector servers only if several communication protocols are used, as
     * defined by the carol configuration.
     */
    private JMXConnectorServer[] connectorServers = null;

    /**
     * The JMXServiceURLs the connector servers are actually listening on.
     */
    private JMXServiceURL[] connectorServerURLs = null;

    /**
     * JOnAS Registry Service.
     */
    private RegistryService registryService = null;

    /**
     * List of Connector ObjectName (1 / protocol).
     */
    private List<ObjectName> connectorObjectNames = null;

    /**
     * Naming Manager.
     */
    private JNamingManager namingManager = null;

    /**
     * Whether the JMX server is secured.
     */
    private boolean jmxSecured = false;

    /**
     * Authentication method used when JMX server secured.
     */
    private String authenticationMethod = null;

    /**
     * Authentication method's parameter used when JMX server secured.
     */
    private String authenticationParameter = null;

    /**
     * Authorization method used when JMX server secured.
     */
    private String authorizationMethod = null;

    /**
     * Authorization method's parameter used when JMX server secured.
     */
    private String authorizationParameter = null;

    /**
     * Test if MX4J CommonsLoggger class is present.
     * In this case, redirect MX4J logging to Jakarta Commons Logging.
     */
    private void initializeMX4JLoggingSystem() {

        Class<?> mx4jCommonsLoggerClass = null;
        try {
            ClassLoader loader = Thread.currentThread().getContextClassLoader();
            mx4jCommonsLoggerClass = loader.loadClass(MX4J_COMMONS_LOGGER_CLASSNAME);
            if (logger.isLoggable(BasicLevel.DEBUG)) {
                logger.log(BasicLevel.DEBUG, "Class " + MX4J_COMMONS_LOGGER_CLASSNAME + " founded");
            }
            Object o = mx4jCommonsLoggerClass.newInstance();

            // Load Log a Logger class
            Class<?> clazz = loader.loadClass(MX4J_LOG_CLASSNAME);
            Class<?> mx4jLoggerClass = loader.loadClass(MX4J_LOGGER_CLASS);

            // Then get method redirectTo
            Method m = clazz.getMethod("redirectTo", new Class[] {mx4jLoggerClass});
            m.invoke(clazz, new Object[] {o});
            if (logger.isLoggable(BasicLevel.DEBUG)) {
                logger.log(BasicLevel.DEBUG, "MX4J logging redirected to the Jakarta commons logger");
            }
        } catch (ClassNotFoundException cnfe) {
            if (logger.isLoggable(BasicLevel.DEBUG)) {
                logger.log(BasicLevel.DEBUG, "Class " + MX4J_COMMONS_LOGGER_CLASSNAME + " not found: " + cnfe);
            }
        } catch (Exception e) {
            if (logger.isLoggable(BasicLevel.WARN)) {
                logger.log(BasicLevel.WARN, "Problem with " + MX4J_COMMONS_LOGGER_CLASSNAME + " instance creation "
                                + e);
            }
        }
    }

    /**
     * Start the Service. Only need to create a RMI connector
     * @exception ServiceException the service could not be started
     */
    @Override
    public void doStart() throws ServiceException {
        logger.log(BasicLevel.DEBUG, "Starting JMX Service");

        // Init MX4J
        initializeMX4JLoggingSystem();

        // Don't forget to call super class
        super.doStart();

        // Server name
        String serverName = getJonasServerName();
        if (serverName == null) {
            throw new ServiceException("Cannot start JMX service. No serverName.");
        }

        // Store the default protocol name
        String defaultProtocolName = registryService.getDefaultProtocolName();

        try {

            // Create one or more connector servers
            // (cf. JSR 160, JMX Remote 1.0)
            // Create a JMXServiceURL and a JMXConnectorServer per protocol
            // As some protocols does not have associated connectors, the
            // 2 arrays below may have null values
            // ------------------------------------
            List<String> protocols = registryService.getActiveProtocolNames();
            int nbProtocols = protocols.size();
            connectorServerURLs = new JMXServiceURL[nbProtocols];
            connectorServers = new JMXConnectorServer[nbProtocols];
            connectorObjectNames = new ArrayList<ObjectName>();

            int index = 0;
            for (String name : protocols) {

                // Connector JNDI name
                String connectorName = null;

                // Use this protocol as the new default
                registryService.setDefaultProtocol(name);

                // Only IIOP do not use the rmi protocol in the URL
                String protocol = "rmi";

                URI carolURL = registryService.getProviderURL(name);
                String scheme = carolURL.getScheme();
                String host = carolURL.getHost();
                int port = registryService.getExportedObjectPort(name);

                String ictxFactory = registryService.getInitialContextFactoryName(name);

                Properties props = new Properties();

                // - Set the "jmx.remote.jndi.rebind" to ask the JMX impl to do
                // a rebind() rather than a bind() in the registry
                props.put("jmx.remote.jndi.rebind", "true");
                props.put(Context.INITIAL_CONTEXT_FACTORY, ictxFactory);
                props.put(Context.PROVIDER_URL, carolURL.toString());

                JMXServiceURL serviceURL = null;

                if (name.equals("jrmp")) {
                    connectorName = "jrmpconnector_" + serverName;
                } else if (name.equals("irmi")) {
                    connectorName = "irmiconnector_" + serverName;

                    // Add 1 to this port for IRMI as the JMX object will
                    // not use IRMI to bind but JRMP methods.
                    if (port != 0) {
                        port++;
                    }
                } else if (name.equals("iiop")) {
                    connectorName = "iiopconnector_" + serverName;
                    protocol = "iiop";

                    Object orb = null;

                    IExecution<Object> exec = new IExecution<Object>() {
                        public Object execute() throws Exception {
                            return getNamingManager().getInitialContext().lookup("java:comp/ORB");
                        }
                    };

                    ExecutionResult<Object> res = RunnableHelper.execute(getClass().getClassLoader(), exec);
                    if (res.hasException()) {
                        throw res.getException();
                    }
                    orb = res.getResult();

                    props.put("java.naming.corba.orb", orb);
                } else {
                    // ignore that unsupported protocol
                    continue;
                }

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

                    // Authentication, avoid NullPointerExceptions
                    if(authenticationMethod != null && authenticationParameter != null) {
                        if ("jmx.remote.x.password.file".equals(authenticationMethod)) {
                            // Make sure files get read using JONAS_BASE
                            props.put(authenticationMethod, JProp.getJonasBase() + fileSeparator + authenticationParameter);
                        } else {
                            props.put(authenticationMethod, authenticationParameter);
                        }
                    }

                    // Authorization, avoid NullPointerExceptions
                    if(authorizationMethod != null && authorizationParameter != null) {
                        if (authorizationMethod.startsWith("jmx.remote.x.access.rolebased")) {
                            final long timeout = System.currentTimeMillis() + 10000;
                            Throwable lastCause = null;
                            while (System.currentTimeMillis() < timeout) {
                                try {
                                    forwarder = RoleBasedAuthorizationModule.newProxyInstance(
                                            authorizationMethod.substring("jmx.remote.x.access.rolebased".length() + 1),
                                            authorizationParameter);
                                    break;
                                } catch (NoClassDefFoundError e) {
                                    // The JOnAS security service will always
                                    // start after JMX, since JMX is in the
                                    // jonas-autodeploy-bundles list.
                                    //
                                    // Wait for security service to start.
                                    //
                                    // See: http://jira.ow2.org/browse/JONAS-224
                                    lastCause = e;
                                    Thread.sleep(1000);
                                }
                            }
                            if (forwarder == null) {
                                throw new ServiceException("Role-based JMX security authorization cannot be initialized. "
                                    + "Please make sure the JOnAS security service is enabled.", lastCause);
                            }
                        } else if ("jmx.remote.x.access.file".equals(authorizationMethod)) {
                            // Make sure files get read using JONAS_BASE
                            props.put(authorizationMethod, JProp.getJonasBase() + fileSeparator + authorizationParameter);
                        } else {
                            props.put(authorizationMethod, authorizationParameter);
                        }
                    }
                }

                serviceURL = new JMXServiceURL(protocol, host, port, "/jndi/" + carolURL.toString() + "/" + connectorName);
                JMXConnectorServer connectorServer = JMXConnectorServerFactory.newJMXConnectorServer(serviceURL,
                                                                                                     (Map) props,
                                                                                                     null);
                if (forwarder != null) {
                    connectorServer.setMBeanServerForwarder(forwarder);

                    try {
                        if (forwarder.getMBeanServer() == null) {
                            // MBeanServerForwarder.getMBeanServer() will throw
                            // an IllegalStateException if the MBeanServer is
                            // null on a Sun JVM. This throw clause is to make
                            // sure that exception is always thrown.
                            throw new IllegalStateException();
                        }
                    } catch (IllegalStateException e) {
                        logger.log(BasicLevel.DEBUG, "Setting the forwarder's MBeanServer to " + this.getJmxServer());
                        forwarder.setMBeanServer(this.getJmxServer());
                    }
                }
                connectorServers[index] = connectorServer;
                // Create the MBean associated to the connector
                String connectorObjectName = "connector_" + name;
                ObjectName connectorServerName = JonasObjectName.jmxConnectorServer(scheme, connectorObjectName);
                connectorObjectNames.add(connectorServerName);

                // Unregister MBean is already registered
                if (getJmxServer().isRegistered(connectorServerName)) {
                    getJmxServer().unregisterMBean(connectorServerName);
                }
                getJmxServer().registerMBean(connectorServer, connectorServerName);
                // Start the JMXConnectorServer

                final int idx = index;
                final JMXConnectorServer server = connectorServer;
                IExecution<Void> exec = new IExecution<Void>() {

                    public Void execute() throws Exception {
                        server.start();
                        connectorServerURLs[idx] = server.getAddress();
                        if(jmxSecured) {
                            // Test security: try to connect without credentials
                            try {
                                JMXConnectorFactory.connect(connectorServerURLs[idx]).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");
                            }
                        } else {
                            logger.log(BasicLevel.INFO, "JMX security is disabled");
                        }
                        return null;
                    }

                };

                // Export the connector for only this protocol
                // So disable the MultiPRODelegate provided by Carol
                System.setProperty("carol.multipro.protocol", name);
                ExecutionResult<Void> res = RunnableHelper.execute(JOnASJMXService.class.getClassLoader(), exec);
                System.setProperty("carol.multipro.protocol", "any");

                if (res.hasException()) {
                    throw res.getException();
                }

                /*
                 * try { connectorServer.start(); connectorServerURLs[index] = connectorServer.getAddress(); } catch
                 * (IllegalArgumentException e) { throw e; }
                 */

                // increment
                index++;

            }
        } catch (javax.naming.NameAlreadyBoundException ne) {
            logger.log(BasicLevel.DEBUG, "Cannot start JMX service " + ne);
            throw new JonasAlreadyStartedException();
        } catch (Exception e) {
            throw new ServiceException("Cannot start JMX service", e);
        } finally {
            // Switch back to the old default
            registryService.setDefaultProtocol(defaultProtocolName);
        }

        String jmxConnectors = null;
        if (connectorServerURLs != null && connectorServerURLs.length == 1 && connectorServerURLs[0] == null) {
            jmxConnectors = ". No JMX Connector.";
        } else {
            jmxConnectors = " using connector(s)" + Arrays.asList(connectorServerURLs);
        }


        logger.log(BasicLevel.INFO, "JMX Service started" + jmxConnectors);

    }

    /**
     * Stop this service.
     */
    @Override
    public void doStop() {
        // Don't forget to call super class
        super.doStop();

        logger.log(BasicLevel.DEBUG, "Stopping JMX Service");

        if (connectorObjectNames != null) {
            // Unregister Connector MBeans
            for (final ObjectName name : connectorObjectNames) {
                try {
                    // Unregister mbean in an execution block
                    IExecution<Void> exec = new IExecution<Void>() {
                        public Void execute() throws Exception {
                            getJmxServer().unregisterMBean(name);
                            return null;
                        }
                    };
                    // Unexport the connector for only this protocol
                    // So disable the MultiPRODelegate provided by Carol
                    String protocol = name.getKeyProperty("protocol");
                    System.setProperty("carol.multipro.protocol", protocol);
                    // Execute
                    ExecutionResult<Void> result = RunnableHelper.execute(getClass().getClassLoader(), exec);
                    System.setProperty("carol.multipro.protocol", "any");
                    // Throw an Exception if needed
                    if (result.hasException()) {
                        throw result.getException();
                    }
                } catch (Exception e) {
                    logger.log(BasicLevel.ERROR, "Cannot unregister Connector MBean for '" + name + "'", e);
                }
            }

        }

        if (connectorServers != null) {
            // Desactivates the connector server, that is, stops listening for
            // client connections.
            // Calling this method will also close all client connections that
            // were made by this server.
            for (int i = 0; i < connectorServers.length; i++) {
                try {
                    connectorServers[i].stop();
                } catch (Exception e) {
                    logger.log(BasicLevel.INFO, "Cannot Stop JMX Connector", e);
                }
            }

        }

        logger.log(BasicLevel.INFO, "JMX Service stopped");
    }

    /**
     * @return The actual adresses on which listen the created connector servers
     */
    public JMXServiceURL[] getConnectorServerURLs() {
        return this.connectorServerURLs;
    }

    /**
     * @param registryService the registryService to set
     */
    public void setRegistryService(final RegistryService registryService) {
        this.registryService = registryService;
    }

    /**
     * @return the naming manager reference
     */
    public JNamingManager getNamingManager() {
        return namingManager;
    }

    /**
     * @param namingManager the namingManager to set
     */
    public void setNamingManager(final JNamingManager namingManager) {
        this.namingManager = namingManager;
    }

    /**
     * @param jmxSecured  Whether the JMX server is secured.
     */
    public void setJmxSecured(final boolean jmxSecured) {
        this.jmxSecured = jmxSecured;
    }

    /**
     * @param authenticationMethod  Authentication method used when JMX server
     *                              secured.
     */
    public void setAuthenticationMethod(final String authenticationMethod) {
        this.authenticationMethod = authenticationMethod;
    }

    /**
     * @param authenticationParameter  Authentication method's parameter used
     *                                 when JMX server secured.
     */
    public void setAuthenticationParameter(final String authenticationParameter) {
        this.authenticationParameter = authenticationParameter;
    }

    /**
     * @param authorizationMethod  Authorization method used when JMX server
     *                             secured.
     */
    public void setAuthorizationMethod(final String authorizationMethod) {
        this.authorizationMethod = authorizationMethod;
    }

    /**
     * @param authorizationParameter  Authorization method's parameter used
     *                                when JMX server secured.
     */
    public void setAuthorizationParameter(final String authorizationParameter) {
        this.authorizationParameter = authorizationParameter;
    }
}
