/*
 * RHQ Management Platform
 * Copyright (C) 2005-2009 Red Hat, Inc.
 * All rights reserved.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation version 2 of the License.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */
package org.jboss.on.common.jbossas;

import java.io.File;
import java.util.Properties;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.mc4j.ems.connection.ConnectionFactory;
import org.mc4j.ems.connection.EmsConnection;
import org.mc4j.ems.connection.settings.ConnectionSettings;
import org.mc4j.ems.connection.support.ConnectionProvider;
import org.mc4j.ems.connection.support.metadata.ConnectionTypeDescriptor;
import org.mc4j.ems.connection.support.metadata.InternalVMTypeDescriptor;

import org.rhq.core.domain.configuration.Configuration;
import org.rhq.core.domain.configuration.PropertySimple;

/**
 * Shared helper class to connect to a remote JMX server.
 *
 * @author Heiko W. Rupp
 */
public class JmxConnectionHelper {
    public static final String CONNECTOR_DESCRIPTOR_TYPE = "connectorDescriptorType";
    public static final String CONNECTOR_ADDRESS = "connectorAddress";
    public static final String CONNECTOR_PRINCIPAL = "connectorPrincipal";
    public static final String CONNECTOR_CREDENTIALS = "connectorCredentials";
    public static final String JBOSS_HOME_DIR = "jbossHomeDir";

    private static final Log log = LogFactory.getLog(JmxConnectionHelper.class);

    private static EmsConnection connection;
    private static Configuration configuration;

    private static final String JNP_DISABLE_DISCOVERY_JNP_INIT_PROP = "jnp.disableDiscovery";

    /**
     * This is the timeout for the initial connection to the MBeanServer that is made by the AS ResourceComponent's
     * start method.
     */
    private static final int JNP_TIMEOUT = 30 * 1000; // 30 seconds
    /**
     * This is the timeout for MBean attribute gets/sets and operations invoked on the remote MBeanServer.
     * NOTE: This timeout comes into play if the JBossAS instance has gone down since the original JNP connection was made.
     */
    private static final int JNP_SO_TIMEOUT = 15 * 1000; // 15 seconds

    /**
     * Controls the dampening of connection error stack traces in an attempt to control spam to the log file. Each time
     * a connection error is encountered, this will be incremented. When the connection is finally established, this
     * will be reset to zero.
     */
    private static int consecutiveConnectionErrors;

    private boolean copyConnectionLibraries;
    private File tmpDir;

    /**
     * Constructs a new connection helper.
     * 
     * @param copyConnectionLibraries whether to copy the libraries need for the connection so that
     * the ems classloader doesn't block the application access to them.
     * @param tmpDir the temporary directory to use when copying the libraries
     */
    public JmxConnectionHelper(boolean copyConnectionLibraries, File tmpDir) {
        this.copyConnectionLibraries = copyConnectionLibraries;
        this.tmpDir = tmpDir;
    }

    /**
     * Obtain an EmsConnection for the passed connection properties. The properties will be retained.
     * To create a connection with different properties, use this method again with a different set
     * of properties.
     * @param config Configuration properties for this connection
     * @return an EmsConnection or null in case of failure
     * @see #getEmsConnection()
     */
    public EmsConnection getEmsConnection(Configuration config) {
        EmsConnection emsConnection = null;
        configuration = config;

        try {
            emsConnection = loadConnection(config, copyConnectionLibraries, tmpDir);
        } catch (Exception e) {
            log.error("Component attempting to access a connection that could not be loaded");
        }

        return emsConnection;
    }

    /**
     * Obtain an EmsConnection. This will only work if the connection properties have passed
     * before via a call to {@link #getEmsConnection(Configuration)}
     * @return an EmsConnection or null in case of failure
     * @see #getEmsConnection(org.rhq.core.domain.configuration.Configuration)
     */
    public EmsConnection getEmsConnection() {
        EmsConnection emsConnection = null;
        if (configuration == null) {
            throw new RuntimeException("No configuration set");
        }

        try {
            emsConnection = loadConnection(configuration, copyConnectionLibraries, tmpDir);
        } catch (Exception e) {
            log.error("Component attempting to access a connection that could not be loaded");
        }

        return emsConnection;
    }

    /**
     * This is the preferred way to use a connection from within this class; methods should not access the connection
     * property directly as it may not have been instantiated if the connection could not be made.
     * <p/>
     * <p>If the connection has already been established, return the object reference to it. If not, attempt to make a
     * live connection to the JMX server.</p>
     * <p/>
     * <p>If the connection could not be made in the start(org.rhq.core.pluginapi.inventory.ResourceContext) method,
     * this method will effectively try to load the connection on each attempt to use it. As such, multiple threads may
     * attempt to access the connection through this means at a time. Therefore, the method has been made synchronized
     * on instances of the class.</p>
     * <p/>
     * <p>If any errors are encountered, this method will log the error, taking into account logic to prevent spamming
     * the log file. Calling methods should take care to not redundantly log the exception thrown by this method.</p>
     *
     * @param pluginConfig
     * @return live connection to the JMX server; this will not be <code>null</code>
     * @throws Exception if there are any issues at all connecting to the server
     */
    private static synchronized EmsConnection loadConnection(Configuration pluginConfig,
        boolean copyConnectionLibraries, File tmpDir) throws Exception {
        if (connection == null) {
            try {
                //Configuration pluginConfig = this.resourceContext.getPluginConfiguration();

                ConnectionSettings connectionSettings = new ConnectionSettings();

                String connectionTypeDescriptorClass = pluginConfig.getSimple(CONNECTOR_DESCRIPTOR_TYPE)
                    .getStringValue();
                PropertySimple serverUrl = pluginConfig.getSimple(CONNECTOR_ADDRESS);

                connectionSettings.initializeConnectionType((ConnectionTypeDescriptor) Class.forName(
                    connectionTypeDescriptorClass).newInstance());
                // if not provided use the default serverUrl
                if (null != serverUrl) {
                    connectionSettings.setServerUrl(serverUrl.getStringValue());
                }

                connectionSettings.setPrincipal(pluginConfig.getSimpleValue(CONNECTOR_PRINCIPAL, null));
                connectionSettings.setCredentials(pluginConfig.getSimpleValue(CONNECTOR_CREDENTIALS, null));
                connectionSettings.setLibraryURI(pluginConfig.getSimpleValue(JBOSS_HOME_DIR, null));

                if (connectionSettings.getAdvancedProperties() == null) {
                    connectionSettings.setAdvancedProperties(new Properties());
                }

                connectionSettings.getAdvancedProperties().setProperty(ConnectionFactory.USE_CONTEXT_CLASSLOADER,
                    "true");
                connectionSettings.getAdvancedProperties().setProperty(JNP_DISABLE_DISCOVERY_JNP_INIT_PROP, "true");

                // Make sure the timeout always happens, even if the JBoss server is hung.
                connectionSettings.getAdvancedProperties().setProperty("jnp.timeout", String.valueOf(JNP_TIMEOUT));
                connectionSettings.getAdvancedProperties().setProperty("jnp.sotimeout", String.valueOf(JNP_SO_TIMEOUT));

                // Tell EMS "don't bother creating your own classloader, I (the caller of EMS) already created
                // an isolated classloader for you, with all necessary jars - just use the context classloader
                // and don't bother creating a new one for yourself"
                connectionSettings.getAdvancedProperties().setProperty(ConnectionFactory.USE_CONTEXT_CLASSLOADER,
                    Boolean.TRUE.toString());

                if (copyConnectionLibraries) {
                    // Tell EMS to make copies of jar files so that the ems classloader doesn't lock
                    // application files (making us unable to update them)  Bug: JBNADM-670
                    connectionSettings.getControlProperties().setProperty(ConnectionFactory.COPY_JARS_TO_TEMP,
                        String.valueOf(Boolean.TRUE));
                }

                // Tell EMS to use the plugin's temp dir, so the PC will be able to clean it up and so an access control
                // policy can easily be defined for the dir. EMS will use this for connection libraries (i.e. client
                // jars) if COPY_JARS control prop is true, but it will also use it for the ems-mpl jar no matter what,
                // so we need to always set it.
                connectionSettings.getControlProperties().setProperty(ConnectionFactory.JAR_TEMP_DIR,
                    tmpDir.getAbsolutePath());

                connectionSettings.getAdvancedProperties().setProperty(InternalVMTypeDescriptor.DEFAULT_DOMAIN_SEARCH,
                    "jboss");

                ConnectionFactory connectionFactory = new ConnectionFactory();
                connectionFactory.discoverServerClasses(connectionSettings);

                ConnectionProvider connectionProvider = connectionFactory.getConnectionProvider(connectionSettings);
                connection = connectionProvider.connect();

                connection.loadSynchronous(false); // this loads all the MBeans

                consecutiveConnectionErrors = 0;

                if (log.isDebugEnabled())
                    log.debug("Successfully made connection to the remote server instance");
            } catch (Exception e) {

                // The connection will be established even in the case that the principal cannot be authenticated,
                // but the connection will not work. That failure seems to come from the call to loadSynchronous after
                // the connection is established. If we get to this point that an exception was thrown, close any
                // connection that was made and null it out so we can try to establish it again.
                if (connection != null) {
                    if (log.isDebugEnabled())
                        log.debug("Connection created but an exception was thrown. Closing the connection.", e);
                    connection.close();
                    connection = null;
                }

                // Since the connection is attempted each time it's used, failure to connect could result in log
                // file spamming. Log it once for every 10 consecutive times it's encountered.
                if (consecutiveConnectionErrors % 10 == 0) {
                    log.warn("Could not establish connection to the instance [" + (consecutiveConnectionErrors + 1)
                        + "] times.", e);
                }

                if (log.isDebugEnabled())
                    log.debug("Could not connect to the instance for resource ", e);

                consecutiveConnectionErrors++;

                throw e;
            }
        }

        return connection;
    }

    /**
     * If necessary attempt to close the EMS connection, then set this.connection null.  Synchronized ensure we play
     * well with loadConnection.
     */
    public synchronized void closeConnection() {
        if (connection != null) {
            try {
                connection.close();
            } catch (Exception e) {
                log.error("Error closing EMS connection: " + e);
            }
            connection = null;
        }
    }
}
