/*
* SoapUI, Copyright (C) 2004-2016 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;
}
}
}