/*
* Copyright 2010-2015 Institut Pasteur.
*
* This file is part of Icy.
*
* Icy 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.
*
* Icy 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 Icy. If not, see <http://www.gnu.org/licenses/>.
*/
package icy.plugin;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EventListener;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import javax.swing.event.EventListenerList;
import icy.file.Loader;
import icy.gui.frame.progress.ProgressFrame;
import icy.main.Icy;
import icy.network.NetworkUtil;
import icy.plugin.PluginDescriptor.PluginIdent;
import icy.plugin.PluginDescriptor.PluginKernelNameSorter;
import icy.plugin.abstract_.Plugin;
import icy.plugin.classloader.JarClassLoader;
import icy.plugin.interface_.PluginBundled;
import icy.plugin.interface_.PluginDaemon;
import icy.preferences.PluginPreferences;
import icy.system.IcyExceptionHandler;
import icy.system.thread.SingleProcessor;
import icy.system.thread.ThreadUtil;
import icy.util.ClassUtil;
/**
* Plugin Loader class.<br>
* This class is used to load plugins from "plugins" package and "plugins" directory
*
* @author Stephane<br>
*/
public class PluginLoader
{
public final static String PLUGIN_PACKAGE = "plugins";
public final static String PLUGIN_KERNEL_PACKAGE = "plugins.kernel";
public final static String PLUGIN_PATH = "plugins";
/**
* static class
*/
private static final PluginLoader instance = new PluginLoader();
/**
* class loader
*/
private ClassLoader loader;
/**
* active daemons plugins
*/
private List<PluginDaemon> activeDaemons;
/**
* Loaded plugin list
*/
private List<PluginDescriptor> plugins;
/**
* listeners
*/
private final EventListenerList listeners;
/**
* JAR Class Loader disabled flag
*/
protected boolean JCLDisabled;
/**
* internals
*/
private final Runnable reloader;
final SingleProcessor processor;
private boolean initialized;
private boolean loading;
// private boolean logError;
/**
* static class
*/
private PluginLoader()
{
super();
// default class loader
loader = new PluginClassLoader();
// active daemons
activeDaemons = new ArrayList<PluginDaemon>();
JCLDisabled = false;
initialized = false;
loading = false;
// needReload = false;
// logError = true;
plugins = new ArrayList<PluginDescriptor>();
listeners = new EventListenerList();
// reloader
reloader = new Runnable()
{
@Override
public void run()
{
reloadInternal();
}
};
processor = new SingleProcessor(true, "Local Plugin Loader");
// don't load by default as we need Preferences to be ready first
};
static void prepare()
{
if (!instance.initialized)
{
if (isLoading())
waitWhileLoading();
else
reload();
}
}
/**
* Reload the list of installed plugins (asynchronous version).
*/
public static void reloadAsynch()
{
instance.processor.submit(instance.reloader);
}
/**
* Reload the list of installed plugins (wait for completion).
*/
public static void reload()
{
instance.processor.submit(instance.reloader);
// ensure we don't miss the reloading
ThreadUtil.sleep(500);
waitWhileLoading();
}
/**
* @deprecated Use {@link #reload()} instead.
*/
@SuppressWarnings("unused")
@Deprecated
public static void reload(boolean forceNow)
{
reload();
}
/**
* Stop and restart all daemons plugins.
*/
public static synchronized void resetDaemons()
{
// reset will be done later
if (isLoading())
return;
stopDaemons();
startDaemons();
}
/**
* Reload the list of installed plugins (in "plugins" directory)
*/
void reloadInternal()
{
// needReload = false;
loading = true;
// stop daemon plugins
stopDaemons();
// reset plugins and loader
final List<PluginDescriptor> newPlugins = new ArrayList<PluginDescriptor>();
final ClassLoader newLoader;
// special case where JCL is disabled
if (JCLDisabled)
newLoader = PluginLoader.class.getClassLoader();
else
{
newLoader = new PluginClassLoader();
// reload plugins directory to search path
((PluginClassLoader) newLoader).add(PLUGIN_PATH);
}
// no need to complete loading...
if (processor.hasWaitingTasks())
return;
final Set<String> classes = new HashSet<String>();
try
{
// search for plugins in "Plugins" package (needed when working from JAR archive)
ClassUtil.findClassNamesInPackage(PLUGIN_PACKAGE, true, classes);
// search for plugins in "Plugins" directory with default plugin package name
ClassUtil.findClassNamesInPath(PLUGIN_PATH, PLUGIN_PACKAGE, true, classes);
}
catch (IOException e)
{
System.err.println("Error loading plugins :");
IcyExceptionHandler.showErrorMessage(e, true);
}
for (String className : classes)
{
// we only want to load classes from 'plugins' package
if (!className.startsWith(PLUGIN_PACKAGE))
continue;
// filter incorrect named classes (Jython classes for instances)
if (className.contains("$"))
continue;
// no need to complete loading...
if (processor.hasWaitingTasks())
return;
try
{
// try to load class and check we have a Plugin class at same time
final Class<? extends Plugin> pluginClass = newLoader.loadClass(className).asSubclass(Plugin.class);
newPlugins.add(new PluginDescriptor(pluginClass));
}
catch (NoClassDefFoundError e)
{
// fatal error
System.err.println("Class '" + className + "' cannot be loaded :");
System.err.println(
"Required class '" + ClassUtil.getQualifiedNameFromPath(e.getMessage()) + "' not found.");
}
catch (OutOfMemoryError e)
{
// fatal error
IcyExceptionHandler.showErrorMessage(e, false);
System.err.println("Class '" + className + "' is discarded");
}
catch (UnsupportedClassVersionError e)
{
// java version error
System.err.println("Newer java version required for class '" + className + "' (discarded)");
}
catch (Error e)
{
// fatal error
IcyExceptionHandler.showErrorMessage(e, false);
System.err.println("Class '" + className + "' is discarded");
}
catch (ClassCastException e)
{
// ignore ClassCastException (for classes which doesn't extend Plugin)
}
catch (ClassNotFoundException e)
{
// ignore ClassNotFoundException (for no public classes)
}
catch (Exception e)
{
// fatal error
IcyExceptionHandler.showErrorMessage(e, false);
System.err.println("Class '" + className + "' is discarded");
}
}
// sort list
Collections.sort(newPlugins, PluginKernelNameSorter.instance);
// release loaded resources
if (loader instanceof JarClassLoader)
((JarClassLoader) loader).unloadAll();
loader = newLoader;
plugins = newPlugins;
loading = false;
// notify change
changed();
}
/**
* Returns the list of daemon type plugins.
*/
public static ArrayList<PluginDescriptor> getDaemonPlugins()
{
final ArrayList<PluginDescriptor> result = new ArrayList<PluginDescriptor>();
synchronized (instance.plugins)
{
for (PluginDescriptor pluginDescriptor : instance.plugins)
{
if (pluginDescriptor.isInstanceOf(PluginDaemon.class))
{
// accept class ?
if (!pluginDescriptor.isAbstract() && !pluginDescriptor.isInterface())
result.add(pluginDescriptor);
}
}
}
return result;
}
/**
* Returns the list of active daemon plugins.
*/
public static ArrayList<PluginDaemon> getActiveDaemons()
{
synchronized (instance.activeDaemons)
{
return new ArrayList<PluginDaemon>(instance.activeDaemons);
}
}
/**
* Start daemons plugins.
*/
static synchronized void startDaemons()
{
// at this point active daemons should be empty !
if (!instance.activeDaemons.isEmpty())
stopDaemons();
final List<String> inactives = PluginPreferences.getInactiveDaemons();
final List<PluginDaemon> newDaemons = new ArrayList<PluginDaemon>();
for (PluginDescriptor pluginDesc : getDaemonPlugins())
{
// not found in inactives ?
if (inactives.indexOf(pluginDesc.getClassName()) == -1)
{
try
{
final PluginDaemon plugin = (PluginDaemon) pluginDesc.getPluginClass().newInstance();
final Thread thread = new Thread(plugin, pluginDesc.getName());
thread.setName(pluginDesc.getName());
// so icy can exit even with running daemon plugin
thread.setDaemon(true);
// init daemon
plugin.init();
// start daemon
thread.start();
// register daemon plugin (so we can stop it later)
Icy.getMainInterface().registerPlugin((Plugin) plugin);
// add daemon plugin to list
newDaemons.add(plugin);
}
catch (Throwable t)
{
IcyExceptionHandler.handleException(pluginDesc, t, true);
}
}
}
instance.activeDaemons = newDaemons;
}
/**
* Stop daemons plugins.
*/
public synchronized static void stopDaemons()
{
for (PluginDaemon daemonPlug : getActiveDaemons())
{
try
{
// stop the daemon
daemonPlug.stop();
}
catch (Throwable t)
{
IcyExceptionHandler.handleException(((Plugin) daemonPlug).getDescriptor(), t, true);
}
}
// no more active daemons
instance.activeDaemons = new ArrayList<PluginDaemon>();
}
/**
* Return the loader
*/
public static ClassLoader getLoader()
{
return instance.loader;
}
/**
* Return all resources present in the Plugin class loader.
*/
public static Map<String, URL> getAllResources()
{
prepare();
synchronized (instance.loader)
{
if (instance.loader instanceof JarClassLoader)
return ((JarClassLoader) instance.loader).getResources();
}
return new HashMap<String, URL>();
}
/**
* Return content of all loaded resources.
*/
public static Map<String, byte[]> getLoadedResources()
{
prepare();
synchronized (instance.loader)
{
if (instance.loader instanceof JarClassLoader)
return ((JarClassLoader) instance.loader).getLoadedResources();
}
return new HashMap<String, byte[]>();
}
/**
* Return all loaded classes.
*/
@SuppressWarnings("rawtypes")
public static Map<String, Class<?>> getLoadedClasses()
{
prepare();
synchronized (instance.loader)
{
if (instance.loader instanceof JarClassLoader)
{
final HashMap<String, Class<?>> result = new HashMap<String, Class<?>>();
final Map<String, Class> classes = ((JarClassLoader) instance.loader).getLoadedClasses();
for (Entry<String, Class> entry : classes.entrySet())
result.put(entry.getKey(), entry.getValue());
return result;
}
}
return new HashMap<String, Class<?>>();
}
/**
* Return all classes.
*
* @deprecated Use {@link #getLoadedClasses()} instead as we load classes on demand.
*/
@Deprecated
public static Map<String, Class<?>> getAllClasses()
{
return getLoadedClasses();
}
/**
* Return a resource as data stream from given resource name
*
* @param name
* resource name
*/
public static InputStream getResourceAsStream(String name)
{
prepare();
synchronized (instance.loader)
{
return instance.loader.getResourceAsStream(name);
}
}
/**
* Return the list of loaded plugins.
*/
public static ArrayList<PluginDescriptor> getPlugins()
{
return getPlugins(true);
}
/**
* Return the list of loaded plugins.
*
* @param wantBundled
* specify if we also want plugin implementing the {@link PluginBundled} interface.
*/
public static ArrayList<PluginDescriptor> getPlugins(boolean wantBundled)
{
prepare();
final ArrayList<PluginDescriptor> result = new ArrayList<PluginDescriptor>();
// better to return a copy as we have async list loading
synchronized (instance.plugins)
{
for (PluginDescriptor plugin : instance.plugins)
{
if (wantBundled || (!plugin.isBundled()))
result.add(plugin);
}
}
return result;
}
/**
* Return the list of loaded plugins which derive from the specified class.
*
* @param clazz
* The class object defining the class we want plugin derive from.
*/
public static ArrayList<PluginDescriptor> getPlugins(Class<?> clazz)
{
return getPlugins(clazz, true, false, false);
}
/**
* Return the list of loaded plugins which derive from the specified class.
*
* @param clazz
* The class object defining the class we want plugin derive from.
* @param wantBundled
* specify if we also want plugin implementing the {@link PluginBundled} interface
* @param wantAbstract
* specify if we also want abstract classes
* @param wantInterface
* specify if we also want interfaces
*/
public static ArrayList<PluginDescriptor> getPlugins(Class<?> clazz, boolean wantBundled, boolean wantAbstract,
boolean wantInterface)
{
prepare();
final ArrayList<PluginDescriptor> result = new ArrayList<PluginDescriptor>();
if (clazz != null)
{
synchronized (instance.plugins)
{
for (PluginDescriptor pluginDescriptor : instance.plugins)
{
if (pluginDescriptor.isInstanceOf(clazz))
{
// accept class ?
if ((wantAbstract || !pluginDescriptor.isAbstract())
&& (wantInterface || !pluginDescriptor.isInterface())
&& (wantBundled || !pluginDescriptor.isBundled()))
result.add(pluginDescriptor);
}
}
}
}
return result;
}
/**
* Return the list of "actionable" plugins (mean we can launch them from GUI).
*
* @param wantBundled
* specify if we also want plugin implementing the {@link PluginBundled} interface
*/
public static ArrayList<PluginDescriptor> getActionablePlugins(boolean wantBundled)
{
prepare();
final ArrayList<PluginDescriptor> result = new ArrayList<PluginDescriptor>();
synchronized (instance.plugins)
{
for (PluginDescriptor pluginDescriptor : instance.plugins)
{
if (pluginDescriptor.isActionable() && (wantBundled || !pluginDescriptor.isBundled()))
result.add(pluginDescriptor);
}
}
return result;
}
/**
* Return the list of "actionable" plugins (mean we can launch them from GUI).<br>
* By default plugin implementing the {@link PluginBundled} interface are not returned.
*/
public static ArrayList<PluginDescriptor> getActionablePlugins()
{
return getActionablePlugins(false);
}
/**
* @return the loading
*/
public static boolean isLoading()
{
return instance.processor.hasWaitingTasks() || instance.loading;
}
/**
* wait until loading completed
*/
public static void waitWhileLoading()
{
while (isLoading())
ThreadUtil.sleep(100);
}
/**
* Returns <code>true</code> if the specified plugin exists in the {@link PluginLoader}.
*
* @param plugin
* the plugin we are looking for.
* @param acceptNewer
* allow newer version of the plugin
*/
public static boolean isLoaded(PluginDescriptor plugin, boolean acceptNewer)
{
return (getPlugin(plugin.getIdent(), acceptNewer) != null);
}
/**
* Returns <code>true</code> if the specified plugin class exists in the {@link PluginLoader}.
*
* @param className
* class name of the plugin we are looking for.
*/
public static boolean isLoaded(String className)
{
return (getPlugin(className) != null);
}
/**
* Returns the plugin corresponding to the specified plugin identity structure.<br>
* Returns <code>null</code> if the plugin does not exists in the {@link PluginLoader}.
*
* @param ident
* plugin identity
* @param acceptNewer
* allow newer version of the plugin
*/
public static PluginDescriptor getPlugin(PluginIdent ident, boolean acceptNewer)
{
prepare();
synchronized (instance.plugins)
{
return PluginDescriptor.getPlugin(instance.plugins, ident, acceptNewer);
}
}
/**
* Returns the plugin corresponding to the specified plugin class name.<br>
* Returns <code>null</code> if the plugin does not exists in the {@link PluginLoader}.
*
* @param className
* class name of the plugin we are looking for.
*/
public static PluginDescriptor getPlugin(String className)
{
prepare();
synchronized (instance.plugins)
{
return PluginDescriptor.getPlugin(instance.plugins, className);
}
}
/**
* Returns the plugin class corresponding to the specified plugin class name.<br>
* Returns <code>null</code> if the plugin does not exists in the {@link PluginLoader}.
*
* @param className
* class name of the plugin we are looking for.
*/
public static Class<? extends Plugin> getPluginClass(String className)
{
prepare();
final PluginDescriptor descriptor = getPlugin(className);
if (descriptor != null)
return descriptor.getPluginClass();
return null;
}
/**
* Try to load and returns the specified class from the {@link PluginLoader}.<br>
* This method is equivalent to call {@link #getLoader()} then call
* <code>loadClass(String)</code> method from it.
*
* @param className
* class name of the class we want to load.
*/
public static Class<?> loadClass(String className) throws ClassNotFoundException
{
prepare();
synchronized (instance.loader)
{
// try to load class
return instance.loader.loadClass(className);
}
}
/**
* Verify the specified plugin is correctly installed.<br>
* Returns an empty string if the plugin is valid otherwise it returns the error message.
*/
public static String verifyPlugin(PluginDescriptor plugin)
{
final String mess = "Fatal error while loading '" + plugin.getClassName() + "' class from "
+ plugin.getJarFilename() + " :\n";
synchronized (instance.loader)
{
try
{
// then try to load the plugin class as Plugin class
instance.loader.loadClass(plugin.getClassName()).asSubclass(Plugin.class);
}
catch (UnsupportedClassVersionError e)
{
return mess + "Newer java version required.";
}
catch (Error e)
{
return mess + e.toString();
}
catch (ClassCastException e)
{
return mess + IcyExceptionHandler.getErrorMessage(e, false)
+ "Your plugin class should extends 'icy.plugin.abstract_.Plugin' class.";
}
catch (ClassNotFoundException e)
{
return mess + IcyExceptionHandler.getErrorMessage(e, false)
+ "Verify you correctly set the class name in your plugin description.";
}
catch (Exception e)
{
return mess + IcyExceptionHandler.getErrorMessage(e, false);
}
}
return "";
}
/**
* Load all classes from specified path
*/
// private static ArrayList<String> loadAllClasses(String path)
// {
// // search for class names in that path
// final HashSet<String> classNames = ClassUtil.findClassNamesInPath(path, true);
// final ArrayList<String> result = new ArrayList<String>();
//
// synchronized (loader)
// {
// for (String className : classNames)
// {
// try
// {
// // try to load class
// loader.loadClass(className);
// }
// catch (Error err)
// {
// // fatal error while loading class, store error String
// result.add("Fatal error while loading " + className + " :\n" + err.toString() + "\n");
// }
// catch (ClassNotFoundException cnfe)
// {
// // ignore ClassNotFoundException (happen with private class)
// }
// catch (Exception exc)
// {
// result.add("Fatal error while loading " + className + " :\n" + exc.toString() + "\n");
// }
// }
// }
//
// return result;
// }
public static boolean isJCLDisabled()
{
return instance.JCLDisabled;
}
public static void setJCLDisabled(boolean value)
{
instance.JCLDisabled = value;
}
/**
* @deprecated
*/
@Deprecated
public static boolean getLogError()
{
return false;
// return instance.logError;
}
/**
* @deprecated
*/
@Deprecated
public static void setLogError(boolean value)
{
// instance.logError = value;
}
/**
* Called when class loader
*/
protected void changed()
{
// check for missing or mis-installed plugins on first start
if (!initialized)
{
initialized = true;
checkPlugins(false);
}
// start daemon plugins
startDaemons();
// notify listener we have changed
fireEvent(new PluginLoaderEvent());
ThreadUtil.bgRun(new Runnable()
{
@Override
public void run()
{
// pre load the importers classes as they can be heavy
Loader.getSequenceFileImporters();
Loader.getFileImporters();
Loader.getImporters();
}
});
}
/**
* Check for missing plugins and install them if needed.
*/
public static void checkPlugins(boolean showProgress)
{
final List<PluginDescriptor> plugins = getPlugins(false);
final List<PluginDescriptor> required = new ArrayList<PluginDescriptor>();
final List<PluginDescriptor> missings = new ArrayList<PluginDescriptor>();
final List<PluginDescriptor> faulties = new ArrayList<PluginDescriptor>();
if (NetworkUtil.hasInternetAccess())
{
ProgressFrame pf;
if (showProgress)
{
pf = new ProgressFrame("Checking plugins...");
pf.setLength(plugins.size());
pf.setPosition(0);
}
else
pf = null;
PluginRepositoryLoader.waitLoaded();
// get list of required and faulty plugins
for (PluginDescriptor plugin : plugins)
{
// get dependencies
if (!PluginInstaller.getDependencies(plugin, required, null, false))
// error in dependencies --> try to reinstall the plugin
faulties.add(PluginRepositoryLoader.getPlugin(plugin.getClassName()));
if (pf != null)
pf.incPosition();
}
if (pf != null)
pf.setLength(required.size());
// check for missing plugins
for (PluginDescriptor plugin : required)
{
// dependency missing ? --> try to reinstall the plugin
if (!plugin.isInstalled())
{
final PluginDescriptor toInstall = PluginRepositoryLoader.getPlugin(plugin.getClassName());
if (toInstall != null)
missings.add(toInstall);
}
if (pf != null)
pf.incPosition();
}
if ((faulties.size() > 0) || (missings.size() > 0))
{
if (pf != null)
{
pf.setMessage("Installing missing plugins...");
pf.setPosition(0);
pf.setLength(faulties.size() + missings.size());
}
// remove faulty plugins
// for (PluginDescriptor plugin : faulties)
// PluginInstaller.desinstall(plugin, false, false);
// PluginInstaller.waitDesinstall();
// install missing plugins
for (PluginDescriptor plugin : missings)
{
PluginInstaller.install(plugin, true);
if (pf != null)
pf.incPosition();
}
// and reinstall faulty plugins
for (PluginDescriptor plugin : faulties)
{
PluginInstaller.install(plugin, true);
if (pf != null)
pf.incPosition();
}
}
}
}
/**
* Add a listener
*
* @param listener
*/
public static void addListener(PluginLoaderListener listener)
{
synchronized (instance.listeners)
{
instance.listeners.add(PluginLoaderListener.class, listener);
}
}
/**
* Remove a listener
*
* @param listener
*/
public static void removeListener(PluginLoaderListener listener)
{
synchronized (instance.listeners)
{
instance.listeners.remove(PluginLoaderListener.class, listener);
}
}
/**
* fire event
*/
void fireEvent(PluginLoaderEvent e)
{
synchronized (listeners)
{
for (PluginLoaderListener listener : listeners.getListeners(PluginLoaderListener.class))
listener.pluginLoaderChanged(e);
}
}
public static class PluginClassLoader extends JarClassLoader
{
public PluginClassLoader()
{
super();
}
/**
* Give access to this method
*/
public Class<?> getLoadedClass(String name)
{
return super.findLoadedClass(name);
}
/**
* Give access to this method
*/
public boolean isLoadedClass(String name)
{
return getLoadedClass(name) != null;
}
}
public static interface PluginLoaderListener extends EventListener
{
public void pluginLoaderChanged(PluginLoaderEvent e);
}
public static class PluginLoaderEvent
{
public PluginLoaderEvent()
{
super();
}
@Override
public boolean equals(Object obj)
{
if (obj instanceof PluginLoaderEvent)
return true;
return super.equals(obj);
}
}
}