/*
 * RHQ Management Platform
 * Copyright (C) 2005-2010 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.enterprise.server.plugin.pc.perspective;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Collections;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

import javax.management.MBeanException;
import javax.management.MBeanServerConnection;
import javax.management.ObjectName;
import javax.naming.Context;
import javax.naming.InitialContext;

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

import org.rhq.core.util.file.FileUtil;
import org.rhq.enterprise.server.plugin.pc.ServerPluginEnvironment;
import org.rhq.enterprise.server.plugin.pc.ServerPluginManager;
import org.rhq.enterprise.server.plugin.pc.perspective.metadata.PerspectivePluginMetadataManager;
import org.rhq.enterprise.server.xmlschema.generated.serverplugin.perspective.PerspectivePluginDescriptorType;

/**
 * This loads in all perspective server plugins that can be found. You can obtain a loaded plugin's
 * {@link ServerPluginEnvironment environment}, including its classloader, from this object as well.
 *
 * @author Jay Shaughnessy 
 * @author John Mazzitelli
 */
public class PerspectiveServerPluginManager extends ServerPluginManager {
    private final Log log = LogFactory.getLog(this.getClass());

    private PerspectivePluginMetadataManager metadataManager;

    public PerspectiveServerPluginManager(PerspectiveServerPluginContainer pc) {
        super(pc);
    }

    @Override
    public void initialize() throws Exception {
        super.initialize();
        this.metadataManager = new PerspectivePluginMetadataManager();
    }

    @Override
    public void shutdown() {
        super.shutdown();
        this.metadataManager = null;
    }

    /**
     * An object that can be used to process and store all metadata from all perspective plugins. This object will contain all the
     * metadata found in all loaded perspective plugins.
     *
     * @return object to retrieve plugin metadata from
     */
    public PerspectivePluginMetadataManager getMetadataManager() {
        return this.metadataManager;
    }

    /* At load-time ensure that any WAR files packaged with the server plugin are deployed to the RHQ
     * Server.  Note, perspective apps are logically deployed along-side, not inside the rhq.ear (although physically
     * under the default/work). They should have access to the same shared components.
     *  
     * @see org.rhq.enterprise.server.plugin.pc.AbstractTypeServerPluginContainer#loadPlugin(org.rhq.enterprise.server.plugin.pc.ServerPluginEnvironment)
     */
    @Override
    public synchronized void loadPlugin(ServerPluginEnvironment env, boolean enabled) throws Exception {
        super.loadPlugin(env, enabled);

        this.metadataManager.loadPlugin((PerspectivePluginDescriptorType) env.getPluginDescriptor());

        // Do this here and not startPlugin() because if the app does not deploy the perspective should
        // be considered invalid.
        deployEmbeddedWars(env);
    }

    private void deployEmbeddedWars(ServerPluginEnvironment env) {
        String name = null;
        try {
            JarFile pluginJarFile = new JarFile(env.getPluginUrl().getFile());
            try {
                for (JarEntry entry : Collections.list(pluginJarFile.entries())) {
                    name = entry.getName();
                    if (name.toLowerCase().endsWith(".war")) {
                        deployWar(env, entry.getName(), pluginJarFile.getInputStream(entry));
                    }
                }
            } finally {
                pluginJarFile.close();
            }
        } catch (Exception e) {
            Throwable t = (e instanceof MBeanException) ? e.getCause() : e;
            log.error("Failed to deploy " + env.getPluginKey().getPluginName() + "#" + name, t);
        }
    }

    private void deployWar(ServerPluginEnvironment env, String name, InputStream iStream) throws Exception {        
        // Save the war file to the plugins data directory. This survives restarts and will
        // act as our deploy directory.
        File deployFile = writeWarToFile(getDeployFile(env, name), iStream);

        // get reference to MBean server
        Context ic = new InitialContext();
        MBeanServerConnection server = (MBeanServerConnection) ic.lookup("jmx/invoker/RMIAdaptor");

        // get reference to MainDeployer MBean
        ObjectName mainDeployer = new ObjectName("jboss.system:service=MainDeployer");

        server.invoke(mainDeployer, "deploy", new Object[] { deployFile.getAbsolutePath() },
            new String[] { String.class.getName() });
    }

    private File getDeployFile(ServerPluginEnvironment env, String name) throws IOException {
        File dataDir = getServerPluginContext(env).getDataDirectory();
        dataDir.mkdirs();
        return new File(dataDir, name);
    }

    private File writeWarToFile(File destFile, InputStream iStream) throws Exception {
        // Delete any previous instance of the file
        if (destFile.exists()) {
            getLog().debug("Existing file found and will be deleted at: " + destFile);
            destFile.delete();
        }

        FileUtil.writeFile(iStream, destFile);

        if (!destFile.exists()) {
            String err = "Temporary file for application update not written to: " + destFile;
            getLog().error(err);
            throw new Exception(err);
        }

        return destFile;
    }

    @Override
    protected void startPlugin(String pluginName) {
        super.startPlugin(pluginName);

        // If the metadata manager is started, then this is a hot deploy (or redeploy) of the perspective, so restart
        // the metadata manage to merge the metadata from this perspective and all other previously loaded perspectives.
        // If the metadata manager is *not* started, then this is the initial load of the perspective, and the master PC
        // will call startPlugins() once all perspectives have been loaded, which will take care of starting the
        // metadata manager.
        if (isPluginEnabled(pluginName) && this.metadataManager.isStarted()) {
            this.metadataManager.stop();
            this.metadataManager.start();
        }

        // TODO: execute An MBean Start on any perspective webapps?        
    }

    /**
     * All of the plugins have been loaded, so now let the metadata manager sort through the definitions.
     *
     * @see org.rhq.enterprise.server.plugin.pc.ServerPluginManager#startPlugins()
     */
    @Override
    public synchronized void startPlugins() {
        super.startPlugins();
        this.metadataManager.start();
    }

    @Override
    protected void stopPlugin(String pluginName) {
        // TODO: This method can be called in three cases:
        //       1) hot-undeploy of a plugin
        //       2) hot-redeploy of a plugin (stopPlugin() is called, then startPlugin() is called)
        //       3) during shutdown of the plugin container
        // We only want to restart our metadata manager in case 2, but there's no way for us to tell which case we're
        // dealing with...
        /*if (case2 && this.metadataManager.isStarted()) {
            this.metadataManager.stop();
            this.metadataManager.start();
        }*/

        // TODO: execute An MBean Stop on any perspective webapps?

        super.stopPlugin(pluginName);
    }

    /* (non-Javadoc)
     * @see org.rhq.enterprise.server.plugin.pc.ServerPluginManager#stopPlugins()
     */
    @Override
    public synchronized void stopPlugins() {
        this.metadataManager.stop();
        super.stopPlugins();
    }

    /* At unload-time ensure that any WAR files packaged with the server plugin are un-deployed on
     * the RHQ Server.
     * 
     * @see org.rhq.enterprise.server.plugin.pc.AbstractTypeServerPluginContainer#unloadPlugin(org.rhq.enterprise.server.plugin.pc.ServerPluginEnvironment)
     */
    @Override
    public synchronized void unloadPlugin(String pluginName) throws Exception {
        ServerPluginEnvironment env = getPluginEnvironment(pluginName);
        // Do this here and not stopPlugin(), treat this as a precursor to a possible perspectve deletion 
        undeployWars(env);
        this.metadataManager.unloadPlugin((PerspectivePluginDescriptorType) env.getPluginDescriptor());
        super.unloadPlugin(pluginName);
    }

    private void undeployWars(ServerPluginEnvironment env) {
        String name = null;
        try {
            JarFile plugin = new JarFile(env.getPluginUrl().getFile());
            try {
                for (JarEntry entry : Collections.list(plugin.entries())) {
                    name = entry.getName();
                    if (name.toLowerCase().endsWith(".war")) {
                        undeployWar(getDeployFile(env, entry.getName()));
                    }
                }
            } finally {
                plugin.close();
            }
        } catch (Exception e) {
            this.log.error("Failed to deploy " + env.getPluginKey().getPluginName() + "#" + name, e);
        }
    }

    private void undeployWar(File deployFile) {
        try {
            // get reference to MBean server
            Context ic = new InitialContext();
            MBeanServerConnection server = (MBeanServerConnection) ic.lookup("jmx/invoker/RMIAdaptor");

            // get reference to MainDeployer MBean
            ObjectName mainDeployer = new ObjectName("jboss.system:service=MainDeployer");

            URL fileUrl = deployFile.toURI().toURL();

            Boolean isDeployed = (Boolean) server.invoke(mainDeployer, "isDeployed", new Object[] { fileUrl },
                new String[] { URL.class.getName() });

            if (isDeployed) {
                server.invoke(mainDeployer, "undeploy", new Object[] { fileUrl }, new String[] { URL.class.getName() });
            }
        } catch (Exception e) {
            log.error("Failed to undeploy perspective WAR [" + deployFile + "].", e);
        }
    }
}