/*
 * 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);
        }
    }
}