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

Java tutorial

Introduction

Here is the source code for org.hyperic.hq.product.server.session.PluginManagerImpl.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-2011], VMware, 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.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.Reader;
import java.io.StringReader;
import java.io.Writer;
import java.net.JarURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarInputStream;
import java.util.jar.Manifest;

import javax.annotation.PostConstruct;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hyperic.hq.agent.server.session.AgentSynchronizer;
import org.hyperic.hq.appdef.Agent;
import org.hyperic.hq.appdef.server.session.AgentPluginStatus;
import org.hyperic.hq.appdef.server.session.AgentPluginStatusDAO;
import org.hyperic.hq.appdef.server.session.AgentPluginStatusEnum;
import org.hyperic.hq.appdef.server.session.AgentPluginSyncRestartThrottle;
import org.hyperic.hq.appdef.shared.AgentPluginUpdater;
import org.hyperic.hq.authz.server.session.AuthzSubject;
import org.hyperic.hq.authz.shared.AuthzSubjectManager;
import org.hyperic.hq.authz.shared.PermissionException;
import org.hyperic.hq.authz.shared.PermissionManager;
import org.hyperic.hq.authz.shared.ResourceManager;
import org.hyperic.hq.common.SystemException;
import org.hyperic.hq.common.shared.TransactionRetry;
import org.hyperic.hq.context.Bootstrap;
import org.hyperic.hq.measurement.server.session.MonitorableType;
import org.hyperic.hq.measurement.server.session.MonitorableTypeDAO;
import org.hyperic.hq.product.Plugin;
import org.hyperic.hq.product.shared.PluginDeployException;
import org.hyperic.hq.product.shared.PluginManager;
import org.hyperic.hq.product.shared.PluginTypeEnum;
import org.hyperic.hq.zevents.Zevent;
import org.hyperic.hq.zevents.ZeventListener;
import org.hyperic.hq.zevents.ZeventManager;
import org.hyperic.hq.zevents.ZeventPayload;
import org.hyperic.hq.zevents.ZeventSourceId;
import org.hyperic.util.file.FileUtil;
import org.hyperic.util.timer.StopWatch;
import org.jdom.Document;
import org.jdom.JDOMException;
import org.jdom.input.SAXBuilder;
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.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.xml.sax.EntityResolver;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

@Service
@Transactional(readOnly = true)
public class PluginManagerImpl implements PluginManager, ApplicationContextAware {
    private static final Log log = LogFactory.getLog(PluginManagerImpl.class);

    // this is hacky.  In a perfect world the plugin itself would define whether it is a "server"
    // plugin or not.  We shouldn't have to hardcode this :-(
    /** [HHQ-4776] a server plugin cannot be updated by the Plugin Manager UI */
    private static final Set<String> serverPlugins = new HashSet<String>();
    static {
        serverPlugins.add("system-plugin.jar");
        serverPlugins.add("netservices-plugin.jar");
        serverPlugins.add("netdevice-plugin.jar");
        serverPlugins.add("hqagent-plugin.jar");
    }
    private static final String TMP_DIR = System.getProperty("java.io.tmpdir");
    private static final String PLUGIN_DIR = "hq-plugins";
    private static final String AGENT_PLUGIN_DIR = "[/\\\\]pdk[/\\\\]plugins[/\\\\]";

    // used AtomicBoolean so that a groovy script may disable the mechanism live, no restarts
    private final AtomicBoolean isEnabled = new AtomicBoolean(true);

    private final PermissionManager permissionManager;
    private final AgentSynchronizer agentSynchronizer;
    private final AgentPluginSyncRestartThrottle agentPluginSyncRestartThrottle;
    private final PluginDAO pluginDAO;
    private final AgentPluginStatusDAO agentPluginStatusDAO;
    private final MonitorableTypeDAO monitorableTypeDAO;
    private final ResourceManager resourceManager;
    private final AuthzSubjectManager authzSubjectManager;
    private final ZeventManager zeventManager;
    private final TransactionRetry transactionRetry;

    private ApplicationContext ctx;

    private File customPluginDir;

    @Autowired
    public PluginManagerImpl(PluginDAO pluginDAO, AgentPluginStatusDAO agentPluginStatusDAO,
            MonitorableTypeDAO monitorableTypeDAO, PermissionManager permissionManager,
            ResourceManager resourceManager, AgentPluginSyncRestartThrottle agentPluginSyncRestartThrottle,
            AgentSynchronizer agentSynchronizer, AuthzSubjectManager authzSubjectManager,
            ZeventManager zeventManager, TransactionRetry transactionRetry) {
        this.pluginDAO = pluginDAO;
        this.agentPluginStatusDAO = agentPluginStatusDAO;
        this.monitorableTypeDAO = monitorableTypeDAO;
        this.permissionManager = permissionManager;
        this.agentPluginSyncRestartThrottle = agentPluginSyncRestartThrottle;
        this.agentSynchronizer = agentSynchronizer;
        this.resourceManager = resourceManager;
        this.authzSubjectManager = authzSubjectManager;
        this.zeventManager = zeventManager;
        this.transactionRetry = transactionRetry;
    }

    @PostConstruct
    public void postConstruct() {
        zeventManager.addBufferedListener(PluginFileRemoveZevent.class,
                new ZeventListener<PluginFileRemoveZevent>() {
                    public void processEvents(List<PluginFileRemoveZevent> events) {
                        for (final PluginFileRemoveZevent event : events) {
                            deletePluginFiles(event.getPluginFileNames());
                        }
                    }
                });
        zeventManager.addBufferedListener(PluginRemoveZevent.class, new ZeventListener<PluginRemoveZevent>() {
            public void processEvents(List<PluginRemoveZevent> events) {
                for (final PluginRemoveZevent event : events) {
                    final Collection<String> pluginFileNames = event.getPluginFileNames();
                    final AuthzSubject subj = event.getAuthzSubject();
                    final PluginManager pluginManager = ctx.getBean(PluginManager.class);
                    final Runnable runner = new Runnable() {
                        public void run() {
                            try {
                                pluginManager.removePlugins(subj, pluginFileNames);
                            } catch (PluginDeployException e) {
                                log.error(e, e);
                            }
                        }
                    };
                    transactionRetry.runTransaction(runner, 3, 1000);
                }
            }
        });
    }

    public Plugin getByJarName(String jarName) {
        return pluginDAO.getByFilename(jarName);
    }

    @Transactional(readOnly = false)
    public void removePlugins(AuthzSubject subj, Collection<String> pluginFileNames) throws PluginDeployException {
        try {
            permissionManager.checkIsSuperUser(subj);
        } catch (PermissionException e) {
            throw new PluginDeployException("plugin.manager.deploy.super.user", e);
        }
        // [HHQ-4776] certain plugins should not be removed from HQ
        for (Iterator<String> it = pluginFileNames.iterator(); it.hasNext();) {
            String filename = it.next();
            if (serverPlugins.contains(filename)) {
                log.error("Attempt to remove plugin with filename=" + filename + " is being ignored"
                        + " since this is a plugin of type=" + PluginTypeEnum.SERVER_PLUGIN);
                it.remove();
            }
        }
        final Set<Agent> agents = new HashSet<Agent>();
        for (final String fileName : pluginFileNames) {
            agents.addAll(agentPluginStatusDAO.getPluginStatusByFileName(fileName).keySet());
        }
        final Map<String, Plugin> pluginMap = getPluginMap(pluginFileNames);
        removePluginsAndAssociatedResources(subj, new ArrayList<Plugin>(pluginMap.values()));
        final AgentPluginUpdater agentPluginUpdater = Bootstrap.getBean(AgentPluginUpdater.class);
        final Map<Integer, Collection<String>> toRemove = new HashMap<Integer, Collection<String>>(agents.size());
        for (final Agent agent : agents) {
            toRemove.put(agent.getId(), pluginFileNames);
        }
        agentPluginUpdater.queuePluginRemoval(toRemove);
        try {
            checkCanDeletePluginFiles(pluginFileNames);
        } catch (PluginDeployException e) {
            log.error(e, e);
        }
        removePluginsWithoutAssociatedStatuses(pluginFileNames, pluginMap);
        zeventManager.enqueueEventAfterCommit(new PluginFileRemoveZevent(pluginFileNames));
    }

    @Transactional(readOnly = false)
    public void removePluginsInBackground(AuthzSubject subj, Collection<String> pluginFileNames)
            throws PluginDeployException {
        try {
            permissionManager.checkIsSuperUser(subj);
        } catch (PermissionException e) {
            throw new PluginDeployException("plugin.manager.deploy.super.user", e);
        }

        final Collection<Plugin> plugins = pluginDAO.getPluginsByFileNames(pluginFileNames);
        for (final Plugin plugin : plugins) {
            plugin.setDeleted(true);
        }
        zeventManager.enqueueEventAfterCommit(new PluginRemoveZevent(subj, pluginFileNames));
    }

    private class PluginRemoveZevent extends Zevent {
        private final AuthzSubject subj;
        private final Collection<String> pluginFileNames;

        @SuppressWarnings("serial")
        public PluginRemoveZevent(AuthzSubject subj, Collection<String> pluginFileNames) {
            super(new ZeventSourceId() {
            }, new ZeventPayload() {
            });
            this.subj = subj;
            this.pluginFileNames = pluginFileNames;
        }

        private AuthzSubject getAuthzSubject() {
            return subj;
        }

        private Collection<String> getPluginFileNames() {
            return pluginFileNames;
        }
    }

    @Transactional(readOnly = false, propagation = Propagation.REQUIRES_NEW)
    public void removeOrphanedPluginsInNewTran() throws PluginDeployException {
        final Collection<Plugin> plugins = agentPluginStatusDAO.getOrphanedPlugins();
        final boolean debug = log.isDebugEnabled();
        final Collection<String> pluginFileNames = new ArrayList<String>(plugins.size());
        for (final Plugin plugin : plugins) {
            if (debug)
                log.debug("removing orphaned plugin " + plugin);
            pluginFileNames.add(plugin.getPath());
        }
        final AuthzSubject overlord = authzSubjectManager.getOverlordPojo();
        removePlugins(overlord, pluginFileNames);
    }

    private void removePluginsWithoutAssociatedStatuses(Collection<String> pluginFileNames,
            Map<String, Plugin> pluginMap) {
        final Map<String, Long> counts = agentPluginStatusDAO.getFileNameCounts(pluginFileNames);
        for (final String filename : pluginFileNames) {
            Long count = counts.get(filename);
            if (count == null || count <= 0) {
                final Plugin plugin = pluginMap.get(filename);
                pluginDAO.remove(plugin);
            }
        }
    }

    private Map<String, Plugin> getPluginMap(Collection<String> pluginFileNames) {
        final Collection<Plugin> plugins = pluginDAO.getPluginsByFileNames(pluginFileNames);
        final Map<String, Plugin> rtn = new HashMap<String, Plugin>(plugins.size());
        for (final Plugin plugin : plugins) {
            rtn.put(plugin.getPath(), plugin);
        }
        return rtn;
    }

    private void removePluginsAndAssociatedResources(AuthzSubject subj, Collection<Plugin> plugins) {
        final long now = System.currentTimeMillis();
        for (final Plugin plugin : plugins) {
            if (plugin != null) {
                final Map<String, MonitorableType> map = monitorableTypeDAO.findByPluginName(plugin.getName());
                resourceManager.removeResourcesAndTypes(subj, map.values());
                plugin.setDeleted(true);
                plugin.setModifiedTime(now);
            }
        }
    }

    public Collection<PluginTypeEnum> getPluginType(Plugin plugin) {
        final Collection<PluginTypeEnum> rtn = new HashSet<PluginTypeEnum>();
        final String pluginFile = plugin.getPath();
        final File customFile = new File(customPluginDir, pluginFile);
        final File defaultFile = new File(getServerPluginDir(), pluginFile);
        if (serverPlugins.contains(pluginFile)) {
            rtn.add(PluginTypeEnum.SERVER_PLUGIN);
        }
        if (customFile.exists()) {
            rtn.add(PluginTypeEnum.CUSTOM_PLUGIN);
        } else if (defaultFile.exists()) {
            rtn.add(PluginTypeEnum.DEFAULT_PLUGIN);
        }
        return rtn;
    }

    @Value(value = "${server.custom.plugin.dir}")
    public void setCustomPluginDir(String customPluginDir) {
        if (this.customPluginDir != null) {
            return;
        }
        if (customPluginDir.trim().isEmpty()) {
            File wdParent = new File(System.getProperty("user.dir")).getParentFile();
            this.customPluginDir = new File(wdParent, PLUGIN_DIR);
        } else {
            final File file = new File(customPluginDir);
            if (!file.exists()) {
                final boolean success = file.mkdirs();
                if (!success) {
                    throw new SystemException("cannot create custom plugin dir, " + customPluginDir
                            + ", as defined in hq-server.conf");
                }
            } else if (!file.isDirectory()) {
                throw new SystemException(
                        "custom plugin dir, " + customPluginDir + ", defined in hq-server.conf is not a directory");
            }
            this.customPluginDir = file;
        }
    }

    public File getCustomPluginDir() {
        return customPluginDir;
    }

    public File getServerPluginDir() {
        try {
            return ctx.getResource("WEB-INF/" + PLUGIN_DIR).getFile();
        } catch (IOException e) {
            throw new SystemException(e);
        }
    }

    private void checkCanDeletePluginFiles(Collection<String> pluginFileNames) throws PluginDeployException {
        final File serverPluginDir = getServerPluginDir();
        final File customPluginDir = getCustomPluginDir();
        // Want this to be all or nothing, so first check if we can delete all the files
        for (final String filename : pluginFileNames) {
            final File customPlugin = new File(customPluginDir.getAbsolutePath() + "/" + filename);
            final File serverPlugin = new File(serverPluginDir.getAbsolutePath() + "/" + filename);
            if (!customPlugin.exists() && !serverPlugin.exists()) {
                String msg = "Could not remove plugin " + filename + " from " + customPlugin.getAbsoluteFile()
                        + " or " + serverPlugin.getAbsoluteFile() + " file does not exist."
                        + "  Will ignore and continue with plugin removal";
                log.warn(msg);
            } else if (!canDelete(customPlugin) && !canDelete(serverPlugin)) {
                final String msg = "plugin.manager.delete.filesystem.perms";
                throw new PluginDeployException(msg, filename, customPlugin.getAbsolutePath(),
                        serverPlugin.getAbsolutePath());
            }
        }
    }

    private void deletePluginFiles(Collection<String> pluginFileNames) {
        final File serverPluginDir = getServerPluginDir();
        final File customPluginDir = getCustomPluginDir();
        for (final String filename : pluginFileNames) {
            final File customPlugin = new File(customPluginDir.getAbsolutePath() + "/" + filename);
            final File serverPlugin = new File(serverPluginDir.getAbsolutePath() + "/" + filename);
            customPlugin.delete();
            serverPlugin.delete();
        }
    }

    private boolean canDelete(File file) {
        if (!file.exists()) {
            return false;
        }
        // if a user does not have write perms to the dir or the file then they can't delete it
        if (!file.getParentFile().canWrite() && !file.canWrite()) {
            return false;
        }
        return true;
    }

    public Set<Integer> getAgentIdsInQueue() {
        final Set<Integer> rtn = new HashSet<Integer>();
        rtn.addAll(agentSynchronizer.getJobListByDescription(Arrays.asList(new String[] {
                AgentPluginUpdater.AGENT_PLUGIN_REMOVE, AgentPluginUpdater.AGENT_PLUGIN_TRANSFER })));
        rtn.addAll(agentPluginSyncRestartThrottle.getQueuedAgentIds());
        return rtn;
    }

    public Map<Integer, Long> getAgentIdsInRestartState() {
        return agentPluginSyncRestartThrottle.getAgentIdsInRestartState();
    }

    // XXX currently if one plugin validation fails all will fail.  Probably want to deploy the
    // plugins that are valid and return error status if any fail.
    public void deployPluginIfValid(AuthzSubject subj, Map<String, byte[]> pluginInfo)
            throws PluginDeployException {
        validatePluginFileNotInDeleteState(pluginInfo.keySet());
        final Collection<File> files = new ArrayList<File>();
        for (final Entry<String, byte[]> entry : pluginInfo.entrySet()) {
            final String filename = entry.getKey();
            final byte[] bytes = entry.getValue();
            File file = null;
            if (serverPlugins.contains(filename.toLowerCase())) {
                throw new PluginDeployException("plugin.cannot.deploy.server.type.plugin", filename);
            } else if (filename.toLowerCase().endsWith(".jar")) {
                file = getFileAndValidateJar(filename, bytes);
            } else if (filename.toLowerCase().endsWith(".xml")) {
                file = getFileAndValidateXML(filename, bytes);
            } else {
                throw new PluginDeployException("plugin.manager.bad.file.extension", filename);
            }
            files.add(file);
        }
        deployPlugins(files);
    }

    private void validatePluginFileNotInDeleteState(Collection<String> pluginFileNames)
            throws PluginDeployException {
        Collection<Plugin> plugins = pluginDAO.getPluginsByFileNames(pluginFileNames);
        for (Plugin plugin : plugins) {
            if (plugin == null) {
                continue;
            }
            if (plugin.isDeleted()) {
                throw new PluginDeployException("plugin.manager.plugin.is.deleted", plugin.getPath());
            }
        }
    }

    private File getFileAndValidateXML(String filename, byte[] bytes) throws PluginDeployException {
        FileWriter writer = null;
        File rtn = null;
        try {
            rtn = new File(TMP_DIR + File.separator + filename);
            final ByteArrayInputStream is = new ByteArrayInputStream(bytes);
            final Document doc = getDocument(new InputStreamReader(is), new HashMap<String, Reader>());
            final String name = doc.getRootElement().getName();
            if (!name.equals("plugin")) {
                throw new PluginDeployException("plugin.manager.invalid.xml", filename);
            }
            writer = new FileWriter(rtn);
            final String str = new String(bytes);
            writer.write(str);
            return rtn;
        } catch (JDOMException e) {
            if (rtn != null && rtn.exists()) {
                rtn.delete();
            }
            throw new PluginDeployException("plugin.manager.file.xml.wellformed.error", e, filename);
        } catch (IOException e) {
            if (rtn != null && rtn.exists()) {
                rtn.delete();
            }
            throw new PluginDeployException("plugin.manager.file.ioexception", e, filename);
        } finally {
            close(writer);
        }
    }

    private void deployPlugins(Collection<File> files) throws PluginDeployException {
        final File pluginDir = getCustomPluginDir();
        if (!pluginDir.exists() && !pluginDir.isDirectory() && !pluginDir.mkdir()) {
            throw new SystemException(pluginDir.getAbsolutePath() + " does not exist or is not a directory");
        }
        for (final File file : files) {
            final File dest = new File(pluginDir.getAbsolutePath() + File.separator + file.getName());
            if (!file.renameTo(dest)) {
                // Rename sometimes fails on Windows for no apparent reason
                try {
                    FileUtil.copyFile(file, dest);
                } catch (FileNotFoundException e) {
                    throw new PluginDeployException("plugin.manager.file.notfound.exception", e);
                } catch (IOException e) {
                    throw new PluginDeployException("plugin.manager.file.ioexception", e, file.getName());
                } finally {
                    file.delete();
                }
            }
        }
    }

    private File getFileAndValidateJar(String filename, byte[] bytes) throws PluginDeployException {
        ByteArrayInputStream bais = null;
        JarInputStream jis = null;
        FileOutputStream fos = null;
        String file = null;
        try {
            bais = new ByteArrayInputStream(bytes);
            jis = new JarInputStream(bais);
            final Manifest manifest = jis.getManifest();
            if (manifest == null) {
                throw new PluginDeployException("plugin.manager.jar.manifest.does.not.exist", filename);
            }
            file = TMP_DIR + File.separator + filename;
            fos = new FileOutputStream(file);
            fos.write(bytes);
            fos.flush();
            final File rtn = new File(file);
            final URL url = new URL("jar", "", "file:" + file + "!/");
            final JarURLConnection jarConn = (JarURLConnection) url.openConnection();
            final JarFile jarFile = jarConn.getJarFile();
            processJarEntries(jarFile, file, filename);
            return rtn;
        } catch (IOException e) {
            final File toRemove = new File(file);
            if (toRemove != null && toRemove.exists()) {
                toRemove.delete();
            }
            throw new PluginDeployException("plugin.manager.file.ioexception", e, filename);
        } finally {
            close(jis);
            close(fos);
        }
    }

    private void processJarEntries(JarFile jarFile, String jarFilename, String filename)
            throws PluginDeployException, IOException {
        final Map<String, JDOMException> xmlFailures = new HashMap<String, JDOMException>();
        boolean hasPluginRootElement = false;
        final Enumeration<JarEntry> entries = jarFile.entries();
        final Map<String, Reader> xmlReaders = getXmlReaderMap(jarFile);
        while (entries.hasMoreElements()) {
            Reader reader = null;
            String currXml = null;
            try {
                final JarEntry entry = entries.nextElement();
                if (entry.isDirectory()) {
                    continue;
                }
                if (!entry.getName().toLowerCase().endsWith(".xml")) {
                    continue;
                }
                currXml = entry.getName();
                reader = xmlReaders.get(currXml);
                final Document doc = getDocument(reader, xmlReaders);
                if (doc.getRootElement().getName().toLowerCase().equals("plugin")) {
                    hasPluginRootElement = true;
                }
                currXml = null;
            } catch (JDOMException e) {
                log.debug(e, e);
                xmlFailures.put(currXml, e);
            }
        }
        if (!hasPluginRootElement) {
            final File toRemove = new File(jarFilename);
            if (toRemove != null && toRemove.exists()) {
                toRemove.delete();
            }
            if (!xmlFailures.isEmpty()) {
                for (final Entry<String, JDOMException> entry : xmlFailures.entrySet()) {
                    final String xml = entry.getKey();
                    JDOMException ex = entry.getValue();
                    log.error("could not parse " + xml, ex);
                }
                throw new PluginDeployException("plugin.manager.file.xml.wellformed.error",
                        xmlFailures.keySet().toString());
            } else {
                throw new PluginDeployException("plugin.manager.no.plugin.root.element", filename);
            }
        }
    }

    private Map<String, Reader> getXmlReaderMap(JarFile jarFile) throws IOException {
        final Map<String, Reader> rtn = new HashMap<String, Reader>();
        final Enumeration<JarEntry> entries = jarFile.entries();
        while (entries.hasMoreElements()) {
            final JarEntry entry = entries.nextElement();
            if (entry.isDirectory()) {
                continue;
            }
            if (!entry.getName().toLowerCase().endsWith(".xml")) {
                continue;
            }
            InputStream is = null;
            try {
                is = jarFile.getInputStream(entry);
                final BufferedReader br = new BufferedReader(new InputStreamReader(is));
                final StringBuilder buf = new StringBuilder();
                String tmp;
                while (null != (tmp = br.readLine())) {
                    buf.append(tmp);
                }
                rtn.put(entry.getName(), new NoCloseStringReader(buf.toString()));
            } finally {
                close(is);
            }
        }
        return rtn;
    }

    private Document getDocument(Reader reader, final Map<String, Reader> xmlReaders)
            throws JDOMException, IOException {
        final SAXBuilder builder = new SAXBuilder();
        builder.setEntityResolver(new EntityResolver() {
            // systemId = file:///pdk/plugins/process-metrics.xml
            public InputSource resolveEntity(String publicId, String systemId) throws SAXException, IOException {
                final File entity = new File(systemId);
                for (final Entry<String, Reader> entry : xmlReaders.entrySet()) {
                    final String filename = entry.getKey();
                    if (entity.getAbsolutePath().contains(filename)) {
                        return new InputSource(entry.getValue());
                    }
                }
                final String filename = entity.getName().replaceAll(AGENT_PLUGIN_DIR, "");
                File file = new File(getCustomPluginDir(), filename);
                if (!file.exists()) {
                    file = new File(getServerPluginDir(), filename);
                }
                return (file.exists()) ? new InputSource(file.toURI().toString())
                        : new InputSource(xmlReaders.get(filename));
            }
        });
        return builder.build(reader);
    }

    private class NoCloseStringReader extends StringReader {
        private NoCloseStringReader(String s) {
            super(s);
        }

        @Override
        public void close() {
        }
    }

    public Map<Integer, Map<AgentPluginStatusEnum, Integer>> getPluginRollupStatus() {
        final StopWatch watch = new StopWatch();
        final boolean debug = log.isDebugEnabled();
        final Map<String, Plugin> pluginsByName = getAllPluginsByName();
        final List<Integer> statusIds = agentPluginStatusDAO.getAllIds();
        final Map<Integer, Map<AgentPluginStatusEnum, Integer>> rtn = new HashMap<Integer, Map<AgentPluginStatusEnum, Integer>>(
                statusIds.size());
        if (debug)
            watch.markTimeBegin("loop");
        for (final Integer statusId : statusIds) {
            if (debug)
                watch.markTimeBegin("get");
            final AgentPluginStatus status = agentPluginStatusDAO.get(statusId);
            if (debug)
                watch.markTimeEnd("get");
            if (status == null || status.getAgent().getPlatforms().isEmpty()) {
                continue;
            }
            final String name = status.getPluginName();
            final Plugin plugin = pluginsByName.get(name);
            if (plugin == null) {
                continue;
            }
            if (debug)
                watch.markTimeBegin("setPluginRollup");
            setPluginRollup(status, plugin.getId(), rtn);
            if (debug)
                watch.markTimeEnd("setPluginRollup");
        }
        if (debug)
            watch.markTimeEnd("loop");
        if (debug)
            log.debug(watch);
        return rtn;
    }

    private void setPluginRollup(AgentPluginStatus status, Integer pluginId,
            Map<Integer, Map<AgentPluginStatusEnum, Integer>> map) {
        Map<AgentPluginStatusEnum, Integer> tmp;
        if (null == (tmp = map.get(pluginId))) {
            tmp = new HashMap<AgentPluginStatusEnum, Integer>();
            tmp.put(AgentPluginStatusEnum.SYNC_FAILURE, 0);
            tmp.put(AgentPluginStatusEnum.SYNC_IN_PROGRESS, 0);
            tmp.put(AgentPluginStatusEnum.SYNC_SUCCESS, 0);
            map.put(pluginId, tmp);
        }
        final String lastSyncStatus = status.getLastSyncStatus();
        if (lastSyncStatus == null) {
            return;
        }
        final AgentPluginStatusEnum e = AgentPluginStatusEnum.valueOf(lastSyncStatus);
        tmp.put(e, tmp.get(e) + 1);
    }

    public Plugin getPluginById(Integer id) {
        return pluginDAO.get(id);
    }

    @Transactional(readOnly = false)
    public void markDisabled(Collection<Integer> pluginIds) {
        final long now = System.currentTimeMillis();
        for (final Integer pluginId : pluginIds) {
            final Plugin plugin = pluginDAO.get(pluginId);
            if (plugin == null || plugin.isDeleted() || plugin.isDisabled()) {
                continue;
            }
            plugin.setDisabled(true);
            plugin.setModifiedTime(now);
        }
    }

    public Map<String, Integer> getAllPluginIdsByName() {
        final List<Plugin> plugins = pluginDAO.findAll();
        final Map<String, Integer> rtn = new HashMap<String, Integer>(plugins.size());
        for (final Plugin plugin : plugins) {
            rtn.put(plugin.getName(), plugin.getId());
        }
        return rtn;
    }

    private Map<String, Plugin> getAllPluginsByName() {
        final List<Plugin> plugins = pluginDAO.findAll();
        final Map<String, Plugin> rtn = new HashMap<String, Plugin>(plugins.size());
        for (final Plugin plugin : plugins) {
            rtn.put(plugin.getName(), plugin);
        }
        return rtn;
    }

    public Collection<AgentPluginStatus> getStatusesByPluginId(int pluginId, AgentPluginStatusEnum... keys) {
        if (keys.length == 0) {
            return Collections.emptyList();
        }
        final Plugin plugin = pluginDAO.get(pluginId);
        if (plugin == null) {
            return Collections.emptyList();
        }
        return agentPluginStatusDAO.getPluginStatusByFileName(plugin.getPath(), Arrays.asList(keys));
    }

    public Map<Integer, AgentPluginStatus> getStatusesByAgentId(AgentPluginStatusEnum... keys) {
        final Map<Integer, AgentPluginStatus> rtn = new HashMap<Integer, AgentPluginStatus>();
        final List<AgentPluginStatus> statuses = agentPluginStatusDAO.getPluginStatusByAgent(keys);
        for (final AgentPluginStatus status : statuses) {
            rtn.put(status.getAgent().getId(), status);
        }
        return rtn;
    }

    public boolean isPluginSyncEnabled() {
        return isEnabled.get();
    }

    @Value(value = "${server.pluginsync.enabled}")
    public void setPluginSyncEnabled(boolean enabled) {
        isEnabled.set(enabled);
    }

    public Map<Plugin, Collection<AgentPluginStatus>> getOutOfSyncAgentsByPlugin() {
        return agentPluginStatusDAO.getOutOfSyncAgentsByPlugin();
    }

    public List<Plugin> getAllPlugins() {
        return pluginDAO.findAll();
    }

    public Collection<String> getOutOfSyncPluginNamesByAgentId(Integer agentId) {
        return agentPluginStatusDAO.getOutOfSyncPluginNamesByAgentId(agentId);
    }

    @Transactional(readOnly = false)
    public void updateAgentPluginSyncStatus(Integer agentId, AgentPluginStatusEnum from, AgentPluginStatusEnum to) {
        final Collection<Plugin> plugins = pluginDAO.findAll();
        final Map<String, AgentPluginStatus> statusMap = agentPluginStatusDAO.getStatusByAgentId(agentId);
        for (final Plugin plugin : plugins) {
            if (plugin == null || plugin.isDisabled()) {
                continue;
            }
            final AgentPluginStatus status = statusMap.get(plugin.getName());
            if (status == null || !status.getLastSyncStatus().equals(from.toString())) {
                continue;
            }
            status.setLastSyncStatus(to.toString());
        }
    }

    @Transactional(readOnly = false)
    public void updateAgentPluginSyncStatus(AgentPluginStatusEnum status,
            Map<Integer, Collection<Plugin>> agentToPlugins, Map<Integer, Collection<String>> agentToFileNames) {
        if (agentToPlugins == null) {
            agentToPlugins = Collections.emptyMap();
        }
        if (agentToFileNames == null) {
            agentToFileNames = Collections.emptyMap();
        }
        if (agentToPlugins.isEmpty() && agentToFileNames.isEmpty()) {
            return;
        }
        final long now = System.currentTimeMillis();
        final Set<Integer> agentIds = new HashSet<Integer>(agentToPlugins.keySet());
        agentIds.addAll(agentToFileNames.keySet());
        final Map<Integer, Map<String, AgentPluginStatus>> statusMap = agentPluginStatusDAO
                .getStatusByAgentIds(agentIds);
        for (final Entry<Integer, Collection<Plugin>> entry : agentToPlugins.entrySet()) {
            final Integer agentId = entry.getKey();
            final Map<String, AgentPluginStatus> map = statusMap.get(agentId);
            if (map == null) {
                continue;
            }
            final Collection<Plugin> plugins = entry.getValue();
            updateStatuses(agentId, plugins, map, now, status);
        }
        for (final Entry<Integer, Collection<String>> entry : agentToFileNames.entrySet()) {
            final Integer agentId = entry.getKey();
            final Map<String, AgentPluginStatus> map = statusMap.get(agentId);
            if (map == null) {
                continue;
            }
            final Collection<String> filenames = entry.getValue();
            final Collection<Plugin> plugins = pluginDAO.getPluginsByFileNames(filenames);
            updateStatuses(agentId, plugins, map, now, status);
        }
    }

    private void updateStatuses(Integer agentId, Collection<Plugin> plugins, Map<String, AgentPluginStatus> map,
            long now, AgentPluginStatusEnum s) {
        final String inProgress = AgentPluginStatusEnum.SYNC_IN_PROGRESS.toString();
        for (final Plugin plugin : plugins) {
            final AgentPluginStatus status = map.get(plugin.getName());
            if (status == null) {
                continue;
            }
            final String lastSyncStatus = status.getLastSyncStatus();
            if ((lastSyncStatus == null || !lastSyncStatus.equals(inProgress))
                    && s == AgentPluginStatusEnum.SYNC_IN_PROGRESS) {
                status.setLastSyncAttempt(now);
            }
            status.setLastSyncStatus(s.toString());
        }
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW, readOnly = false)
    public void updateAgentPluginSyncStatusInNewTran(AgentPluginStatusEnum s, Integer agentId,
            Collection<Plugin> plugins) {
        final String inProgress = AgentPluginStatusEnum.SYNC_IN_PROGRESS.toString();
        if (plugins == null) {
            plugins = pluginDAO.findAll();
        }
        if (plugins.isEmpty()) {
            return;
        }
        final Map<String, AgentPluginStatus> statusMap = agentPluginStatusDAO.getStatusByAgentId(agentId);
        final long now = System.currentTimeMillis();
        for (final Plugin plugin : plugins) {
            if (plugin == null || plugin.isDisabled()) {
                continue;
            }
            final AgentPluginStatus status = statusMap.get(plugin.getName());
            if (status == null) {
                continue;
            }
            // only setLastSyncAttempt if it changes from !"in progress" to "in progress"
            if (!status.getLastSyncStatus().equals(inProgress) && s == AgentPluginStatusEnum.SYNC_IN_PROGRESS) {
                status.setLastSyncAttempt(now);
            }
            status.setLastSyncStatus(s.toString());
        }
    }

    private void close(OutputStream os) {
        if (os == null) {
            return;
        }
        try {
            os.close();
        } catch (IOException e) {
            log.debug(e, e);
        }
    }

    private void close(Writer writer) {
        if (writer == null) {
            return;
        }
        try {
            writer.close();
        } catch (IOException e) {
            log.debug(e, e);
        }
    }

    private void close(InputStream is) {
        if (is == null) {
            return;
        }
        try {
            is.close();
        } catch (IOException e) {
            log.debug(e, e);
        }
    }

    @Transactional(readOnly = false)
    public void removeAgentPluginStatuses(Integer agentId, Collection<String> pluginFileNames) {
        agentPluginStatusDAO.removeAgentPluginStatuses(agentId, pluginFileNames);
    }

    @Transactional(readOnly = false)
    public void markDisabled(String pluginFileName) {
        final Plugin plugin = pluginDAO.getByFilename(pluginFileName);
        if (plugin == null || plugin.isDeleted()) {
            return;
        }
        if (!plugin.isDisabled()) {
            plugin.setDisabled(true);
            plugin.setModifiedTime(System.currentTimeMillis());
        }
    }

    public void setApplicationContext(ApplicationContext ctx) throws BeansException {
        this.ctx = ctx;
    }

    @Transactional(readOnly = false)
    public void markPluginDisabledByName(String pluginName) {
        final Plugin plugin = pluginDAO.findByName(pluginName);
        if (plugin == null || plugin.isDeleted()) {
            return;
        }
        if (!plugin.isDisabled()) {
            plugin.setDisabled(true);
            plugin.setModifiedTime(System.currentTimeMillis());
        }
    }

    @Transactional(readOnly = false)
    public void markEnabled(String pluginName) {
        final Plugin plugin = pluginDAO.findByName(pluginName);
        if (plugin == null || plugin.isDeleted()) {
            return;
        }
        if (plugin.isDisabled()) {
            plugin.setDisabled(false);
            plugin.setModifiedTime(System.currentTimeMillis());
        }
    }

    private class PluginFileRemoveZevent extends Zevent {
        @SuppressWarnings("serial")
        private PluginFileRemoveZevent(Collection<String> pluginFileNames) {
            super(new ZeventSourceId() {
            }, new PluginFileRemovePayload(pluginFileNames));
        }

        private Collection<String> getPluginFileNames() {
            return ((PluginFileRemovePayload) getPayload()).getPluginFileNames();
        }
    }

    private class PluginFileRemovePayload implements ZeventPayload {
        private final Collection<String> pluginFileNames;

        private PluginFileRemovePayload(Collection<String> pluginFileNames) {
            this.pluginFileNames = pluginFileNames;
        }

        private Collection<String> getPluginFileNames() {
            return pluginFileNames;
        }
    }

}