/*
* 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.model.iface.SoapUIListener;
import com.eviware.soapui.support.action.SoapUIAction;
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 org.reflections.Reflections;
import org.reflections.util.ConfigurationBuilder;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* Responsible for loading plugins into SoapUI.
*/
public class PluginLoader extends LoaderBase {
public static Logger log = Logger.getLogger(PluginLoader.class);
public PluginLoader(SoapUIFactoryRegistry factoryRegistry,
SoapUIActionRegistry actionRegistry, ListenerRegistry listenerRegistry) {
super(listenerRegistry, actionRegistry, factoryRegistry);
}
InstalledPluginRecord loadPlugin(File pluginFile, Collection<JarClassLoader> dependencyClassLoaders) throws IOException {
ReflectionsAndClassLoader tuple = makeJarFileScanner(pluginFile, dependencyClassLoaders);
Class<?> pluginClass = readPluginConfigurationClasses(pluginFile, tuple.reflections);
Plugin plugin = loadPlugin(pluginClass, tuple.reflections);
return new InstalledPluginRecord(plugin, tuple.jarClassLoader);
}
private ReflectionsAndClassLoader makeJarFileScanner(File pluginFile, Collection<JarClassLoader> dependencyClassLoaders) throws IOException {
File tempFile = File.createTempFile("soapuios", ".jar");
tempFile.deleteOnExit();
FileUtils.copyFile(pluginFile, tempFile);
JarClassLoader jarClassLoader = new JarClassLoader(tempFile, PluginLoader.class.getClassLoader(), dependencyClassLoaders);
ConfigurationBuilder configurationBuilder = new ConfigurationBuilder().setUrls(jarClassLoader.getURLs()).addClassLoader(jarClassLoader);
if (jarClassLoader.hasScripts()) {
configurationBuilder.addClassLoader(jarClassLoader.getScriptClassLoader());
configurationBuilder.addScanners(new TypeAnnotationsScanner());
configurationBuilder.setMetadataAdapter(new GroovyAndJavaReflectionAdapter(jarClassLoader));
}
return new ReflectionsAndClassLoader(new Reflections(configurationBuilder), jarClassLoader);
}
private Class<?> readPluginConfigurationClasses(File pluginFile, Reflections jarFileScanner) {
Set<Class<?>> pluginClasses = jarFileScanner.getTypesAnnotatedWith(PluginConfiguration.class);
if (pluginClasses.isEmpty()) {
log.warn("No plugin classes found in JAR file " + pluginFile);
throw new MissingPluginClassException("No plugin class found in " + pluginFile);
} else if (pluginClasses.size() > 1) {
throw new InvalidPluginException("Multiple plugin classes found in " + pluginFile + ": " + pluginClasses);
}
return pluginClasses.iterator().next();
}
Plugin loadPlugin(Class<?> pluginClass, Reflections jarFileScanner) {
try {
PluginConfiguration configurationAnnotation = pluginClass.getAnnotation(PluginConfiguration.class);
Version minimumReadyApiVersion = Version.fromString(configurationAnnotation.minimumReadyApiVersion());
Version installedReadyApiVersion = Version.fromString(SoapUI.SOAPUI_VERSION);
if (minimumReadyApiVersion.compareTo(installedReadyApiVersion) > 0) {
throw new InvalidPluginException("Plugin " + configurationAnnotation.name() + " requires version " +
minimumReadyApiVersion + " of Ready!API. Current application version: " + installedReadyApiVersion);
}
Plugin plugin;
if (Plugin.class.isAssignableFrom(pluginClass)) {
plugin = (Plugin) pluginClass.newInstance();
} else {
plugin = new EmptyPlugin(configurationAnnotation);
}
boolean autoDetect = configurationAnnotation.autoDetect();
if (plugin.isActive()) {
plugin.initialize();
Collection<SoapUIFactory> factories = loadPluginFactories(plugin, autoDetect, jarFileScanner);
List<SoapUIAction> actions = loadPluginActions(plugin, autoDetect, jarFileScanner);
List<Class<? extends SoapUIListener>> listeners = loadPluginListeners(plugin, autoDetect, jarFileScanner);
return createLoadedPluginInstance(plugin, factories, actions, listeners);
}
return plugin;
} catch (InvalidPluginException e) {
throw e;
} catch (Throwable e) {
throw new InvalidPluginException("Error loading plugin " + pluginClass, e);
}
}
private LoadedPlugin createLoadedPluginInstance(Plugin plugin, Collection<SoapUIFactory> factories, List<SoapUIAction> actions,
List<Class<? extends SoapUIListener>> listeners) {
LoadedPlugin loadedPlugin = new LoadedPlugin(plugin, factories, actions, listeners);
for (SoapUIFactory factory : factories) {
if (factory instanceof PluginAware) {
((PluginAware)factory).setPlugin(loadedPlugin);
}
}
for (SoapUIAction action : actions) {
if (action instanceof PluginAware) {
((PluginAware)action).setPlugin(loadedPlugin);
}
}
return loadedPlugin;
}
private Collection<SoapUIFactory> loadPluginFactories(Plugin plugin, boolean autoDetect, Reflections jarFileScanner)
throws IllegalAccessException, InstantiationException {
Collection<SoapUIFactory> factories = new HashSet<SoapUIFactory>(plugin.getFactories());
if (!factories.isEmpty())
registerFactories(factories);
if (autoDetect) {
factories.addAll(loadFactories(jarFileScanner));
}
return factories;
}
private List<Class<? extends SoapUIListener>> loadPluginListeners(Plugin plugin, boolean autoDetect, Reflections jarFileScanner) throws IllegalAccessException, InstantiationException {
List<Class<? extends SoapUIListener>> listeners = new ArrayList<Class<? extends SoapUIListener>>(plugin.getListeners());
if (!listeners.isEmpty())
registerListeners(listeners);
if (autoDetect) {
listeners.addAll(loadListeners(jarFileScanner));
}
return listeners;
}
private List<SoapUIAction> loadPluginActions(Plugin plugin, boolean autoDetect, Reflections jarFileScanner)
throws InstantiationException, IllegalAccessException {
List<SoapUIAction> actions = new ArrayList<SoapUIAction>(plugin.getActions());
if (!actions.isEmpty())
registerActions(actions);
if (autoDetect) {
actions.addAll(loadActions(jarFileScanner));
}
return actions;
}
public void unloadPlugin(Plugin plugin) {
unregisterActions(plugin.getActions());
unregisterListeners(plugin.getListeners());
unregisterFactories(plugin.getFactories());
}
public PluginInfo loadPluginInfoFrom(File pluginFile, Collection<JarClassLoader> dependencyClassLoaders) throws IOException {
ReflectionsAndClassLoader tuple = makeJarFileScanner(pluginFile, dependencyClassLoaders);
Class<?> pluginClass = readPluginConfigurationClasses(pluginFile, tuple.reflections);
return readPluginInfoFrom(pluginClass);
}
static PluginInfo readPluginInfoFrom(Class<?> pluginClass) {
PluginInfo pluginInfo = readPluginInfoFromAnnotation(pluginClass.getAnnotation(PluginConfiguration.class));
addDependency(pluginInfo, pluginClass.getAnnotation(PluginDependency.class));
PluginDependencies pluginDependenciesAnnotation = pluginClass.getAnnotation(PluginDependencies.class);
if (pluginDependenciesAnnotation != null) {
for (PluginDependency pluginDependency : pluginDependenciesAnnotation.value()) {
addDependency(pluginInfo, pluginDependency);
}
}
return pluginInfo;
}
private static void addDependency(PluginInfo pluginInfo, PluginDependency dependencyAnnotation) {
if (dependencyAnnotation != null) {
PluginId id = new PluginId(dependencyAnnotation.groupId(), dependencyAnnotation.name());
pluginInfo.addDependency(new PluginInfo(id, Version.fromString(dependencyAnnotation.minimumVersion()), "", ""));
}
}
static PluginInfo readPluginInfoFromAnnotation(PluginConfiguration annotation) {
PluginId id = new PluginId(annotation.groupId(), annotation.name());
Version version = Version.fromString(annotation.version());
String infoUrl = annotation.infoUrl();
return new PluginInfo(id, version, annotation.description(), infoUrl);
}
// due to Reflections internals (or my misunderstanding of them) this class has to be
// named as its superclass
private static class TypeAnnotationsScanner extends org.reflections.scanners.TypeAnnotationsScanner {
@Override
public boolean acceptsInput(String file) {
if (file.endsWith(".groovy")) {
return true;
} else {
return super.acceptsInput(file);
}
}
}
private class LoadedPlugin implements Plugin{
private final Plugin plugin;
private final Collection<SoapUIFactory> factories;
private final List<SoapUIAction> actions;
private final List<Class<? extends SoapUIListener>> listeners;
public LoadedPlugin(Plugin plugin, Collection<SoapUIFactory> factories, List<SoapUIAction> actions,
List<Class<? extends SoapUIListener>> listeners) {
this.plugin = plugin;
this.factories = factories;
this.actions = actions;
this.listeners = listeners;
}
@Override
public PluginInfo getInfo() {
return plugin.getInfo();
}
@Override
public boolean isActive() {
return plugin.isActive();
}
@Override
public void initialize() {
throw new IllegalStateException("Plugin has already been initialized");
}
@Override
public List<Class<? extends SoapUIListener>> getListeners() {
return listeners;
}
@Override
public List<? extends SoapUIAction> getActions() {
return actions;
}
@Override
public Collection<? extends ApiImporter> getApiImporters() {
return Collections.emptySet();
}
@Override
public Collection<? extends SoapUIFactory> getFactories() {
return factories;
}
@Override
public boolean hasSameIdAs(Plugin otherPlugin) {
return plugin.hasSameIdAs(otherPlugin);
}
@Override
public String toString() {
return plugin.toString();
}
}
private class EmptyPlugin implements Plugin {
private PluginInfo pluginInfo;
private EmptyPlugin(PluginConfiguration annotation) {
pluginInfo = readPluginInfoFromAnnotation(annotation);
}
@Override
public PluginInfo getInfo() {
return pluginInfo;
}
@Override
public boolean isActive() {
return true;
}
@Override
public void initialize() {
}
@Override
public List<Class<? extends SoapUIListener>> getListeners() {
return Collections.emptyList();
}
@Override
public List<? extends SoapUIAction> getActions() {
return Collections.emptyList();
}
@Override
public Collection<? extends ApiImporter> getApiImporters() {
return Collections.emptySet();
}
@Override
public Collection<? extends SoapUIFactory> getFactories() {
return Collections.emptySet();
}
@Override
public boolean hasSameIdAs(Plugin otherPlugin) {
return pluginInfo.getId().equals(otherPlugin.getInfo().getId());
}
}
private class ReflectionsAndClassLoader {
Reflections reflections;
JarClassLoader jarClassLoader;
private ReflectionsAndClassLoader(Reflections reflections, JarClassLoader jarClassLoader) {
this.reflections = reflections;
this.jarClassLoader = jarClassLoader;
}
}
}