/* * 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.Properties; /** * @author szavrel */ public class PluginRegistry implements ApplicationContextAware { 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())); } } pluginDAO.storePlugin(defaultPlugin); installPlugin(defaultPlugin.getPluginId().getUri(), defaultPlugin.getPluginId().getVersion()); } } } // 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; } 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; } }