com.eviware.soapui.plugins.PluginManager.java Source code

Java tutorial

Introduction

Here is the source code for com.eviware.soapui.plugins.PluginManager.java

Source

/*
 * SoapUI, Copyright (C) 2004-2017 SmartBear Software
 *
 * Licensed under the EUPL, Version 1.1 or - as soon as they will be approved by the European Commission - subsequent 
 * versions of the EUPL (the "Licence"); 
 * You may not use this work except in compliance with the Licence. 
 * You may obtain a copy of the Licence at: 
 * 
 * http://ec.europa.eu/idabc/eupl 
 * 
 * Unless required by applicable law or agreed to in writing, software distributed under the Licence is 
 * distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 
 * express or implied. See the Licence for the specific language governing permissions and limitations 
 * under the Licence. 
 */

package com.eviware.soapui.plugins;

import com.eviware.soapui.SoapUI;
import com.eviware.soapui.support.UISupport;
import com.eviware.soapui.support.action.SoapUIActionRegistry;
import com.eviware.soapui.support.factory.SoapUIFactoryRegistry;
import com.eviware.soapui.support.listener.ListenerRegistry;
import org.apache.commons.io.FileUtils;
import org.apache.log4j.Logger;

import java.io.File;
import java.io.FileFilter;
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.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveTask;

public class PluginManager {

    FileOperations fileOperations = new DefaultFileOperations();
    PluginLoader pluginLoader;

    private static Logger log = Logger.getLogger(PluginManager.class);
    private Map<File, InstalledPluginRecord> installedPlugins = new HashMap<File, InstalledPluginRecord>();
    private File pluginDirectory;
    private List<PluginListener> listeners = new ArrayList<PluginListener>();
    private final File pluginDeleteListFile;
    private PluginDependencyResolver resolver;

    private static ForkJoinPool forkJoinPool = new ForkJoinPool(Runtime.getRuntime().availableProcessors(),
            ForkJoinPool.defaultForkJoinWorkerThreadFactory, new Thread.UncaughtExceptionHandler() {
                @Override
                public void uncaughtException(Thread t, Throwable e) {
                    System.err.println("Problem running task in the forkJoinPool");
                    e.printStackTrace();
                }
            }, false);

    public PluginManager(SoapUIFactoryRegistry factoryRegistry, SoapUIActionRegistry actionRegistry,
            ListenerRegistry listenerRegistry) {
        pluginLoader = new PluginLoader(factoryRegistry, actionRegistry, listenerRegistry);
        File soapUiDirectory = new File(System.getProperty("user.home"), ".soapuios");
        pluginDirectory = new File(soapUiDirectory, "plugins");
        if (!pluginDirectory.exists() && !pluginDirectory.mkdirs()) {
            log.error("Couldn't create plugin directory in location " + pluginDirectory.getAbsolutePath());
        }
        pluginDeleteListFile = new File(pluginDirectory, "delete_files.txt");
        if (pluginDeleteListFile.exists()) {
            deleteOldPluginFiles();
        }
    }

    public PluginLoader getPluginLoader() {
        return pluginLoader;
    }

    public static ForkJoinPool getForkJoinPool() {
        return forkJoinPool;
    }

    public void loadPlugins() {
        File[] pluginFiles = pluginDirectory.listFiles(new FileFilter() {
            @Override
            public boolean accept(File pathname) {
                return pathname.isFile() && (pathname.getName().toLowerCase().endsWith(".jar")
                        || pathname.getName().toLowerCase().endsWith(".zip"));
            }
        });
        if (pluginFiles != null) {
            List<File> pluginFileList = new ArrayList<>();
            ProductBodyguard productBodyguard = new ProductBodyguard();
            for (File f : pluginFiles) {
                if (!productBodyguard.isKnown(f)) {
                    SoapUI.log.warn("Plugin '" + f.getName()
                            + "' is not loaded because it hasn't been signed by SmartBear Software.");
                } else {
                    pluginFileList.add(f);
                }
            }

            resolver = null;
            try {
                resolver = new PluginDependencyResolver(pluginLoader, pluginFileList);
                pluginFileList = resolver.determineLoadOrder();
            } catch (Exception e) {
                log.error("Couldn't resolve plugin dependency order. This may impair plugin functionality.", e);
            }
            long startTime = System.currentTimeMillis();

            getForkJoinPool().invoke(new LoadPluginsTask(pluginFileList));
            long timeTaken = System.currentTimeMillis() - startTime;
            log.info(pluginFileList.size() + " plugins loaded in " + timeTaken + " ms");
        }
    }

    private Collection<JarClassLoader> findDependentClassLoaders(File pluginFile) throws IOException {
        if (resolver == null) {
            return Collections.emptySet();
        }
        Collection<PluginInfo> allDependencies = resolver.findAllDependencies(pluginFile);
        Set<JarClassLoader> classLoaders = new HashSet<JarClassLoader>();
        for (PluginInfo dependency : allDependencies) {
            for (InstalledPluginRecord installedPluginRecord : installedPlugins.values()) {
                if (installedPluginRecord.plugin.getInfo().isCompatibleWith(dependency)) {
                    classLoaders.add(installedPluginRecord.pluginClassLoader);
                    break;
                }
            }
        }
        return classLoaders;
    }

    private Plugin doInstallPlugin(File pluginFile, Collection<JarClassLoader> classLoaders) throws IOException {
        // add jar to resource classloader so embedded images can be found with UISupport.loadImageIcon(..)
        UISupport.addResourceClassLoader(new URLClassLoader(new URL[] { pluginFile.toURI().toURL() }));

        InstalledPluginRecord context = pluginLoader.loadPlugin(pluginFile, classLoaders);
        installedPlugins.put(pluginFile, context);
        for (PluginListener listener : listeners) {
            listener.pluginLoaded(context.plugin);
        }

        return context.plugin;
    }

    public Plugin installPlugin(File pluginFile) throws IOException {
        PluginInfo pluginInfo = pluginLoader.loadPluginInfoFrom(pluginFile, Collections.<JarClassLoader>emptySet());
        if (findInstalledVersionOf(pluginInfo) != null && !overwriteConfirmed(pluginInfo)) {
            return null;
        }
        File destinationFile = new File(pluginDirectory, pluginFile.getName());
        if (destinationFile.exists()) {
            destinationFile = createNonExistingFileName(destinationFile);
        }
        fileOperations.copyFile(pluginFile, destinationFile);
        resolver.addPlugin(pluginInfo, destinationFile);
        if (uninstallPlugin(pluginInfo, true)) {
            return doInstallPlugin(destinationFile, findDependentClassLoaders(destinationFile));
        } else {
            return null;
        }
    }

    private File createNonExistingFileName(File fileToWrite) {
        String originalFileName = fileToWrite.getName();
        String newFileName = null;
        if (originalFileName.matches(".+\\-\\d+\\.jar")) {
            while (newFileName == null || new File(pluginDirectory, newFileName).exists()) {
                int lastDashIndex = originalFileName.lastIndexOf('-');
                int fileCountIndex = Integer
                        .parseInt(originalFileName.substring(lastDashIndex + 1, originalFileName.length() - 4));
                newFileName = originalFileName.substring(0, lastDashIndex + 1) + (fileCountIndex + 1) + ".jar";
            }
        } else {
            newFileName = originalFileName.substring(0, originalFileName.length() - 4) + "-2.jar";
        }
        return new File(pluginDirectory, newFileName);
    }

    private boolean overwriteConfirmed(PluginInfo pluginInfo) {
        PluginInfo installedPluginInfo = findInstalledVersionOf(pluginInfo).getInfo();
        return UISupport
                .confirm("You currently have version " + installedPluginInfo.getVersion() + " of the plugin "
                        + pluginInfo.getId().getName() + " installed.\nDo you want to overwrite it with version "
                        + pluginInfo.getVersion() + " of the same plugin?", "Overwrite plugin");
    }

    private Plugin findInstalledVersionOf(PluginInfo pluginInfo) {
        for (InstalledPluginRecord installedPlugin : installedPlugins.values()) {
            if (installedPlugin.plugin.getInfo().getId().equals(pluginInfo.getId())) {
                return installedPlugin.plugin;
            }
        }
        return null;
    }

    public boolean uninstallPlugin(Plugin plugin) throws IOException {
        return uninstallPlugin(plugin.getInfo(), false);
    }

    public boolean uninstallPlugin(PluginInfo pluginInfo, boolean silent) throws IOException {
        for (File installedPluginFile : installedPlugins.keySet()) {
            Plugin installedPlugin = installedPlugins.get(installedPluginFile).plugin;
            if (installedPlugin.getInfo().getId().equals(pluginInfo.getId())) {
                if (!fileOperations.deleteFile(installedPluginFile)) {
                    log.warn("Couldn't delete old plugin file " + installedPluginFile + " - aborting uninstall");
                    return false;
                }
                String uninstallMessage = "Plugin uninstalled - you should restart SoapUI to ensure that the changes to take effect";
                if (installedPlugin instanceof UninstallablePlugin) {
                    try {
                        boolean uninstalled = ((UninstallablePlugin) installedPlugin).uninstall();

                        if (uninstalled) {
                            uninstallMessage = "Plugin uninstalled successfully";
                        }
                    } catch (Exception e) {
                        if (silent) {
                            log.error("Error while uninstalling plugin", e);
                        } else {
                            UISupport.showErrorMessage(
                                    "The plugin file has been deleted but could not be uninstalled - "
                                            + "restart SoapUI for the changes to take effect");
                        }
                        return false;
                    }
                }

                try {
                    pluginLoader.unloadPlugin(installedPlugin);
                    for (PluginListener listener : listeners) {
                        listener.pluginUnloaded(installedPlugin);
                    }
                } catch (Exception e) {
                    uninstallMessage = "Plugin unloaded unsuccessfully - please restart";
                    log.error("Couldn't unload plugin", e);
                }

                installedPlugins.remove(installedPluginFile);
                resolver.removePlugin(pluginInfo);
                if (!silent) {
                    UISupport.showInfoMessage(uninstallMessage);
                }
                break;
            }
        }

        return true;
    }

    public Collection<Plugin> getInstalledPlugins() {
        Set<Plugin> plugins = new HashSet<Plugin>();
        for (InstalledPluginRecord installedPluginRecord : installedPlugins.values()) {
            plugins.add(installedPluginRecord.plugin);
        }
        return Collections.unmodifiableCollection(plugins);
    }

    public void addPluginListener(PluginListener listener) {
        listeners.add(listener);
    }

    public void removePluginListener(PluginListener listener) {
        listeners.add(listener);
    }

    /* Helper methods */

    private void deleteOldPluginFiles() {
        try {
            List<String> filesToDelete = FileUtils.readLines(pluginDeleteListFile);
            for (String fileName : filesToDelete) {
                File oldPluginFile = new File(pluginDirectory, fileName.trim());
                if (oldPluginFile.exists()) {
                    if (!oldPluginFile.delete()) {
                        log.warn("Couldn't delete old plugin file " + fileName + " on startup");
                    }
                } else {
                    log.info("Old plugin file not found: " + fileName);
                }
            }
        } catch (IOException e) {
            log.error("Couldn't read list of old plugin files to delete from file "
                    + pluginDeleteListFile.getAbsolutePath());
        } finally {
            if (!pluginDeleteListFile.delete()) {
                log.warn("Couldn't remove file with list of old plugin files to delete");
            }
        }
    }

    private List<PluginInfo> findUnsatisfiedDependencies(PluginInfo pluginInfo) {
        List<PluginInfo> unsatisfiedDependencies = new ArrayList<PluginInfo>();
        for (PluginInfo dependency : pluginInfo.getDependencies()) {
            if (!dependencyInstalled(dependency)) {
                unsatisfiedDependencies.add(dependency);
            }
        }
        return unsatisfiedDependencies;
    }

    private boolean dependencyInstalled(PluginInfo dependency) {
        for (Plugin plugin : getInstalledPlugins()) {
            PluginId dependencyId = dependency.getId();
            Version minimumVersion = dependency.getVersion();
            if (plugin.getInfo().getId().equals(dependencyId)
                    && plugin.getInfo().getVersion().compareTo(minimumVersion) != -1) {
                return true;
            }
        }
        return false;
    }

    public void installPlugins(List<File> pluginFilesToInstall) throws IOException {
        PluginDependencyResolver downloadsResolver = new PluginDependencyResolver(pluginLoader,
                pluginFilesToInstall);
        for (File file : downloadsResolver.determineLoadOrder()) {
            installPlugin(file);
        }
    }

    public Collection<Plugin> getDependentPlugins(Plugin selectedPlugin) {
        Set<Plugin> dependentPlugins = new HashSet<Plugin>();
        for (InstalledPluginRecord installedPluginRecord : installedPlugins.values()) {
            Collection<PluginInfo> allDependencies = resolver
                    .findAllDependencies(installedPluginRecord.plugin.getInfo());
            for (PluginInfo dependency : allDependencies) {
                if (dependency.isCompatibleWith(selectedPlugin.getInfo())) {
                    dependentPlugins.add(installedPluginRecord.plugin);
                    break;
                }
            }
        }
        return dependentPlugins;
    }

    private class DefaultFileOperations implements FileOperations {

        @Override
        public void copyFile(File sourceFile, File destinationFile) throws IOException {
            FileUtils.copyFile(sourceFile, destinationFile);
        }

        @Override
        public boolean deleteFile(File fileToDelete) throws IOException {
            if (!fileToDelete.delete()) {
                try {
                    FileUtils.write(pluginDeleteListFile, fileToDelete.getName() + "\r\n", true);
                } catch (IOException e) {
                    log.error("Couldn't schedule plugin file " + fileToDelete.getName() + " for deletion", e);
                    return false;
                }
            }
            return true;
        }

    }

    static interface FileOperations {

        void copyFile(File sourceFile, File destinationFile) throws IOException;

        boolean deleteFile(File fileToDelete) throws IOException;
    }

    private class LoadPluginsTask extends RecursiveTask<List<Plugin>> {

        private List<File> files;

        private LoadPluginsTask(Collection<File> files) {
            this.files = new ArrayList<File>(files);
        }

        @Override
        protected List<Plugin> compute() {
            int splitPoint = findSplitPoint(files.size() / 2);
            if (splitPoint == 0 || splitPoint == files.size() - 1) {
                return computeSequentially();
            } else {
                LoadPluginsTask leftTask = new LoadPluginsTask(files.subList(0, splitPoint));
                leftTask.fork();
                LoadPluginsTask rightTask = new LoadPluginsTask(files.subList(splitPoint, files.size()));
                List<Plugin> rightTaskResult = rightTask.compute();
                List<Plugin> leftTaskResult = leftTask.join();
                List<Plugin> result = new ArrayList<Plugin>();
                result.addAll(leftTaskResult);
                result.addAll(rightTaskResult);
                return result;
            }
        }

        private int findSplitPoint(int tentativeSplitPoint) {
            if (tentativeSplitPoint <= 0) {
                return 0;
            } else if (tentativeSplitPoint >= files.size() - 1) {
                return files.size() - 1;
            }
            List<PluginInfo> pluginInfoList = resolver.getPluginInfoListFromFiles(files);
            if (pluginInfoList.get(tentativeSplitPoint + 1).getDependencies().isEmpty()) {
                return tentativeSplitPoint;
            } else {
                int leftSplitPoint = findSplitPoint(tentativeSplitPoint - 1);
                int rightSplitPoint = findSplitPoint(tentativeSplitPoint + 1);
                if (leftSplitPoint > 0
                        && (tentativeSplitPoint - leftSplitPoint <= rightSplitPoint - tentativeSplitPoint)) {
                    return leftSplitPoint;
                } else if (rightSplitPoint < files.size() - 1) {
                    return rightSplitPoint;
                } else {
                    return 0;
                }
            }
        }

        private List<Plugin> computeSequentially() {
            List<Plugin> result = new ArrayList<Plugin>();
            for (File pluginFile : files) {
                try {
                    log.info("Adding plugin from [" + pluginFile.getAbsolutePath() + "]");
                    try {
                        Plugin plugin = doInstallPlugin(pluginFile, findDependentClassLoaders(pluginFile));
                        result.add(plugin);
                    } catch (MissingPluginClassException e) {
                        log.error("No plugin found in [" + pluginFile + "]");
                    } catch (Exception e) {
                        log.warn("Could not load plugin from file [" + pluginFile + "]", e);
                    }
                } catch (Throwable e) {
                    log.error("Failed to load module [" + pluginFile.getName() + "]", e);
                }
            }
            return result;
        }
    }
}