/*
 * RHQ Management Platform
 * Copyright (C) 2005-2008 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.rhq.plugins.jdbctrace;

import java.io.File;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.jboss.byteman.agent.submit.Submit;

import org.rhq.core.domain.configuration.Configuration;
import org.rhq.core.domain.configuration.ConfigurationUpdateStatus;
import org.rhq.core.domain.measurement.AvailabilityType;
import org.rhq.core.pluginapi.configuration.ConfigurationFacet;
import org.rhq.core.pluginapi.configuration.ConfigurationUpdateReport;
import org.rhq.core.pluginapi.inventory.ResourceComponent;
import org.rhq.core.pluginapi.inventory.ResourceContext;
import org.rhq.core.pluginapi.operation.OperationFacet;
import org.rhq.core.pluginapi.operation.OperationResult;
import org.rhq.core.util.exception.ThrowableUtil;
import org.rhq.plugins.byteman.BytemanAgentComponent;

/**
 * Component that can trace JDBC calls via byte-code maniuplation.
 * 
 * @author John Mazzitelli
 */
public class JdbcTracerComponent implements ResourceComponent<BytemanAgentComponent>, OperationFacet,
    ConfigurationFacet {

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

    private Configuration resourceConfiguration;
    private ResourceContext<BytemanAgentComponent> resourceContext;

    public void start(ResourceContext<BytemanAgentComponent> context) {
        this.resourceContext = context;

        try {
            prepareRules(true);
        } catch (Exception e) {
            log.warn("Failed to prepare the jdbc trace rules in the remote byteman agent", e);
        }

        return;
    }

    public void stop() {
        this.resourceContext = null;
    }

    public AvailabilityType getAvailability() {
        try {
            prepareRules(false);
            // the above always makes sure the remote byteman agent has the rules loaded; so we can always return UP here
            return AvailabilityType.UP;
        } catch (Exception e) {
            log.debug("Failed to prepare the jdbc trace rules in the remote byteman agent during avail check", e);
            return AvailabilityType.DOWN;
        }
    }

    public OperationResult invokeOperation(String name, Configuration configuration) {
        OperationResult result = new OperationResult();

        try {
            if ("refresh".equals(name)) {
                prepareRules(true);
            } else {
                throw new UnsupportedOperationException(name);
            }
        } catch (Exception e) {
            result.setErrorMessage(ThrowableUtil.getAllMessages(e));
        }

        return result;
    }

    public Configuration loadResourceConfiguration() {
        // here we simulate the loading of the managed resource's configuration

        if (resourceConfiguration == null) {
            // for this example, we will create a simple dummy configuration to start with.
            // note that it is empty, so we're assuming there are no required configs in the plugin descriptor.
            resourceConfiguration = new Configuration();
        }

        Configuration config = resourceConfiguration;

        return config;
    }

    public void updateResourceConfiguration(ConfigurationUpdateReport report) {
        // this simulates the plugin taking the new configuration and reconfiguring the managed resource
        resourceConfiguration = report.getConfiguration().deepCopy();

        report.setStatus(ConfigurationUpdateStatus.SUCCESS);
    }

    /**
     * Returns <code>true</code> if this component's plugin configuration tells us the JDBC
     * tracing is to be enabled. If <code>true</code>, the remote Byteman agent will (or should) have
     * the JDBC trace rules installed.
     *
     * @return <code>true</code> if this component's JDBC tracing is enabled
     */
    private boolean isEnabled() {
        Configuration pc = this.resourceContext.getPluginConfiguration();
        String enabledString = pc.getSimpleValue(JdbcTracerUtil.PLUGINCONFIG_ENABLED, "true");
        return Boolean.parseBoolean(enabledString);
    }

    /**
     * If this resource has been "enabled" (see its plugin configuration), this will ensure that the script
     * is added to the remote Byteman agent; if it is not enabled, the script will be deleted from the
     * remote Byteman agent.
     * 
     * @param refresh if <code>true</code>, this will ensure the byteman agent has up to date rules. This means
     *                that even if the remote byteman agent already has the script installed, this method will
     *                delete the script and immediately re-install the current one. This parameter is meaningless
     *                if this resource has not been enabled (because, in that case, the script is deleted from
     *                the byteman agent so there is nothing to keep up to date)
     *
     * @throws Exception
     */
    private void prepareRules(boolean refresh) throws Exception {
        Configuration pc = this.resourceContext.getPluginConfiguration();
        String scriptName = pc.getSimpleValue(JdbcTracerUtil.PLUGINCONFIG_SCRIPTNAME,
            JdbcTracerUtil.DEFAULT_JDBC_TRACER_SCRIPT_NAME);
        String helperJarFileName = JdbcTracerUtil.DEFAULT_JDBC_TRACER_HELPER_JAR;

        JdbcTracerUtil jdbcTracerUtil = new JdbcTracerUtil();
        BytemanAgentComponent bytemanAgentResource = this.resourceContext.getParentResourceComponent();
        File script = jdbcTracerUtil.getJdbcTraceRulesScriptFile(bytemanAgentResource, scriptName);
        String scriptAbsolutePath = script.getAbsolutePath();
        File helper = jdbcTracerUtil.getHelperJarFile(bytemanAgentResource, helperJarFileName);
        String helperAbsolutePath = helper.getAbsolutePath();

        // see if there are already jdbc trace rules installed in the byteman agent
        // note that we talk directly to the remote byteman agent to get the info; our parent resource might not have our script yet in its cache
        Submit client = bytemanAgentResource.getBytemanClient();
        Map<String, String> allKnownScripts = client.getAllScripts();
        String existingRules = null;
        if (allKnownScripts != null) {
            existingRules = allKnownScripts.get(scriptAbsolutePath);
        }

        // add or remove the rules as appropriate
        if (isEnabled()) {
            // if we are to refresh the script, remove the current script that is loaded (if one is loaded) 
            if (existingRules != null && refresh) {
                Map<String, String> doomed = new HashMap<String, String>(1);
                doomed.put(scriptAbsolutePath, existingRules);
                client.deleteRules(doomed);
                existingRules = null;
            }

            if (refresh || !helper.exists()) {
                // the helper was deleted previously, we need to write it back out again
                jdbcTracerUtil.extractHelperJarFile(bytemanAgentResource, helperJarFileName);
            }
            if (refresh || !client.getLoadedSystemClassloaderJars().contains(helperAbsolutePath)) {
                client.addJarsToSystemClassloader(Arrays.asList(helperAbsolutePath));
            }

            if (existingRules == null) {
                if (!script.exists()) {
                    // the script was deleted previously, we need to write it back out again
                    jdbcTracerUtil.extractJdbcTraceRulesScriptFile(bytemanAgentResource, scriptName);
                }
                client.addRulesFromFiles(Arrays.asList(scriptAbsolutePath));
            }
        } else {
            if (existingRules != null) {
                Map<String, String> doomed = new HashMap<String, String>(1);
                doomed.put(scriptAbsolutePath, existingRules);
                client.deleteRules(doomed);
            }
        }

        return;
    }
}
