All Downloads are FREE. Search and download functionalities are using the official Maven repository.

step.core.plugins.PluginManager Maven / Gradle / Ivy

There is a newer version: 2.2.3
Show newest version
/*******************************************************************************
 * Copyright (C) 2020, exense GmbH
 *  
 * This file is part of STEP
 *  
 * STEP is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *  
 * STEP 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 Affero General Public License for more details.
 *  
 * You should have received a copy of the GNU Affero General Public License
 * along with STEP.  If not, see .
 ******************************************************************************/
package step.core.plugins;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import step.core.plugins.exceptions.PluginCriticalException;
import step.core.scanner.CachedAnnotationScanner;

public class PluginManager {
	
	private static Logger logger = LoggerFactory.getLogger(PluginManager.class);
	
	private final Class pluginClass;
	private final List plugins;
	
	private PluginManager(Class pluginClass, List plugins) {
		super();
		this.pluginClass = pluginClass;
		this.plugins = plugins;
		
		logger.info("Starting plugin manager with following plugins: "+Arrays.toString(plugins.toArray()));
	}
	
	public T getProxy() {
		return getProxy(pluginClass);
	}

	public  I getProxy(Class proxyInterface) {
		@SuppressWarnings("unchecked")
		I proxy = (I) Proxy.newProxyInstance(
				proxyInterface.getClassLoader(),
				new Class[] { proxyInterface }, new InvocationHandler() {
					
					@Override
					public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
						for(T plugin:plugins) {
							try {
								method.invoke(plugin, args);
							} catch (IllegalArgumentException e) {
								// Ignore
							} catch (Throwable e) {
								if (e instanceof InvocationTargetException && ((InvocationTargetException) e).getTargetException() instanceof PluginCriticalException) {
									throw ((InvocationTargetException) e).getTargetException();
								} else {
									logger.error("Error invoking method #" + method.getName() + " of plugin '" + plugin.getClass().getName() + "'" + "(" + e.toString() + ")", e);
								}
							} 
						}
						return null;
					}
				});
		return proxy;
	}
	
	public List getPlugins() {
		return plugins;
	}

	public static  Builder builder(Class pluginClass) {
		return new Builder(pluginClass);
	}
	
	public static class Builder {
		
		private final Class pluginClass;
		protected List plugins = new ArrayList<>();
		private Predicate pluginsFilter = null;
		
		public Builder(Class pluginClass) {
			super();
			this.pluginClass = pluginClass;
		}
		
		public Builder withPluginFilter(Predicate pluginsFilter) {
			this.pluginsFilter = pluginsFilter;
			return this;
		}

		public Builder withPlugin(T plugin) {
			plugins.add(plugin);
			return this;
		}
		
		public Builder withPlugins(List plugins_) {
			plugins.addAll(plugins_);
			return this;
		}
		
		public Builder withPluginsFromClasspath() throws InstantiationException, IllegalAccessException, ClassNotFoundException {
			return withPluginsFromClasspath(null);
		}
		
		public Builder withPluginsFromClasspath(String packagePrefix) throws InstantiationException, IllegalAccessException, ClassNotFoundException {
			List pluginsFromClassLoader = getPluginsFromClassLoader(packagePrefix);
			plugins.addAll(pluginsFromClassLoader);
			return this;
		}
		
		private List getPluginsFromClassLoader(String packagePrefix) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
			ClassLoader cl = Thread.currentThread().getContextClassLoader();
			Set> classesWithAnnotation = CachedAnnotationScanner.getClassesWithAnnotation(packagePrefix, Plugin.class, cl);
			
			List pluginClasses = new ArrayList<>();
			List plugins = new ArrayList<>();
			for (Class clazz : classesWithAnnotation) {
				if(pluginClass.isAssignableFrom(clazz)) {
					String className = clazz.getName();
					if(clazz.getAnnotation(IgnoreDuringAutoDiscovery.class) == null) {
						pluginClasses.add(className);
						@SuppressWarnings("unchecked")
						T plugin = newPluginInstance((Class) clazz);
						plugins.add(plugin);
					} else {
						logger.debug("Ignoring plugin "+className+" annotated by "+IgnoreDuringAutoDiscovery.class.getName());
					}
				}
			}
			
			return plugins;
		}
		
		private T newPluginInstance(Class _class) throws InstantiationException, IllegalAccessException  {
			T plugin = _class.newInstance();
			return (T) plugin;
		}
		
		/**
		 * Sort the plugins according to their mutual dependencies.
		 * The plugin with the highest dependency to other plugins will be located at the end of the list.
		 * 
		 * @param plugins the unsorted list of plugins
		 * @return the sorted list of plugins
		 * @throws CircularDependencyException if a circular dependency is detected
		 */
		private List sortPluginsByDependencies(List plugins) throws CircularDependencyException {
			// Create a list of additional dependencies based on the attribute "runsBefore"
			// The attribute "runsBefore" specifies a list of plugins before which a specific plugin should be executed.
			// Specifying that "A has to be run before B" has the same meaning as "B is depending on A"
			Map, List>> additionalDependencies = new HashMap<>();
			for (T plugin : plugins) {
				Class pluginClass = plugin.getClass();
				Plugin annotation = pluginClass.getAnnotation(Plugin.class);
				if(annotation != null) {
					Class[] runsBeforeList = annotation.runsBefore();
					for (Class runsBefore : runsBeforeList) {
						// 
						additionalDependencies.computeIfAbsent(runsBefore, c->new ArrayList<>()).add(pluginClass);
					}
				}
			}
			
			List result = new ArrayList<>(plugins);
			
			int iterationCount = 0;
			
			boolean hasModification = true;
			// loop as long as modifications to the ordering of the list are performed
			while(hasModification) {
				if(iterationCount>1000) {
					throw new CircularDependencyException("Circular dependency in the plugin dependencies");
				}
				
				hasModification = false;
				List clone = new ArrayList<>(result);
				for (T plugin : result) {
					Class pluginClass = plugin.getClass();
					Plugin annotation = pluginClass.getAnnotation(Plugin.class);
					
					final List> allDependencies = new ArrayList<>();

					if(annotation != null) {
						Class[] dependencies = annotation.dependencies();
						allDependencies.addAll(Arrays.asList(dependencies));
					}
					
					if(additionalDependencies.containsKey(pluginClass)) {
						allDependencies.addAll(additionalDependencies.get(pluginClass));
					}
					
					int initialPosition = clone.indexOf(plugin);
					int newPosition = -1;
					if(allDependencies.size()>0) {
						for (Class dependency : allDependencies) {
							int positionOfDependencyInClone = IntStream.range(0, clone.size()).filter(i -> dependency.equals(clone.get(i).getClass())).findFirst().orElse(-1);
							// if the dependency is located after the current plugin  
							if(positionOfDependencyInClone>initialPosition) {
								// if this is the highest position of all dependencies of this plugin
								if(positionOfDependencyInClone>newPosition) {
									newPosition = positionOfDependencyInClone;
								}
							}
						}
					}
					if(newPosition>=0) {
						// move the plugin after the dependency with the highest position
						clone.add(newPosition+1, plugin);
						clone.remove(initialPosition);
						hasModification = true;
					}
				}
				
				result = clone;
				iterationCount++;
			}
			
			return result;
		}
		
		@SuppressWarnings("serial")
		public static class CircularDependencyException extends Exception {

			public CircularDependencyException(String message) {
				super(message);
			}
			
		}
		
		public PluginManager build() throws CircularDependencyException {
			List validPlugins = plugins.stream()
									.filter(p->!(p instanceof OptionalPlugin) || ((OptionalPlugin)p).validate())
									.filter(p->pluginsFilter == null || pluginsFilter.test(p))
									.collect(Collectors.toList());		
			List sortedPluginsByDependencies = sortPluginsByDependencies(validPlugins);
			return new PluginManager(pluginClass, sortedPluginsByDependencies);
		}
	}
}