/**
 * 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.jmx;

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

import javax.management.MBeanServerConnection;
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 javax.naming.InitialContext;

import org.objectweb.carol.rmi.util.PortNumber;
import org.objectweb.carol.util.configuration.CarolDefaultValues;
import org.objectweb.carol.util.configuration.ConfigurationRepository;
import org.objectweb.carol.util.configuration.ProtocolConfiguration;
import org.objectweb.jonas.common.JProp;
import org.objectweb.jonas.common.Log;
import org.objectweb.jonas.management.j2eemanagement.J2EEDomain;
import org.objectweb.jonas.security.auth.spi.RoleBasedAuthorizationModule;
import org.objectweb.jonas.service.JonasAlreadyStartedException;
import org.objectweb.jonas.service.ServiceException;
import org.objectweb.util.monolog.api.BasicLevel;
import org.objectweb.util.monolog.api.Logger;

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

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

    /**
     * The JNDI name of the JOnAS RMI connector allowing to access the
     * MBeanServer remotely. Use of this connector was needed before the
     * definition of the Connectors in JMX (before JMX 1.2)
     */
    private String rmiConnectorName = null;

    /**
     * @return The JNDI name of the JOnAS RMI connector
     */
    public String getRmiConnectorName() {
        return this.rmiConnectorName;
    }

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

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

    /**
     * Init the logger and then use super method.
     * @param ctx configuration for the init method
     * @throws ServiceException if initialization failed
     */
    public void doInit(Context ctx) throws ServiceException {
        // Test if MX4J CommonsLoggger class is present.
        // In this case, redirect MX4J logging to Jakarta Commons Logging.
        Class mx4jCommonsLoggerClass = null;
        try {
            mx4jCommonsLoggerClass = Thread.currentThread().getContextClassLoader().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 = Thread.currentThread().getContextClassLoader().loadClass(MX4J_LOG_CLASSNAME);
            Class mx4jLoggerClass = Thread.currentThread().getContextClassLoader().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);
            }
        }
        super.doInit(ctx);
    }

    /**
     * Start the Service. Only need to create a RMI connector
     * @exception ServiceException the service could not be started
     */
    public void doStart() throws ServiceException {
        String serverName = getJonasServerName();
        try {
            // Create a RMI Connector for the JMX agent using the JOnAS
            // RMIConnector class
            // --------------------------------------------------------------------------
            RMIConnector rmiConnector = new RMIConnectorImpl(getJmxServer());
            // Register the connector in JNDI
            InitialContext ictx = new InitialContext();
            rmiConnectorName = "RMIConnector_" + serverName;
            ictx.rebind(rmiConnectorName, rmiConnector);
            ictx.close();

            // 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
            // ------------------------------------
            int nbProtocols = ConfigurationRepository.getActiveConfigurationsNumber();
            connectorServerURLs = new JMXServiceURL[nbProtocols];
            connectorServers = new JMXConnectorServer[nbProtocols];
            // Determine protocols used by Carol and their configuration
            ProtocolConfiguration[] protocolConfigurations = ConfigurationRepository.getConfigurations();
            String serviceURL = null;
            for (int i = 0; i < protocolConfigurations.length; i++) {
                ProtocolConfiguration protocolConfiguration = protocolConfigurations[i];
                ConfigurationRepository.setCurrentConfiguration(protocolConfiguration);
                String carolProtocol = protocolConfiguration.getName();
                String providerUrl = protocolConfiguration.getProviderURL();
                URI carolURL = new URI(providerUrl);
                String host = carolURL.getHost();
                String port = String.valueOf(carolURL.getPort());
                String scheme = carolURL.getScheme();
                String ictxFactory = protocolConfiguration.getProtocol().getInitialContextFactoryClassName();
                Properties props = new Properties();
                if (scheme.equals("rmi") && carolProtocol.equals("jrmp")) {
                    // Treat RMI/JRMP cas
                    String myName = "jrmpconnector_" + serverName;
                    serviceURL = "service:jmx:rmi://" + host;
                    int jrmpExportedPort = 0;
                    // Add port number of exported objects if one is set by
                    // carol.
                    String propertyName = CarolDefaultValues.SERVER_JRMP_PORT;
                    Properties p = ConfigurationRepository.getProperties();
                    if (p != null) {
                        jrmpExportedPort = PortNumber.strToint(p.getProperty(propertyName, "0"), propertyName);
                    }
                    if (jrmpExportedPort > 0) {
                        serviceURL += ":" + jrmpExportedPort;
                    }
                    serviceURL += "/jndi/rmi://" + host + ":" + port + "/" + myName;

                    props.put(Context.INITIAL_CONTEXT_FACTORY, ictxFactory);
                    props.put(Context.PROVIDER_URL, providerUrl);
                } else if (scheme.equals("rmi") && carolProtocol.equals("irmi")) {
                    // Treat RMI/IRMI cas
                    String myName = "irmiconnector_" + serverName;
                    serviceURL = "service:jmx:rmi://" + host;
                    int irmiExportedPort = 0;
                    // Add port number of exported objects if one is set by
                    // carol.
                    String propertyName = CarolDefaultValues.SERVER_IRMI_PORT;
                    Properties p = ConfigurationRepository.getProperties();
                    if (p != null) {
                        irmiExportedPort = PortNumber.strToint(p.getProperty(propertyName, "0"), propertyName);
                        // Add 1 to this port for IRMI as the JMX object will
                        // not use IRMI to bind but JRMP methods.
                        irmiExportedPort++;
                    }
                    if (irmiExportedPort > 1) {
                        serviceURL += ":" + irmiExportedPort;
                    }
                    serviceURL += "/jndi/rmi://" + host + ":" + port + "/" + myName;
                    props.put(Context.INITIAL_CONTEXT_FACTORY, ictxFactory);
                    props.put(Context.PROVIDER_URL, providerUrl);
                } else if (scheme.equals("jrmi")) { // Treat JEREMIE case
                    String myName = "jeremieconnector_" + serverName;
                    serviceURL = "service:jmx:rmi://" + host;
                    int jeremieExportedPort = 0;
                    // Add port number of exported objects if one is set by
                    // carol.
                    String propertyName = CarolDefaultValues.SERVER_JEREMIE_PORT;
                    Properties p = ConfigurationRepository.getProperties();
                    if (p != null) {
                        jeremieExportedPort = PortNumber.strToint(p.getProperty(propertyName, "0"), propertyName);
                        // Add 1 to this port for jeremie as the JMX object will
                        // not use jeremie to bind but JRMP methods.
                        jeremieExportedPort++;
                    }
                    if (jeremieExportedPort > 1024) {
                        serviceURL += ":" + jeremieExportedPort;
                    }
                    serviceURL += "/jndi/jrmi://" + host + ":" + port + "/" + myName;

                    props.put(Context.INITIAL_CONTEXT_FACTORY, ictxFactory);
                    props.put(Context.PROVIDER_URL, providerUrl);
                } else if (scheme.equals("cmi")) {
                    // Treat CMI case
                    String myName = "cmiconnector_" + serverName;
                    serviceURL = "service:jmx:rmi://" + host;

                    int jrmpExportedPort = 0;
                    // Add port number of exported objects if one is set by
                    // carol.
                    String propertyName = CarolDefaultValues.SERVER_JRMP_PORT;
                    Properties p = ConfigurationRepository.getProperties();
                    if (p != null) {
                        jrmpExportedPort = PortNumber.strToint(p.getProperty(propertyName, "0"), propertyName);
                    }
                    if (jrmpExportedPort > 0) {
                        serviceURL += ":" + jrmpExportedPort;
                    }
                    serviceURL += "/jndi/cmi://" + host + ":" + port + "/" + myName;

                    props.put(Context.INITIAL_CONTEXT_FACTORY, ictxFactory);
                    props.put(Context.PROVIDER_URL, providerUrl);

                } else if (scheme.equals("iiop")) {
                    // Treat RMI/IIOP case
                    String myName = "iiopconnector_" + serverName;
                    // serviceURL = "service:jmx:iiop://" + host + "/jndi/" +
                    // myName;

                    serviceURL = "service:jmx:iiop://" + host + "/jndi/iiop://" + host + ":" + port + "/" + myName;

                    props.put("java.naming.corba.orb", new InitialContext().lookup("java:comp/ORB"));
                } else {
                    // Currently do not create connectors for other protocols.
                    connectorServerURLs[i] = null;
                    continue;
                }

                JMXServiceURL url = new JMXServiceURL(serviceURL);

                // 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");

                // Set JMX authentication and authorization options
                // We'll test later if JMX has really been secured
                final boolean jmxSecured = "true".equals(JProp.getInstance().getValue("jonas.service.jmx.secured"));
                MBeanServerForwarder forwarder = null;
                if(jmxSecured) {
                    String fileSeparator = System.getProperty("file.separator");

                    // Authentication, avoid NullPointerExceptions
                    String loginModule = JProp.getInstance().getValue("jonas.service.jmx.authentication.method");
                    String loginParam = JProp.getInstance().getValue("jonas.service.jmx.authentication.parameter");
                    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 = JProp.getInstance().getValue("jonas.service.jmx.authorization.method");
                    String authParam = JProp.getInstance().getValue("jonas.service.jmx.authorization.parameter");
                    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);
                        }
                    }
                }

                // Cast to Map is required for JDK 1.5
                JMXConnectorServer connectorServer =
                    JMXConnectorServerFactory.newJMXConnectorServer(url, (Map) props, null);
                if(forwarder != null) {
                    connectorServer.setMBeanServerForwarder(forwarder);
                }
                connectorServers[i] = connectorServer;

                // Create the MBean associated to the connector
                String connectorObjectName = "connector_" + carolProtocol;
                ObjectName connectorServerName = JonasObjectName.jmxConnectorServer(scheme, connectorObjectName);
                getJmxServer().registerMBean(connectorServer, connectorServerName);

                // Start the JMXConnectorServer
                try {
                    connectorServer.start();
                    connectorServerURLs[i] = connectorServer.getAddress();

                    if(jmxSecured) {
                        // Test security: try to connect without credentials
                        try {
                            JMXConnectorFactory.connect(connectorServerURLs[i]).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");
                    }
                } catch (IllegalArgumentException e) {
                    throw e;
                }
            }
        } catch (javax.naming.NameAlreadyBoundException e) {
            logger.log(BasicLevel.DEBUG, "Cannot start JMX service " + e);
            throw new JonasAlreadyStartedException();
        } catch (Exception e) {
            logger.log(BasicLevel.WARN, "Cannot start JMX service " + e);
            throw new ServiceException("Cannot start JMX service", e);
        }

        // set back default.
        ConfigurationRepository.setCurrentConfiguration(ConfigurationRepository.getDefaultConfiguration());
    }

    /**
     * Stop this service
     */
    public void doStop() {
        try {
            InitialContext ictx = new InitialContext();
            ictx.unbind("RMIConnector_" + getJonasServerName());
            ictx.close();

            // 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.
            // Being a potentialy slow operation, we keep it commented for the
            // moment
            for (int i = 0; i < connectorServers.length; i++) {
                connectorServers[i].stop();
            }
        } catch (Exception e) {
            logger.log(BasicLevel.ERROR, "Cannot Unbind Jmx RMI Connector" + e);
        }
        // Unregister some MBeans
        ObjectName domainOn = J2eeObjectName.J2EEDomain(getDomainName());
        try {
            getJmxServer().unregisterMBean(domainOn);
        } catch (Exception e) {
            logger.log(BasicLevel.INFO, "Cannot unregister JEEDomain MBean:" + domainOn.toString());
        }
        ObjectName serverOn = J2eeObjectName.J2EEServer(getDomainName(), getJonasServerName());
        try {
            getJmxServer().unregisterMBean(serverOn);
        } catch (Exception e) {
            logger.log(BasicLevel.INFO, "Cannot unregister JEEServer MBean:" + serverOn.toString());
        }
        // Remove internal references to the MBeanServer.
        releaseJmxServer();

        if (logger.isLoggable(BasicLevel.DEBUG)) {
            logger.log(BasicLevel.DEBUG, "JMX Service stopped");
        }
    }

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

    /**
     * Extract the protocol from a JMX connector URL
     * @param url JMX connector url to parse
     * @return protocol associated with the url
     */
    public static String getProtocolFromJmxConnectorUrl(String url) {

        int proIndMin = url.indexOf("/jndi/") + "/jndi/".length();
        int proIndMax = url.indexOf("://");
        String protocol = url.substring(proIndMin, proIndMax);

        // for the RMI protocol, we have to parse the connector name to identify
        // the underlaying
        // protocol : either jrmp or irmi
        if (protocol.equals("rmi")) {
            String subUrl = url.substring(proIndMax + "://".length());
            int subProIndMin = subUrl.indexOf("/") + "/".length();
            int subProIndMax = subUrl.indexOf("connector_");
            protocol = subUrl.substring(subProIndMin, subProIndMax);
        }

        return protocol;
    }

    /**
     * Get the provider url from a JMX connector URL
     * @param url JMX connector url to parse
     * @return provider url associated with the url
     */
    public static String getProviderUrlFromJmxConnectorUrl(String url) {

        int proIndMin = url.indexOf("/jndi/") + "/jndi/".length();
        String protocol = getProtocolFromJmxConnectorUrl(url);
        int urlIndMax = url.indexOf("/" + protocol + "connector");
        return url.substring(proIndMin, urlIndMax);
    }

    /**
     * Get the MBeanServerConnection for the named server.
     * Kept for backward compatibility (package org.allesta.wsabi.j2ee.provider.jonas)
     * @deprecated Replaced by J2EEDomain.getConnection()
     * @param serverName
     * @return MBeanServerConnection
     */
    public MBeanServerConnection getServerConnection(String servername) {
        return J2EEDomain.getInstance().getConnection(servername);
    }
}
