org.hyperic.hq.product.server.session.ProductPluginDeployer.java Source code

Java tutorial

Introduction

Here is the source code for org.hyperic.hq.product.server.session.ProductPluginDeployer.java

Source

/*
 * NOTE: This copyright does *not* cover user programs that use HQ
 * program services by normal system calls through the application
 * program interfaces provided as part of the Hyperic Plug-in Development
 * Kit or the Hyperic Client Development Kit - this is merely considered
 * normal use of the program, and does *not* fall under the heading of
 * "derived work".
 *
 * Copyright (C) [2004-2009], Hyperic, Inc.
 * This file is part of HQ.
 *
 * HQ is free software; you can redistribute it and/or modify
 * it under the terms version 2 of the GNU General Public License as
 * published by the Free Software Foundation. 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307
 * USA.
 */

package org.hyperic.hq.product.server.session;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ScheduledFuture;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hyperic.hq.appdef.shared.AgentManager;
import org.hyperic.hq.common.shared.TransactionRetry;
import org.hyperic.hq.hqu.RenditServer;
import org.hyperic.hq.product.PluginException;
import org.hyperic.hq.product.PluginInfo;
import org.hyperic.hq.product.ProductPlugin;
import org.hyperic.hq.product.ProductPluginManager;
import org.hyperic.hq.product.shared.PluginManager;
import org.hyperic.hq.product.shared.ProductManager;
import org.hyperic.util.file.FileUtil;
import org.hyperic.util.file.FileWatcher;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.jmx.export.annotation.ManagedAttribute;
import org.springframework.jmx.export.annotation.ManagedMetric;
import org.springframework.jmx.export.annotation.ManagedOperation;
import org.springframework.jmx.export.annotation.ManagedResource;
import org.springframework.roo.file.monitor.event.FileEvent;
import org.springframework.roo.file.monitor.event.FileEventListener;
import org.springframework.roo.file.monitor.event.FileOperation;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.stereotype.Service;

/**
 * ProductPlugin deployer. We accept $PLUGIN_DIR/*.{jar,xml}
 * 
 * 
 */
@ManagedResource("hyperic.jmx:type=Service,name=ProductPluginDeployer")
@Service
public class ProductPluginDeployer implements Comparator<String>, ApplicationContextAware {

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

    private static final String HQU = "hqu";

    private final RenditServer renditServer;
    private final ProductManager productManager;

    private ProductPluginManager productPluginManager;

    private final List<File> pluginDirs = new ArrayList<File>(2);
    private File hquDir;

    private final AgentManager agentManager;

    private final PluginManager pluginManager;

    private final TransactionRetry transactionRetry;

    private final TaskScheduler taskScheduler;
    private ScheduledFuture<?> pluginExecutorTask;

    private FileWatcher fileWatcher;

    private final Collection<FileEvent> fileEvents = new ArrayList<FileEvent>();

    @Autowired
    public ProductPluginDeployer(RenditServer renditServer, ProductManager productManager,
            AgentManager agentManager, PluginManager pluginManager,
            @Value("#{scheduler}") TaskScheduler taskScheduler, TransactionRetry transactionRetry) {
        this.renditServer = renditServer;
        this.productManager = productManager;
        this.agentManager = agentManager;
        this.pluginManager = pluginManager;
        this.transactionRetry = transactionRetry;
        this.taskScheduler = taskScheduler;
    }

    private void initializePlugins(Collection<File> pluginDirs) {
        Map<String, File> plugins = new HashMap<String, File>(0);
        // On startup, it's necessary to load all plugins first due to
        // inter-plugin class dependencies
        try {
            plugins = loadPlugins(pluginDirs);
        } catch (Exception e) {
            log.error("Error loading product plugins", e);
        }
        // Now we can deploy the plugins
        final Map<String, Integer> existing = pluginManager.getAllPluginIdsByName();
        final List<String> keys = new ArrayList<String>(plugins.keySet());
        Collections.sort(keys, this);
        for (final String pluginName : keys) {
            existing.remove(pluginName);
            deployPlugin(pluginName, plugins.get(pluginName));
        }
        final Runnable runner = new Runnable() {
            public void run() {
                pluginManager.markDisabled(existing.values());
            }
        };
        transactionRetry.runTransaction(runner, 3, 1000);
    }

    /**
     * 
     */
    public ProductPluginManager getProductPluginManager() {
        return productPluginManager;
    }

    private Set<String> getPluginNames(String type) throws PluginException {
        return productPluginManager.getPluginManager(type).getPlugins().keySet();
    }

    /**
     * 
     * List registered plugin names of given type. Intended for use via
     * /jmx-console
     */
    @ManagedAttribute
    public ArrayList<String> getRegisteredPluginNames(String type) throws PluginException {
        return new ArrayList<String>(getPluginNames(type));
    }

    /**
     * 
     * List registered product plugin names. Intended for use via /jmx-console
     */
    @ManagedAttribute
    public ArrayList<String> getRegisteredPluginNames() throws PluginException {
        return new ArrayList<String>(productPluginManager.getPlugins().keySet());
    }

    /**
     * 
     */
    @ManagedMetric
    public int getProductPluginCount() throws PluginException {
        return productPluginManager.getPlugins().keySet().size();
    }

    /**
     * 
     */
    @ManagedMetric
    public int getMeasurementPluginCount() throws PluginException {
        return getPluginNames(ProductPlugin.TYPE_MEASUREMENT).size();
    }

    /**
     * 
     */
    @ManagedMetric
    public int getControlPluginCount() throws PluginException {
        return getPluginNames(ProductPlugin.TYPE_CONTROL).size();
    }

    /**
     * 
     */
    @ManagedMetric
    public int getAutoInventoryPluginCount() throws PluginException {
        return getPluginNames(ProductPlugin.TYPE_AUTOINVENTORY).size();
    }

    /**
     * 
     */
    @ManagedMetric
    public int getLogTrackPluginCount() throws PluginException {
        return getPluginNames(ProductPlugin.TYPE_LOG_TRACK).size();
    }

    /**
     * 
     */
    @ManagedMetric
    public int getConfigTrackPluginCount() throws PluginException {
        return getPluginNames(ProductPlugin.TYPE_CONFIG_TRACK).size();
    }

    /**
     * 
     */
    @ManagedOperation
    public void setProperty(String name, String value) {
        productPluginManager.setProperty(name, value);
        log.info("setProperty(" + name + ", " + value + ")");
    }

    /**
     * 
     */
    @ManagedOperation
    public String getProperty(String name) {
        return productPluginManager.getProperty(name);
    }

    /**
     * 
     */
    @ManagedOperation
    public PluginInfo getPluginInfo(String name) throws PluginException {
        PluginInfo info = productPluginManager.getPluginInfo(name);

        if (info == null) {
            throw new PluginException("No PluginInfo found for: " + name);
        }

        return info;
    }

    public int compare(String s1, String s2) {
        int order1 = productPluginManager.getPluginInfo(s1).deploymentOrder;
        int order2 = productPluginManager.getPluginInfo(s2).deploymentOrder;

        return order1 - order2;
    }

    private PluginInfo registerPluginJar(String pluginJar) {
        if (!productPluginManager.isLoadablePluginName(pluginJar)) {
            return null;
        }
        try {
            PluginInfo plugin = productPluginManager.registerPluginJar(pluginJar, null);
            return plugin;
        } catch (Exception e) {
            log.error("Unable to deploy plugin '" + pluginJar + "'", e);
            return null;
        }
    }

    private void deployPlugin(final String pluginName, File dir) {
        try {
            productManager.deploymentNotify(pluginName, dir);
            final Runnable runner = new Runnable() {
                public void run() {
                    pluginManager.markEnabled(pluginName);
                }
            };
            transactionRetry.runTransaction(runner, 3, 1000);
        } catch (Exception e) {
            // HHQ-5390
            final Runnable runner = new Runnable() {
                public void run() {
                    pluginManager.markPluginDisabledByName(pluginName);
                }
            };
            transactionRetry.runTransaction(runner, 3, 1000);
            log.error("Unable to deploy plugin '" + pluginName + "'", e);
        }
    }

    @PostConstruct
    public void start() throws Exception {
        File propFile = ProductPluginManager.PLUGIN_PROPERTIES_FILE;
        productPluginManager = new ProductPluginManager(propFile);
        productPluginManager.setRegisterTypes(true);

        if (propFile.canRead()) {
            log.info("Loaded custom properties from: " + propFile);
        }

        if (!(pluginDirs.isEmpty())) {
            ProductPluginManager.setPdkPluginsDir(pluginDirs.get(0).getAbsolutePath());
        }
        productPluginManager.init();
        this.fileWatcher = new FileWatcher();
        fileWatcher.addFileEventListener(new ProductPluginFileEventListener());
        for (File pluginDir : this.pluginDirs) {
            fileWatcher.addDir(pluginDir.toString(), false);
        }
        initializePlugins(pluginDirs);
        if (!(pluginDirs.isEmpty())) {
            fileWatcher.start();
        }
        this.pluginExecutorTask = taskScheduler.scheduleWithFixedDelay(new PluginFileExecutor(),
                new Date(now() + 60000l), 5000l);
    }

    @PreDestroy
    public final void destroy() {
        this.pluginExecutorTask.cancel(true/*mayInterruptIfRunning*/);
        this.fileWatcher.stop();
    }//EOM 

    private long now() {
        return System.currentTimeMillis();
    }

    private void unpackJar(File pluginJarFile, File destDir, String prefix) throws Exception {

        JarFile jar = null;
        try {
            jar = new JarFile(pluginJarFile);
            for (Enumeration<JarEntry> e = jar.entries(); e.hasMoreElements();) {
                JarEntry entry = e.nextElement();
                String name = entry.getName();

                if (name.startsWith(prefix)) {
                    name = name.substring(prefix.length());
                    if (name.length() == 0) {
                        continue;
                    }
                    File file = new File(destDir, name);
                    if (entry.isDirectory()) {
                        file.mkdirs();
                    } else {
                        FileUtil.copyStream(jar.getInputStream(entry), new FileOutputStream(file));
                    }
                }
            }
        } catch (Throwable t) {
            t.printStackTrace();
        } finally {
            if (jar != null)
                jar.close();
        }
    }

    private void deployHqu(String plugin, File pluginFile, boolean initializing) throws Exception {
        URLClassLoader pluginClassloader = new URLClassLoader(new URL[] { pluginFile.toURI().toURL() });
        final String prefix = HQU + "/";
        URL hqu = pluginClassloader.getResource(prefix);
        if (hqu == null) {
            return;
        }
        File destDir = new File(hquDir, plugin);
        boolean exists = destDir.exists();
        log.info("Deploying " + plugin + " " + HQU + " to: " + destDir);

        unpackJar(pluginFile, destDir, prefix);

        if (!(initializing) && exists) {
            // update ourselves to avoid having to delete,sleep,unpack
            renditServer.removePluginDir(destDir.getName());
            renditServer.addPluginDir(destDir);
        } // else Rendit watcher will deploy the new plugin
    }

    private Map<String, File> loadPlugins(Collection<File> pluginDirs) throws Exception {
        Map<String, File> map = new HashMap<String, File>();
        for (File pluginDir : pluginDirs) {
            File[] files = pluginDir.listFiles();
            for (File file : files) {
                String filename = file.getName();
                if (map.containsKey(filename)) {
                    if (isInCustomDir(file)) {
                        log.info("plugin file " + file + " takes precedence over " + map.get(filename)
                                + " since it is in the custom plugin " + "deployment dir (this is ok)");
                        map.put(filename, file);
                    } else {
                        log.info("plugin file " + file + " will not be deployed since the custom file "
                                + map.get(filename) + " takes precedence (this is ok)");
                    }
                } else {
                    map.put(filename, file);
                }
            }
        }
        Collection<File> plugins = map.values();
        Map<String, File> rtn = new HashMap<String, File>();
        for (File pluginFile : plugins) {
            PluginInfo plugin = loadPlugin(pluginFile, true);
            if (plugin != null) {
                rtn.put(plugin.name, pluginFile);
            }
        }
        return rtn;
    }

    private boolean undeployPlugin(final File pluginFile, boolean force) throws Exception {
        if (!force && !isDeployable(pluginFile)) {
            log.info("cannot undeploy " + pluginFile + " since it is over-written in " + getCustomPluginDir()
                    + " (this is ok)");
            return false;
        }
        log.info("Undeploying plugin: " + pluginFile);
        productPluginManager.removePluginJar(pluginFile.toString());
        final Runnable runner = new Runnable() {
            public void run() {
                pluginManager.markDisabled(pluginFile.getName());
            }
        };
        transactionRetry.runTransaction(runner, 3, 1000);
        return true;
    }

    private PluginInfo loadPlugin(File pluginFile, boolean initializing) throws Exception {
        PluginInfo plugin = registerPluginJar(pluginFile.toString());
        if (plugin != null) {
            deployHqu(plugin.name, pluginFile, initializing);
            return plugin;
        }
        return null;
    }

    private boolean loadAndDeployPlugin(File pluginFile) throws Exception {
        if (!isDeployable(pluginFile)) {
            log.info("cannot deploy " + pluginFile + " since it is over-written in " + getCustomPluginDir()
                    + " (this is ok)");
            return false;
        }
        PluginInfo pluginInfo = loadPlugin(pluginFile, false);
        if (pluginInfo != null) {
            log.info("Deploying plugin: " + pluginFile.getAbsolutePath());
            deployPlugin(pluginInfo.name, pluginFile.getParentFile());
        }
        return true;
    }

    /**
     * simply checks if the pluginFile is in the serverPluginDir and if there is a duplicate
     * filename in the customPluginDir.  If those conditions are met then return false.
     */
    private boolean isDeployable(File pluginFile) {
        if (isInCustomDir(pluginFile)) {
            return true;
        }
        if (new File(getCustomPluginDir().getAbsoluteFile(), pluginFile.getName()).exists()) {
            return false;
        }
        return true;
    }

    /**
     * @return true if pluginFile is located in customPluginDir
     */
    private boolean isInCustomDir(File file) {
        return file.getAbsoluteFile().getParent().startsWith(getCustomPluginDir().getAbsolutePath());
    }

    private File getServerPluginDir() {
        return pluginManager.getServerPluginDir();
    }

    private File getCustomPluginDir() {
        return pluginManager.getCustomPluginDir();
    }

    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        try {
            this.hquDir = applicationContext.getResource(HQU).getFile();
        } catch (IOException e) {
            log.info("HQU directory not found");
        }
        pluginDirs.add(getServerPluginDir());
        // Add custom hq-plugins dir at same level as server home
        final File customPluginDir = getCustomPluginDir();
        if (!customPluginDir.exists()) {
            customPluginDir.mkdirs();
        }
        if (customPluginDir != null && customPluginDir.exists() && customPluginDir.isDirectory()) {
            pluginDirs.add(customPluginDir);
        } else {
            log.error("custom plugin directory " + customPluginDir.getAbsolutePath()
                    + " does not exist.  Without this directory users "
                    + "will not be able to deploy plugins from the HQ Plugin Manager", new Throwable());
        }
    }

    private class PluginFileExecutor implements Runnable {
        public void run() {
            try {
                handleFileEvents();
            } catch (Throwable t) {
                log.error(t, t);
            }
        }

        public void handleFileEvents() {
            final Collection<String> toSync = new ArrayList<String>();
            final Collection<FileEvent> events;
            synchronized (fileEvents) {
                events = new ArrayList<FileEvent>(fileEvents);
                fileEvents.clear();
            }
            if (events.isEmpty()) {
                return;
            }
            final boolean debug = log.isDebugEnabled();
            for (final FileEvent fileEvent : events) {
                try {
                    if (debug)
                        log.debug("Received product plugin file event: " + fileEvent);
                    final File pluginFile = fileEvent.getFileDetails().getFile();
                    if (FileOperation.CREATED.equals(fileEvent.getOperation())) {
                        File serverPlugin = new File(getServerPluginDir(), pluginFile.getName());
                        if (!serverPlugin.equals(pluginFile)) {
                            // plugin was deployed in the custom dir but already exists in the
                            // server dir.  redeploy it!
                            undeployPlugin(serverPlugin, true);
                        }
                        boolean deployed = loadAndDeployPlugin(pluginFile);
                        if (deployed) {
                            toSync.add(pluginFile.getName());
                        }
                    } else if (FileOperation.DELETED.equals(fileEvent.getOperation())) {
                        File customPlugin = new File(getCustomPluginDir(), pluginFile.getName());
                        if (customPlugin.exists()) {
                            // do nothing, the file existed in both custom dir and server
                            // then the server file was deleted
                        } else {
                            undeployPlugin(pluginFile, false);
                        }
                    } else if (FileOperation.UPDATED.equals(fileEvent.getOperation())
                            && !(pluginDirs.contains(fileEvent.getFileDetails().getFile()))) {
                        boolean undeployed = undeployPlugin(pluginFile, false);
                        if (undeployed) {
                            boolean deployed = loadAndDeployPlugin(pluginFile);
                            if (deployed) {
                                toSync.add(pluginFile.getName());
                            }
                        }
                    }
                } catch (Throwable e) {
                    log.error("Error responding to plugin file event " + fileEvent + ": " + e, e);
                }
            }
            agentManager.syncPluginToAgentsAfterCommit(toSync);
        }
    }

    private class ProductPluginFileEventListener implements FileEventListener {
        public void onFileEvent(FileEvent fileEvent) {
            synchronized (fileEvents) {
                fileEvents.add(fileEvent);
            }
        }
    }

}