/*
 * RHQ Management Platform
 * Copyright (C) 2005-2013 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.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
 */
package org.rhq.enterprise.server.core.plugin;

import java.io.File;
import java.util.Date;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.ejb.ConcurrencyManagement;
import javax.ejb.ConcurrencyManagementType;
import javax.ejb.LocalBean;
import javax.ejb.Singleton;
import javax.ejb.Startup;
import javax.ejb.TransactionAttribute;
import javax.ejb.TransactionAttributeType;

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

import org.jboss.util.StringPropertyReplacer;

import org.rhq.core.clientapi.agent.metadata.PluginMetadataManager;
import org.rhq.core.clientapi.descriptor.AgentPluginDescriptorUtil;
import org.rhq.core.util.MessageDigestGenerator;
import org.rhq.core.util.exception.ThrowableUtil;
import org.rhq.core.util.file.FileUtil;
import org.rhq.enterprise.server.util.JMXUtil;
import org.rhq.enterprise.server.util.LookupUtil;
import org.rhq.enterprise.server.xmlschema.ServerPluginDescriptorUtil;

/**
 * This looks at both the file system and the database for new agent and server plugins.
 * This does not perform any polling - it only performs scanning on-demand. It provides
 * some configuration settings that a timer could use to determine when to periodically
 * scan (see {@link #getScanPeriod()} for example.
 *
 * @author John Mazzitelli
 */
@Singleton
@Startup
@LocalBean
@ConcurrencyManagement(ConcurrencyManagementType.BEAN)
@TransactionAttribute(TransactionAttributeType.SUPPORTS)
public class PluginDeploymentScanner implements PluginDeploymentScannerMBean {

    private Log log = LogFactory.getLog(PluginDeploymentScanner.class);

    /** time, in millis, between each scans */
    private long scanPeriod = 300000L;

    /** where the user can copy agent or server plugins */
    private File userPluginDir = null;

    /** what looks for new or changed agent plugins */
    private AgentPluginScanner agentPluginScanner = new AgentPluginScanner();

    /** what looks for new or changed agent plugins */
    private ServerPluginScanner serverPluginScanner = new ServerPluginScanner();

    // https://issues.jboss.org/browse/AS7-5343 forces us to change the attribute values as Strings
    // we then must convert ourselves.

    //public Long getScanPeriod() {
    public String getScanPeriod() {
        //return this.scanPeriod;
        return Long.toString(this.scanPeriod);
    }

    //public void setScanPeriod(Long ms) {
    public void setScanPeriod(String ms) {
        if (ms != null) {
            //this.scanPeriod = ms;
            this.scanPeriod = Long.parseLong(StringPropertyReplacer.replaceProperties(ms));
        } else {
            this.scanPeriod = 300000L;
        }
    }

    //public File getUserPluginDir() {
    public String getUserPluginDir() {
        //return this.userPluginDir;
        if (this.userPluginDir == null) {
            return null;
        }
        return this.userPluginDir.getAbsolutePath();
    }

    //public void setUserPluginDir(File dir) {
    public void setUserPluginDir(String dir) {
        if (dir == null) {
            return;
        }
        //this.userPluginDir = dir;
        this.userPluginDir = new File(StringPropertyReplacer.replaceProperties(dir));
    }

    //public File getServerPluginDir() {
    public String getServerPluginDir() {
        //return this.serverPluginScanner.getServerPluginDir();
        File dir = this.serverPluginScanner.getServerPluginDir();
        if (dir == null) {
            return null;
        }
        return dir.getAbsolutePath();
    }

    //public void setServerPluginDir(File dir) {
    public void setServerPluginDir(String dir) {
        if (dir == null) {
            return;
        }
        //this.serverPluginScanner.setServerPluginDir(dir);
        this.serverPluginScanner.setServerPluginDir(new File(StringPropertyReplacer.replaceProperties(dir)));
    }

    //public File getAgentPluginDir() {
    public String getAgentPluginDir() {
        //return this.agentPluginScanner.getAgentPluginDeployer().getPluginDir();
        File dir = this.agentPluginScanner.getAgentPluginDeployer().getPluginDir();
        if (dir == null) {
            return null;
        }
        return dir.getAbsolutePath();
    }

    //public void setAgentPluginDir(File dir) {
    public void setAgentPluginDir(String dir) {
        if (dir == null) {
            return;
        }
        //this.agentPluginScanner.getAgentPluginDeployer().setPluginDir(dir);
        this.agentPluginScanner.getAgentPluginDeployer().setPluginDir(
            new File(StringPropertyReplacer.replaceProperties(dir)));
    }

    public PluginMetadataManager getPluginMetadataManager() {
        return this.agentPluginScanner.getAgentPluginDeployer().getPluginMetadataManager();
    }

    private File getUserPluginDirAsFile() {
        return this.userPluginDir;
    }

    private File getAgentPluginDirAsFile() {
        return this.agentPluginScanner.getAgentPluginDeployer().getPluginDir();
    }

    private File getServerPluginDirAsFile() {
        return this.serverPluginScanner.getServerPluginDir();
    }

    public void start() throws Exception {
        return;
    }

    public void stop() {
        this.agentPluginScanner.getAgentPluginDeployer().stop();
        return;
    }

    public void startDeployment() {
        // We are being called by the server's startup bean which essentially informs us that
        // the server's internal EJB/SLSBs are ready and can be called. This means we are allowed to start.
        // NOTE: Make sure we are called BEFORE the master plugin container is started!

        // setup our attributes - skip if the plugin dirs were already set (e.g. from test code)
        String upd = getUserPluginDir();
        String apd = getAgentPluginDir();
        String spd = getServerPluginDir();

        // don't look up the core server mbean if we don't need do
        if (upd == null || apd == null || spd == null) {
            File homeDir = LookupUtil.getCoreServer().getInstallDir();
            File earDir = LookupUtil.getCoreServer().getEarDeploymentDir();

            if (upd == null) {
                upd = new File(homeDir, "plugins").getAbsolutePath();
                setUserPluginDir(upd);
            }

            if (apd == null) {
                apd = new File(earDir, "rhq-downloads/rhq-plugins").getAbsolutePath();
                setAgentPluginDir(apd);
            }

            if (spd == null) {
                spd = new File(earDir, "rhq-serverplugins").getAbsolutePath();
                setServerPluginDir(spd);
            }
        }

        log.info("user plugin dir=" + upd);
        log.info("agent plugin dir=" + apd);
        log.info("server plugin dir=" + spd);

        // This will check to see if there are any agent plugin records in the database
        // that do not have content associated with them and if so, will stream
        // the content from the file system to the database. This is needed only
        // in the case when this server has recently been upgraded from an old
        // version of the software that did not originally have content stored in the DB.
        // Once we do that, we can start the agent plugin deployer.
        try {
            this.agentPluginScanner.fixMissingAgentPluginContent();
            this.agentPluginScanner.getAgentPluginDeployer().start();
        } catch (Exception e) {
            throw new RuntimeException("Cannot start plugin deployment scanner properly", e);
        }

        this.agentPluginScanner.getAgentPluginDeployer().startDeployment();

        // do the initial scan now
        try {
            scanAndRegister();
        } catch (Throwable t) {
            log.error("Scan failed. Cause: " + ThrowableUtil.getAllMessages(t));
            if (log.isDebugEnabled()) {
                log.debug("Scan failure stack trace follows:", t);
            }
        }

        return;
    }

    public synchronized void scan() throws Exception {
        // The user directory is a simple location for the user to put all plugins in.
        // It makes it easy for the user to know where to put the plugins without
        // having to know the internal location for the real plugins under the ear.
        // Now we move the user's plugins to their real location in the ear.
        scanUserDirectory();

        // scan for agent plugins
        this.agentPluginScanner.agentPluginScan();

        // scan for server plugins
        this.serverPluginScanner.serverPluginScan();
        return;
    }

    public synchronized void scanAndRegister() throws Exception {
        // do the scan first to find any new/updated plugins
        scan();

        // now tell the agent plugin scanner to register plugins that it determined needs to be registered
        this.agentPluginScanner.registerAgentPlugins();

        // tell the server plugin scanner the same
        this.serverPluginScanner.registerServerPlugins();
    }

    /**
     * Take the plugins placed in the user directory, and copy them to their apprpriate places
     * in the server.
     */
    private void scanUserDirectory() {
        File userDir = getUserPluginDirAsFile(); // 
        if (userDir == null || !userDir.isDirectory()) {
            return; // not configured for a user directory, just return immediately and do nothing
        }

        File[] listFiles = userDir.listFiles();
        if (listFiles == null || listFiles.length == 0) {
            return; // nothing to do
        }
        for (File file : listFiles) {
            File destinationDirectory;
            boolean isJarLess = file.getName().endsWith("-rhq-plugin.xml");
            if (file.getName().endsWith(".jar") || isJarLess ) {
                try {
                    if (!isJarLess && null == AgentPluginDescriptorUtil.loadPluginDescriptorFromUrl(file.toURI().toURL())) {
                        throw new NullPointerException("no xml descriptor found in jar");
                    }
                    destinationDirectory = getAgentPluginDirAsFile(); //
                } catch (Exception e) {
                    try {
                        log.debug("[" + file.getAbsolutePath() + "] is not an agent plugin jar (Cause: "
                            + ThrowableUtil.getAllMessages(e) + "). Will see if its a server plugin jar");

                        if (null == ServerPluginDescriptorUtil.loadPluginDescriptorFromUrl(file.toURI().toURL())) {
                            throw new NullPointerException("no xml descriptor found in jar");
                        }
                        destinationDirectory = getServerPluginDirAsFile(); //
                    } catch (Exception e1) {
                        // skip it, doesn't look like a valid plugin jar
                        File fixmeFile = new File(file.getAbsolutePath() + ".fixme");
                        boolean renamed = file.renameTo(fixmeFile);
                        log.warn("Does not look like [" + (renamed ? fixmeFile : file).getAbsolutePath()
                            + "] is a plugin jar -(Cause: " + ThrowableUtil.getAllMessages(e1)
                            + "). It will be ignored. Please fix that file or remove it.");
                        continue;
                    }
                }

                try {
                    String fileMd5 = MessageDigestGenerator.getDigestString(file);
                    File realPluginFile = new File(destinationDirectory, file.getName());
                    String realPluginFileMd5 = null;
                    if (realPluginFile.exists()) {
                        realPluginFileMd5 = MessageDigestGenerator.getDigestString(realPluginFile);
                    }
                    if (!fileMd5.equals(realPluginFileMd5)) {
                        if (file.lastModified() > realPluginFile.lastModified()) {
                            FileUtil.copyFile(file, realPluginFile);
                            boolean succeeded = realPluginFile.setLastModified(file.lastModified());
                            if (!succeeded) {
                                log.error("Failed to set mtime to [" + new Date(file.lastModified()) + "] on file ["
                                    + realPluginFile + "].");
                            }
                            String tmp;
                            if (!isJarLess)
                                tmp = "jar";
                            else
                                tmp = "descriptor";
                            log.info("Found plugin " + tmp + " at [" + file.getAbsolutePath() + "] and placed it at ["
                                + realPluginFile.getAbsolutePath() + "]");
                        }
                    }
                    else {
                        log.info("Found a plugin at [" + file.getAbsolutePath() + "], which is the same as the existing one. It will be ignored");
                    }
                    boolean deleted = file.delete();
                    if (!deleted) {
                        log.info("The plugin jar found at[" + file.getAbsolutePath()
                            + "] has been processed and can be deleted. It failed to get deleted, "
                            + "so it may get processed again. You should delete it manually now.");
                    }
                } catch (Exception e) {
                    log.error("Failed to process plugin [" + file.getAbsolutePath() + "], ignoring it", e);
                }
            }
        }

        return;
    }

    @PostConstruct
    private void init() {
        JMXUtil.registerMBean(this, OBJECT_NAME);
        setScanPeriod("${rhq.server.plugin-scan-period-ms:300000}");
    }

    @PreDestroy
    private void destroy() {
        stop();
        JMXUtil.unregisterMBeanQuietly(OBJECT_NAME);
    }

}
