org.easyrec.plugin.container.PluginRegistry.java Source code

Java tutorial

Introduction

Here is the source code for org.easyrec.plugin.container.PluginRegistry.java

Source

/*
 * Copyright 2010 Research Studios Austria Forschungsgesellschaft mBH
 *
 * This file is part of easyrec.
 *
 * easyrec 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, either version 3 of the License, or
 * (at your option) any later version.
 *
 * easyrec 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 easyrec.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.easyrec.plugin.container;

import com.google.common.collect.Maps;
import org.apache.commons.io.IOUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.easyrec.model.core.TenantVO;
import org.easyrec.model.plugin.PluginVO;
import org.easyrec.plugin.Executable.ExecutionState;
import org.easyrec.plugin.Plugin.LifecyclePhase;
import org.easyrec.plugin.generator.Generator;
import org.easyrec.plugin.generator.GeneratorConfiguration;
import org.easyrec.plugin.model.PluginId;
import org.easyrec.plugin.model.Version;
import org.easyrec.plugin.stats.GeneratorStatistics;
import org.easyrec.plugin.support.GeneratorPluginSupport;
import org.easyrec.service.core.TenantService;
import org.easyrec.service.domain.TypeMappingService;
import org.easyrec.store.dao.core.ItemAssocDAO;
import org.easyrec.store.dao.core.types.AssocTypeDAO;
import org.easyrec.store.dao.plugin.NamedConfigurationDAO;
import org.easyrec.store.dao.plugin.PluginDAO;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.core.io.Resource;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.net.URI;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import org.springframework.beans.factory.DisposableBean;

/**
 * @author szavrel
 */
public class PluginRegistry implements ApplicationContextAware, DisposableBean {

    public static final String DEFAULT_PLUGIN_CONFIG_FILE = "easyrec-plugin.xml";
    public static final String GENERATOR_PROP = "generator";
    public static final String PLUGINS_ENABLED_PROP = "plugins.enabled";

    private final Log logger = LogFactory.getLog(this.getClass());

    private ApplicationContext appContext;
    private Resource pluginFolder;
    private PluginDAO pluginDAO;
    private ItemAssocDAO itemAssocDAO;
    private TenantService tenantService;
    private TypeMappingService typeMappingService;
    private Properties properties;
    private Resource overrideFolder;
    private NamedConfigurationDAO namedConfigurationDAO;

    private Map<PluginId, Generator<GeneratorConfiguration, GeneratorStatistics>> generators;
    private Map<PluginId, ClassPathXmlApplicationContext> contexts = Maps.newHashMap();

    public PluginRegistry(Resource pluginFolder, PluginDAO pluginDAO, ItemAssocDAO itemAssocDAO,
            TenantService tenantService, TypeMappingService typeMappingService,
            Map<PluginId, Generator<GeneratorConfiguration, GeneratorStatistics>> generators) {
        this.pluginFolder = pluginFolder;
        this.pluginDAO = pluginDAO;
        this.itemAssocDAO = itemAssocDAO;
        this.tenantService = tenantService;
        this.typeMappingService = typeMappingService;
        this.generators = generators;
    }

    public void init() throws Exception {
        logger.info("Loading plugins ...");
        if (properties != null && "true".equals(properties.getProperty("easyrec.firstrun"))) {
            try {
                installDefaultOnFirstRun();
            } catch (Exception e) {
                logger.error(
                        "An error occured trying to install the default plugins! Try installing plugin manually!",
                        e);
            }
        } else {
            List<PluginVO> installedPlugins = this.pluginDAO.loadPluginInfos(LifecyclePhase.INITIALIZED.toString());
            if (installedPlugins != null && installedPlugins.size() > 0) {
                for (PluginVO plugin : installedPlugins) {
                    logger.info(
                            "Loading plugin " + plugin.getPluginId() + " - " + plugin.getPluginId().getVersion());
                    installPlugin(plugin.getPluginId().getUri(), plugin.getPluginId().getVersion());
                }
            }
        }
    }

    @SuppressWarnings({ "unchecked" })
    public void installPlugin(URI pluginId, Version version) {
        FileOutputStream fos = null;
        File tmpFile = null;

        try {
            PluginVO plugin = pluginDAO.loadPlugin(pluginId, version);
            if (plugin == null)
                throw new Exception("Plugin not found in DB!");
            // write file to plugin folder
            tmpFile = new File(pluginFolder.getFile(),
                    plugin.getId().toString() + "_" + plugin.getDisplayName() + ".jar");
            fos = new FileOutputStream(tmpFile);
            fos.write(plugin.getFile());
            fos.close();

            // install the plugin

            PluginClassLoader ucl = new PluginClassLoader(new URL[] { tmpFile.toURI().toURL() },
                    this.getClass().getClassLoader());

            if (ucl.findResource(DEFAULT_PLUGIN_CONFIG_FILE) == null) {
                logger.warn("no " + DEFAULT_PLUGIN_CONFIG_FILE + " found in plugin jar ");
                return;
            }

            ClassPathXmlApplicationContext cax = new ClassPathXmlApplicationContext(
                    new String[] { DEFAULT_PLUGIN_CONFIG_FILE }, false, appContext);
            cax.setClassLoader(ucl);
            cax.refresh();
            //            cax.stop();
            //            cax.start();

            // currently only GeneratorPluginSupport is used
            Map<String, GeneratorPluginSupport> beans = cax.getBeansOfType(GeneratorPluginSupport.class);

            if (beans.isEmpty()) {
                logger.warn("no GeneratorPluginSupport subclasses found in plugin jar");
                return;
            }

            Generator<GeneratorConfiguration, GeneratorStatistics> generator = beans.values().iterator().next();

            installGenerator(pluginId, version, plugin, cax, generator);
        } catch (Exception e) {
            logger.error("An Exception occurred while installing the plugin!", e);

            pluginDAO.updatePluginState(pluginId, version, LifecyclePhase.INSTALL_FAILED.toString());
        } finally {
            if (fos != null)
                try {
                    fos.close();
                } catch (Exception ignored) {
                    logger.warn("could not close file output stream", ignored);
                }

            /*
            if (tmpFile != null)
            try {
                tmpFile.delete();
            } catch (Exception ignored) {
                logger.warn("could not delete temporary plugin file", ignored);
            }
            */
        }
    }

    private void installGenerator(final URI pluginId, final Version version, final PluginVO plugin,
            final ClassPathXmlApplicationContext cax,
            final Generator<GeneratorConfiguration, GeneratorStatistics> generator) {
        cax.getAutowireCapableBeanFactory().autowireBeanProperties(generator,
                AutowireCapableBeanFactory.AUTOWIRE_BY_NAME, false);

        if (generator.getConfiguration() == null) {
            GeneratorConfiguration generatorConfiguration = generator.newConfiguration();
            generator.setConfiguration(generatorConfiguration);
        }

        if (LifecyclePhase.NOT_INSTALLED.toString().equals(plugin.getState()))
            generator.install(true);
        else
            generator.install(false);

        pluginDAO.updatePluginState(pluginId, version, LifecyclePhase.INSTALLED.toString());

        generator.initialize();
        generators.put(generator.getId(), generator);
        contexts.put(generator.getId(), cax);
        logger.info("registered plugin " + generator.getSourceType());
        pluginDAO.updatePluginState(pluginId, version, LifecyclePhase.INITIALIZED.toString());
    }

    @SuppressWarnings({ "unchecked" })
    public PluginVO checkPlugin(byte[] file) throws Exception {
        PluginVO plugin;
        FileOutputStream fos = null;
        URLClassLoader ucl;
        ClassPathXmlApplicationContext cax = null;
        File tmpFile = null;

        try {
            if (file == null)
                throw new IllegalArgumentException("Passed file must not be null!");

            tmpFile = File.createTempFile("plugin", null);
            tmpFile.deleteOnExit();

            fos = new FileOutputStream(tmpFile);
            fos.write(file);
            fos.close();

            // check if plugin is valid
            ucl = new URLClassLoader(new URL[] { tmpFile.toURI().toURL() }, this.getClass().getClassLoader());

            if (ucl.getResourceAsStream(DEFAULT_PLUGIN_CONFIG_FILE) != null) {
                cax = new ClassPathXmlApplicationContext(new String[] { DEFAULT_PLUGIN_CONFIG_FILE }, false,
                        appContext);
                cax.setClassLoader(ucl);
                logger.info("Classloader: " + cax.getClassLoader());
                cax.refresh();

                Map<String, GeneratorPluginSupport> beans = cax.getBeansOfType(GeneratorPluginSupport.class);

                if (beans.isEmpty()) {
                    logger.debug("No class implementing a generator could be found. Plugin rejected!");
                    throw new Exception("No class implementing a generator could be found. Plugin rejected!");
                }

                Generator<GeneratorConfiguration, GeneratorStatistics> generator = beans.values().iterator().next();

                logger.info(String.format("Plugin successfully validated! class: %s name: %s, id: %s",
                        generator.getClass(), generator.getDisplayName(), generator.getId()));

                cax.getAutowireCapableBeanFactory().autowireBeanProperties(generator,
                        AutowireCapableBeanFactory.AUTOWIRE_BY_NAME, false);

                plugin = new PluginVO(generator.getDisplayName(), generator.getId().getUri(),
                        generator.getId().getVersion(), LifecyclePhase.NOT_INSTALLED.toString(), file, null);

                if (tmpFile.delete())
                    logger.info("tmpFile deleted successfully");

                return plugin;
            } else { // config file not found
                logger.debug("No valid config file found in the supplied .jar file. Plugin rejected!");
                throw new Exception("No valid config file found in the supplied .jar file. Plugin rejected!");
            }
        } catch (Exception e) {
            logger.error("An Exception occurred while checking the plugin!", e);

            throw e;
        } finally {
            if (fos != null)
                fos.close();

            if ((cax != null) && (!cax.isActive()))
                cax.close();

            if (tmpFile != null)
                try {
                    if (!tmpFile.delete())
                        logger.warn("could not delete tmpFile");
                } catch (SecurityException se) {
                    logger.error("Could not delete temporary file! Please check permissions!", se);
                }
        }
    }

    public void deactivatePlugin(URI pluginId, Version version) {
        PluginId key = new PluginId(pluginId, version);
        Generator<GeneratorConfiguration, GeneratorStatistics> generator = generators.get(key);

        if ((generator != null) && (LifecyclePhase.INITIALIZED.equals(generator.getLifecyclePhase()))) {
            String sourceType = generator.getSourceType();

            generator.cleanup();
            pluginDAO.updatePluginState(pluginId, version, LifecyclePhase.INSTALLED.toString());
            generators.remove(key);

            ClassPathXmlApplicationContext cax = contexts.get(key);

            generator.uninstall();
            pluginDAO.updatePluginState(pluginId, version, LifecyclePhase.NOT_INSTALLED.toString());

            if (cax != null)
                cax.close();

            contexts.remove(key);

            if (logger.isDebugEnabled())
                logger.debug("Deactivating configurations for " + key.getUri() + "-" + key.getVersion());

            int deactivates = namedConfigurationDAO.deactivateByPlugin(key);

            if (logger.isDebugEnabled())
                logger.debug("Deactivated " + deactivates + " plugins");

            List<TenantVO> tenants = tenantService.getAllTenants();

            for (TenantVO tenant : tenants) {
                Integer sourceTypeId;

                try {
                    sourceTypeId = typeMappingService.getIdOfSourceType(tenant.getId(), sourceType);
                } catch (IllegalArgumentException iae) {
                    logger.info(
                            String.format("Source type %s not defined for tenant %d", sourceType, tenant.getId()));
                    continue;
                }

                int removedRows = itemAssocDAO.removeItemAssocByTenant(tenant.getId(), null, sourceTypeId, null);

                logger.info(String.format(
                        "Removed %d item assocs of source type %d for tenant %d because plugin is deactivating.",
                        removedRows, sourceTypeId, tenant.getId()));
            }
        }
    }

    public String getPluginDescription(URI pluginId, Version version) {
        PluginId key = new PluginId(pluginId, version);
        Generator<GeneratorConfiguration, GeneratorStatistics> generator = generators.get(key);

        if (generator != null)
            return generator.getPluginDescription();

        return "";
    }

    @SuppressWarnings({ "UnusedDeclaration" })
    public void deletePlugin(URI pluginId, Version version) {
        PluginId key = new PluginId(pluginId, version);
        Generator<GeneratorConfiguration, GeneratorStatistics> generator = generators.get(key);

        if ((generator != null) && (LifecyclePhase.INITIALIZED.equals(generator.getLifecyclePhase())))
            deactivatePlugin(pluginId, version);

        pluginDAO.deletePlugin(pluginId, version);
    }

    public boolean isAllExecutablesStopped() {
        for (Generator<GeneratorConfiguration, GeneratorStatistics> executable : this.generators.values()) {
            ExecutionState state = executable.getExecutionState();

            if (state.isRunning() || state.isAbortRequested())
                return false;
        }

        return true;
    }

    public void installDefaultOnFirstRun() throws Exception {
        if (properties != null && "true".equals(properties.getProperty("easyrec.firstrun"))) {
            properties.setProperty("easyrec.firstrun", "false");
            File of = new File(overrideFolder.getFile(), "easyrec.database.properties");
            properties.store(new FileOutputStream(of), "");

            logger.info("First run after install... installing/updating default plugins!");
            HashMap<URI, Version> installedPlugins = new HashMap<URI, Version>();
            for (PluginVO plugin : pluginDAO.loadPluginInfos()) {
                installedPlugins.put(plugin.getPluginId().getUri(), plugin.getPluginId().getVersion());
            }
            File[] files = new File[0];

            if (pluginFolder.exists())
                files = pluginFolder.getFile().listFiles(new FilenameFilter() {

                    @Override
                    public boolean accept(File dir, String name) {
                        return name.endsWith(".jar");
                    }
                });

            for (File file : files) {
                byte[] pluginContent = IOUtils.toByteArray(new FileInputStream(file));
                PluginVO defaultPlugin = checkPlugin(pluginContent);

                if (defaultPlugin != null) {
                    // if an older version of a default plugin exists, delete it
                    if (installedPlugins.containsKey(defaultPlugin.getPluginId().getUri())) {
                        if (installedPlugins.get(defaultPlugin.getPluginId().getUri())
                                .compareTo(defaultPlugin.getPluginId().getVersion()) < 0) {
                            pluginDAO.deletePlugin(defaultPlugin.getPluginId().getUri(),
                                    installedPlugins.get(defaultPlugin.getPluginId().getUri()));
                        }
                        installedPlugins.remove(defaultPlugin.getPluginId().getUri());
                    }

                    pluginDAO.storePlugin(defaultPlugin);
                    installPlugin(defaultPlugin.getPluginId().getUri(), defaultPlugin.getPluginId().getVersion());

                }
            }
            // set remaining installed plugins to status INSTALLED; need to be initialized manually after update
            for (Entry<URI, Version> plugin : installedPlugins.entrySet()) {
                pluginDAO.updatePluginState(plugin.getKey(), plugin.getValue(),
                        LifecyclePhase.INSTALLED.toString());
            }

        }

        // check if assocType "IS_RELATED" exists for all tenants, if not add it
        List<TenantVO> tenants = tenantService.getAllTenants();
        for (TenantVO tenantVO : tenants) {
            tenantService.insertAssocTypeForTenant(tenantVO.getId(), AssocTypeDAO.ASSOCTYPE_IS_RELATED, true);
        }
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.appContext = applicationContext;
    }

    @Override
    public void destroy() throws Exception {
        for (ClassPathXmlApplicationContext ctx : contexts.values()) {
            ctx.close();
        }
    }

    public void setPluginFolder(Resource pluginFolder) {
        this.pluginFolder = pluginFolder;
    }

    public Map<PluginId, Generator<GeneratorConfiguration, GeneratorStatistics>> getGenerators() {
        return generators;
    }

    public void setProperties(Properties properties) {
        this.properties = properties;
    }

    public void setOverrideFolder(Resource overrideFolder) {
        this.overrideFolder = overrideFolder;
    }

    public NamedConfigurationDAO getNamedConfigurationDAO() {
        return namedConfigurationDAO;
    }

    public void setNamedConfigurationDAO(NamedConfigurationDAO namedConfigurationDAO) {
        this.namedConfigurationDAO = namedConfigurationDAO;
    }
}