/*
 * Copyright (C) 2004-2011 Jive Software. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.jivesoftware.spark;

import org.apache.commons.lang3.SystemUtils;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.Node;
import org.dom4j.io.SAXReader;
import org.jivesoftware.MainWindowListener;
import org.jivesoftware.Spark;
import org.jivesoftware.resource.Default;
import org.jivesoftware.spark.PluginRes.ResourceType;
import org.jivesoftware.spark.plugin.Plugin;
import org.jivesoftware.spark.plugin.PluginClassLoader;
import org.jivesoftware.spark.plugin.PluginDependency;
import org.jivesoftware.spark.plugin.PublicPlugin;
import org.jivesoftware.spark.util.StringUtils;
import org.jivesoftware.spark.util.URLFileSystem;
import org.jivesoftware.spark.util.log.Log;
import org.jivesoftware.sparkimpl.settings.JiveInfo;
import org.jivesoftware.sparkimpl.settings.local.SettingsManager;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.PropertyResourceBundle;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.TreeSet;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isEmpty;
import static org.jivesoftware.sparkimpl.plugin.InternalPlugins.getInternalPlugins;

/**
 * This manager is responsible for the loading of all Plugins and Workspaces within the Spark environment.
 *
 * @author Derek DeMoro
 */
public class PluginManager implements MainWindowListener
{
    public static final Map<String, String> INCOMPATIBLE_PLUGINS = Map.of(
//        normalizePluginName("OTR Plugin"), "0.4 Beta"
        normalizePluginName("Http File Upload Plugin"), ""
    );

    private final List<Plugin> plugins = new ArrayList<>();

    private final List<PublicPlugin> publicPlugins = new CopyOnWriteArrayList<>();
    private static PluginManager singleton;

    /**
     * The root Plugins Directory.
     */
    public static File PLUGINS_DIRECTORY = Spark.getPluginDirectory();

    private PluginClassLoader classLoader;

    private final Collection<String> blacklistPlugins = Default.getPluginBlacklist();
    private final List<String> deactivatedPlugins = SettingsManager.getLocalPreferences().getDeactivatedPlugins();

    public static PluginManager getInstance()
    {
        if ( null == singleton )
        {
            singleton = new PluginManager();
        }
        return singleton;
    }

    private PluginManager()
    {
        // Do not use deployable plugins if not installed.
        if ( System.getProperty( "plugin" ) == null )
        {
            movePlugins();
        }
        Log.debug("Loading plugins from: " + PLUGINS_DIRECTORY);
    }

    private void movePlugins()
    {
        // Current Plugin directory
        File profilePluginsFolder = new File(Spark.getSparkUserHome(), "plugins").getAbsoluteFile();
        profilePluginsFolder.mkdirs();
        deleteOldPlugins(profilePluginsFolder);
        deletePluginIfNotExistInInstallFolder(profilePluginsFolder);

        File[] files = PLUGINS_DIRECTORY.listFiles(File::isFile);
        if ( files != null )
        {
            for (File file : files) {
                    // Copy over
                    File newFile = new File(profilePluginsFolder, file.getName());
                    if (newFile.lastModified() >= file.lastModified()) {
                        continue;
                    }
                    try {
                        URLFileSystem.copy(file.toURI().toURL(), newFile);
                    } catch (IOException e) {
                        Log.error(e);
                    }
                }
        }

        PLUGINS_DIRECTORY = profilePluginsFolder;
    }

    /**
     * Deletes Plugins in pathToSearch that have a different md5-hash than its correspondent in install\spark\plugins\
     */
    private void deleteOldPlugins( File pathToSearch )
    {
        Log.warning("Deleting old plugins in " + pathToSearch.getAbsolutePath());
        File installPath = Spark.getPluginDirectory();
        File[] files = installPath.listFiles();
        List<File> installerFiles = files == null ? List.of() : Arrays.asList(files);

        final File[] installedPlugins = pathToSearch.listFiles( File::isDirectory );
        if ( installedPlugins == null )
        {
            return;
        }

        for ( final File file : installedPlugins )
        {
            final File jarFile = new File( pathToSearch, file.getName() + ".jar" );
            if ( !jarFile.exists() )
            {
                uninstall( file );
            }
            else
            {
                try
                {
                    File f = new File(installPath, jarFile.getName());
                    // Compare old and new files by checksums
                    if ( installerFiles.contains( f ) )
                    {
                        final String oldFile = StringUtils.getMD5Checksum( jarFile.getAbsolutePath() );
                        final String newFile = StringUtils.getMD5Checksum( f.getAbsolutePath() );
                        if ( !oldFile.equals( newFile ) )
                        {
                            Log.warning( "deleting: " + file.getAbsolutePath() + "," + jarFile.getAbsolutePath() );
                            uninstall( file );
                            jarFile.delete();
                        }
                    }
                }
                catch ( Exception e )
                {
                    Log.error( "No such file", e );
                }
            }
        }
    }

    /**
     * Deletes Plugins in pathToSearch which doesn't exist in install\spark\plugins\
     */
    private void deletePluginIfNotExistInInstallFolder(File pathToSearch)
    {
        Log.warning("Deleting plugins in " + pathToSearch.getAbsolutePath() + " which doesn't exist in " + PLUGINS_DIRECTORY.getAbsolutePath());
        final File[] files = PLUGINS_DIRECTORY.listFiles();
        Set<String> installerFiles;
        if ( files == null )
        {
            installerFiles = Collections.emptySet();
        }
        else
        {
            installerFiles = new TreeSet<>();
            for (File file : files) {
                installerFiles.add(file.getName());
                installerFiles.add(file.getName().split("\\.")[0]);
            }
        }

        final File[] installedPlugins = pathToSearch.listFiles();
        if ( installedPlugins == null )
        {
            return;
        }
            for(File plugin : installedPlugins){
                if(!installerFiles.contains(plugin.getName())){
                    uninstall(plugin);
            }
        }
    }

    /**
     * Loads all {@link Plugin} from the agent plugins.xml and extension lib.
     */
    public void loadPlugins()
    {
        // Delete all old plugins
        File[] oldFiles = PLUGINS_DIRECTORY.listFiles(File::isDirectory);
        if ( oldFiles != null )
        {
            for ( File file : oldFiles )
            {
                    // Check to see if it has an associated .jar
                    File jarFile = new File( PLUGINS_DIRECTORY, file.getName() + ".jar" );
                    if ( !jarFile.exists() )
                    {
                        uninstall( file );
                    }
                }
        }

        updateClasspath();
        loadInternalPlugins();
        loadPublicPlugins();
        loadDevPlugin();
        loadPluginResources();
    }

    private void loadDevPlugin() {
        // For development purposes, load the plugin specified by -Dplugin=...
        String plugin = System.getProperty( "plugin" );
        if ( plugin != null )
        {
            final StringTokenizer st = new StringTokenizer( plugin, ",", false );
            while ( st.hasMoreTokens() )
            {
                String token = st.nextToken();
                File pluginXML = new File( token );
                loadPublicPlugin( pluginXML.getParentFile() );
            }
        }
    }

    private boolean hasDependencies( File pluginFile )
    {
        SAXReader saxReader = SAXReader.createDefault();
        try
        {
            final Document pluginXML = saxReader.read( pluginFile );
            final List<?> dependencies = pluginXML.selectNodes( "plugin/depends/plugin" );
            return dependencies != null && !dependencies.isEmpty();
        }
        catch ( DocumentException e )
        {
            Log.error( "Unable to read plugin dependencies from " + pluginFile, e );
            return false;
        }
    }

    static String normalizePluginName( String value )
    {
        return value.replaceAll( "[^0-9a-zA-Z]", "" ).toLowerCase();
    }

    /**
     * Loads public plugins.
     *
     * @param pluginDir the directory of the expanded public plugin.
     * @return the new Plugin model for the Public Plugin.
     */
    private Plugin loadPublicPlugin( File pluginDir )
    {
        File pluginFile = new File( pluginDir, "plugin.xml" );
        SAXReader saxReader = SAXReader.createDefault();
        Document pluginXML = null;
        try
        {
            pluginXML = saxReader.read( pluginFile );
        }
        catch ( DocumentException e )
        {
            Log.error( "Unable to read plugin XML file from " + pluginDir, e );
            return null;
        }

        Node plugin = pluginXML.selectSingleNode( "/plugin" );
        if (plugin == null) {
            Log.error("Not loading plugin " + pluginDir + ": no <plugin>");
            return null;
        }
        PublicPlugin publicPlugin = parsePluginXml((Element) plugin);
        if (publicPlugin == null) {
            return null;
        }
        try {
            String lower = normalizePluginName(publicPlugin.getName());
            // Don't load the plugin if it's known to be incompatible with this version of Spark.
            String incompatibleVersion = INCOMPATIBLE_PLUGINS.get(lower);
            if (incompatibleVersion != null && (incompatibleVersion.isEmpty() || incompatibleVersion.compareTo(publicPlugin.getVersion()) >= 0)) {
                Log.warning("Not loading plugin " + publicPlugin.getName() + " (version " + publicPlugin.getVersion() + ") as it is incompatible with this version of Spark.");
                return null;
            }
            // Don't load the plugin if it's on the Blacklist
            if (deactivatedPlugins.contains(publicPlugin.getName())) {
                Log.warning("Not loading plugin " + publicPlugin.getName() + " as it is deactivated.");
                return null;
            }
            // Don't load the plugin if it's on the Blacklist
            if (blacklistPlugins.contains(lower) || blacklistPlugins.contains(publicPlugin.getPluginClass())) {
                Log.warning("Not loading plugin " + publicPlugin.getName() + " as it is blacklisted.");
                return null;
            }
        } catch (Exception e) {
            Log.warning("An exception occurred while checking the plugin blacklist for " + publicPlugin.getName(), e);
            return null;
        }

        try {
            Class<? extends Plugin> pluginType = classLoader.loadClass(publicPlugin.getPluginClass()).asSubclass(Plugin.class);
            Plugin pluginInstance = pluginType.getDeclaredConstructor().newInstance();
            Log.debug(publicPlugin.getName() + " has been loaded.");
            publicPlugin.setPluginDir(pluginDir);
            publicPlugins.add(publicPlugin);

            registerPlugin(pluginInstance);
            return pluginInstance;
        } catch (Throwable e) {
            Log.error("Unable to load plugin " + publicPlugin.getPluginClass() + ".", e);
        }
        return null;
    }

    /**
     * Parse a single plugin node from plugin.xml.
     * If the plugin's meta-info is invalid or the plugin is not compatible will return null.
     */
    public PublicPlugin parsePluginXml(Element plugin) {
        try {
            Node nameNode = plugin.selectSingleNode("name");
            String name = nameNode != null ? nameNode.getText().trim() : null;
            if (isEmpty(name)) {
                Log.error("Not loading plugin: name is empty" + plugin);
                return null;
            }
            // Do operating system check.
            if (!isOperatingSystemOK(plugin)) {
                return null;
            }
            // Check for a minimum version of Spark
            Node minSparkVersionNode = plugin.selectSingleNode("minSparkVersion");
            if (minSparkVersionNode != null) {
                String minVersion = minSparkVersionNode.getText().trim();
                String buildNumber = JiveInfo.getVersion();
                if (buildNumber.compareTo(minVersion) < 0) {
                    Log.warning("Not loading plugin " + name + " as it is not supported by Spark version.");
                    return null;
                }
            };
            // Check for minimum Java version
            Node nodeJavaVersion = plugin.selectSingleNode("java");
            if (nodeJavaVersion != null) {
                final String pluginMinVersion = nodeJavaVersion.getText().trim();
                final int jv = StringUtils.getJavaMajorVersion(pluginMinVersion);
                final int mv = StringUtils.getJavaMajorVersion(SystemUtils.JAVA_VERSION);
                if (mv < jv) {
                    Log.error("Unable to load plugin " + name + " due to old JavaVersion.\n" +
                        "It Requires " + pluginMinVersion + " you have " + SystemUtils.JAVA_VERSION);
                    return null;
                }
            };

            Node clazzNode = plugin.selectSingleNode("class");
            Node versionNode = plugin.selectSingleNode("version");
            String clazz = clazzNode != null ? clazzNode.getText().trim() : null;
            String version = versionNode != null ? versionNode.getText().trim() : null;
            if (isEmpty(clazz) || isEmpty(version)) {
                Log.error("Not loading plugin: class or version are empty");
                return null;
            }

            PublicPlugin publicPlugin = new PublicPlugin();
            publicPlugin.setName(name);
            publicPlugin.setPluginClass(clazz);
            publicPlugin.setVersion(version);

            // set dependencies
            try
            {
                List<? extends Node> dependencies = plugin.selectNodes( "depends/plugin" );
                dependencies = dependencies != null ? dependencies : List.of();
                for ( Node depend1 : dependencies )
                {
                    Element depend = (Element) depend1;
                    PluginDependency dependency = new PluginDependency();
                    dependency.setVersion( depend.selectSingleNode( "version" ).getText() );
                    dependency.setName( depend.selectSingleNode( "name" ).getText() );
                    publicPlugin.addDependency( dependency );
                }
            }
            catch ( Exception e )
            {
                Log.warning( "An exception occurred during the setting of dependencies while loading plugin " + name, e );
            }

            Node authorNode = plugin.selectSingleNode("author");
            String author = authorNode != null ? authorNode.getText().trim() : null;
            publicPlugin.setAuthor( author );

            Node emailNode = plugin.selectSingleNode("email");
            String email = emailNode != null ? emailNode.getText().trim() : null;
            publicPlugin.setEmail( email );

            Node descriptionNode = plugin.selectSingleNode("description");
            String description = descriptionNode != null ? descriptionNode.getText().trim() : null;
            publicPlugin.setDescription( description );

            Node homePageNode = plugin.selectSingleNode("homePage");
            String homePage = homePageNode != null ? homePageNode.getText().trim() : null;
            publicPlugin.setHomePage( homePage );

            Node downloadNode = plugin.selectSingleNode("downloadURL");
            if (downloadNode != null) {
                String downloadURL = downloadNode.getText();
                publicPlugin.setDownloadURL(downloadURL);
            }

            Node changeLogNode = plugin.selectSingleNode("changeLog");
            if (changeLogNode != null) {
                publicPlugin.setChangeLogURL(changeLogNode.getText());
            }

            Node readMeNode = plugin.selectSingleNode("readme");
            if (readMeNode != null) {
                publicPlugin.setReadMeURL(readMeNode.getText());
            }

            Node smallIcon = plugin.selectSingleNode("smallIcon");
            if (smallIcon != null) {
                publicPlugin.setSmallIconAvailable(true);
            }

            Node largeIcon = plugin.selectSingleNode("largeIcon");
            if (largeIcon != null) {
                publicPlugin.setLargeIconAvailable(true);
            }

            return publicPlugin;
        } catch (Exception e) {
            Log.error("Error retrieving PluginInformation from xml.", e);
            return null;
        }
    }

    /**
     * Loads an internal plugin.
     */
    private void loadInternalPlugins() {
        for (PublicPlugin plugin : getInternalPlugins()) {
                try {
                    Class<? extends Plugin> pluginType = Class.forName(plugin.getPluginClass()).asSubclass(Plugin.class);
                    Plugin pluginInstance = pluginType.getDeclaredConstructor().newInstance();
                    Log.debug(plugin.getName() + " has been loaded. Internal plugin.");
                    registerPlugin(pluginInstance);
                } catch (Throwable ex) {
                    Log.error("Unable to load plugin " + plugin.getPluginClass(), ex);
                }
        }
    }

    private void updateClasspath()
    {
        Log.debug("updating classpath: " + PLUGINS_DIRECTORY);
        try
        {
            classLoader = new PluginClassLoader( getParentClassLoader(), PLUGINS_DIRECTORY );
            PluginRes.setClassLoader( classLoader );
        }
        catch ( MalformedURLException e )
        {
            Log.error( "Error updating classpath.", e );
        }
        Thread.currentThread().setContextClassLoader( classLoader );
    }

    private void loadPluginResources( String resourceName, ResourceType type )
    {
        Log.debug("Loading plugin resources from " + resourceName);
        try
        {
            PropertyResourceBundle prbPlugin = (PropertyResourceBundle) ResourceBundle.getBundle( resourceName, Locale.getDefault(), classLoader );
            for ( String key : prbPlugin.keySet() )
            {
                PluginRes.putRes( key, prbPlugin.getString( key ), type );
            }
        }
        catch ( Exception ex )
        {
            Log.debug( resourceName + " is not overwritten in plugin " );
        }
    }

    /**
     * Loads property resources from spark.properties, default.properties, spark_i18n.properties (properly localized)
     * located in a plugin's jar, if any In case the plugin contains preferences.properties, plugin specific defaults will
     * be loaded instead of spark defaults for preferences This method is called right after all plugins are loaded,
     * specifically after plugins class loader is initialized and plugins jars are loaded in classpath
     */
    private void loadPluginResources()
    {
        loadPluginResources( "spark", ResourceType.SPARK );
        loadPluginResources( "default", ResourceType.DEFAULT );
        loadPluginResources( "preferences", ResourceType.PREFERENCES );
        loadPluginResources( "spark_i18n", ResourceType.I18N );
    }

    /**
     * Registers a plugin.
     */
    public void registerPlugin( Plugin plugin )
    {
        plugins.add( plugin );
    }

    /**
     * Loads and initializes all Plugins.
     *
     * @see Plugin
     */
    public void initializePlugins()
    {
        try
        {
            Log.debug("Start plugin dependency check");
            int j;
            boolean dependencyFound;
            // Dependency check
            for ( int i = 0; i < publicPlugins.size(); i++ )
            {
                // if dependencies are available, check these
                PublicPlugin publicPlugin = publicPlugins.get(i);
                if (!publicPlugin.getDependency().isEmpty())
                {
                    List<PluginDependency> dependencies = publicPlugin.getDependency();
                    // go through all dependencies
                    for ( PluginDependency dependency : dependencies )
                    {
                        j = 0;
                        dependencyFound = false;
                        // look for the specific plugin
                        for ( PublicPlugin plugin : publicPlugins )
                        {
                            if ( plugin.getName() != null && plugin.getName().equals( dependency.getName() ) )
                            {
                                // if the version is compatible then reorder
                                if ( dependency.compareVersion( plugin.getVersion() ) )
                                {
                                    dependencyFound = true;
                                    // when the depended Plugin hadn't been installed yet
                                    if ( j > i )
                                    {
                                        // find the position of plugins-List because it has more entries
                                        int counter = 0, x = 0, z = 0;
                                        for ( Plugin plug : plugins )
                                        {
                                            // find the position of the aim-object
                                            if ( plug.getClass().toString().substring( 6 ).equals( publicPlugins.get( j ).getPluginClass() ) )
                                            {
                                                x = counter;
                                            }
                                            // find the change-position
                                            else if ( plug.getClass().toString().substring( 6 ).equals( publicPlugin.getPluginClass() ) )
                                            {
                                                z = counter;
                                            }
                                            counter++;
                                        }

                                        // change the order
                                        publicPlugins.add( i, publicPlugins.get( j ) );
                                        publicPlugins.remove( j + 1 );

                                        plugins.add( z, plugins.get( x ) );
                                        plugins.remove( x + 1 );

                                        // start again, to check the other dependencies
                                        i--;
                                    }
                                }
                                // else don't load the plugin and show an error
                                else
                                {
                                    Log.error( "Depended Plugin " + dependency.getName() + " hasn't the right version (" + dependency.getVersion() + "<>" + plugin.getVersion() );
                                }
                                break;
                            }
                            j++;
                        }

                        // If the depended Plugin wasn't found, then show error.
                        if ( !dependencyFound )
                        {
                            Log.error( "Depended Plugin " + dependency.getName() + " is missing for the Plugin " + publicPlugin.getName() );

                            // find the position of plugins-List because it has more entries
                            int counter = 0;
                            for ( Plugin plug : plugins )
                            {
                                // find the delete-position
                                if ( plug.getClass().toString().substring( 6 ).equals( publicPlugin.getPluginClass() ) )
                                {
                                    break;
                                }
                                counter++;
                            }
                            // delete the Plugin, because the depended Plugin is missing
                            publicPlugins.remove( i );
                            plugins.remove( counter );
                            i--;
                            break;
                        }
                    }
                }
            }
            Log.debug("Completed plugin dependency check");

                for ( Plugin plugin : plugins )
                {
                    initializePlugin(plugin);
                }
        }
        catch ( Exception e )
        {
            Log.error( "An exception occurred while initializing plugins.", e );
        }
    }

    private static void initializePlugin(Plugin plugin) {
        try
        {
            plugin.initialize();
            Log.debug("Initialized " + plugin.getClass().getSimpleName());
        }
        catch ( Throwable e )
        {
            Log.error( "An exception occurred while initializing plugin " + plugin.getClass().getSimpleName(), e );
        }
    }

    @Override
	public void shutdown()
    {
        for ( Plugin plugin : plugins )
        {
            try
            {
                plugin.shutdown();
            }
            catch ( NoSuchMethodError e )
            {
                Log.error( "NoSuchMethodError on shutdown of plugin" + e );
            }
            catch ( Exception e )
            {
                Log.warning( "Exception on shutdown of plugin.", e );
            }
        }
    }

    /**
     * Locates the best class loader based on context (see class description).
     *
     * @return The best parent classloader to use
     */
    private ClassLoader getParentClassLoader()
    {
        ClassLoader parent = Thread.currentThread().getContextClassLoader();
        if ( parent == null )
        {
            parent = this.getClass().getClassLoader();
            if ( parent == null )
            {
                parent = ClassLoader.getSystemClassLoader();
            }
        }
        return parent;
    }

    /**
     * Expands all plugin packs (.jar files located in the plugin dir with plugin.xml).
     */
    private void expandNewPlugins()
    {
        File[] jars = PLUGINS_DIRECTORY.listFiles( ( dir, name ) -> {
            boolean accept = false;
            String smallName = name.toLowerCase();
            if ( smallName.endsWith( ".jar" ) )
            {
                accept = true;
            }
            return accept;
        } );

        // Do nothing if no jar or zip files were found
        if ( jars == null )
        {
            return;
        }

        for ( File jar : jars )
        {
            if ( jar.isFile() )
            {

                URL url = null;
                try
                {
                    url = jar.toURI().toURL();
                }
                catch ( MalformedURLException e )
                {
                    Log.error( e );
                }
                String name = URLFileSystem.getName( url );
                File directory = new File( PLUGINS_DIRECTORY, name );
                if ( directory.exists() && directory.isDirectory() )
                {
                    // Check to see if directory contains the plugin.xml file.
                    // If not, delete directory.
                    File pluginXML = new File( directory, "plugin.xml" );
                    if ( pluginXML.exists() )
                    {
                        if ( pluginXML.lastModified() < jar.lastModified() )
                        {
                            uninstall( directory );
                            unzipPlugin( jar, directory );
                        }
                        continue;
                    }
                    uninstall( directory );
                }
                else
                {
                    // Unzip contents into directory
                    unzipPlugin( jar, directory );
                }
            }
        }
    }

    private void loadPublicPlugins()
    {
        // First, expand all plugins that have yet to be expanded.
        expandNewPlugins();
        File[] files = PLUGINS_DIRECTORY.listFiles(File::isDirectory);
        // Do nothing if no jar or zip files were found
        if ( files == null )
        {
            return;
        }
        //Make sure to load first the plugins with no dependencies
        // If a plugin with dependencies gets loaded before one of the dependencies,
        //class not found exception may be thrown if a dependency class is used during plugin creation
        List<File> dependencies = new ArrayList<>();
        List<File> nodependencies = new ArrayList<>();
        for ( File file : files )
        {
            File pluginXML = new File( file, "plugin.xml" );
            if ( pluginXML.exists() )
            {
                if ( hasDependencies( pluginXML ) )
                {
                    dependencies.add( file );
                }
                else
                {
                    nodependencies.add( file );
                }
            }
        }
        for (File file : nodependencies) {
            loadPlugin(file);
        }
        for (File file : dependencies) {
            loadPlugin(file);
        }
    }

    private Plugin loadPlugin(File pluginDir) {
        try {
            Log.debug("Start loading plugin " + pluginDir.getAbsolutePath());
            classLoader.addPlugin(pluginDir);
            return loadPublicPlugin(pluginDir);
        } catch (Throwable e) {
            Log.error("Unable to load dirs", e);
            return null;
        }
    }

    /**
     * Adds and installs a new plugin into Spark.
     */
    public void addPlugin(PublicPlugin plugin) {
        Log.debug("Loading plugin from " + plugin.getPluginDir());
        expandNewPlugins();
        Plugin pluginInstance = loadPlugin(plugin.getPluginDir());
        if (pluginInstance == null) {
            return;
        }
        initializePlugin(pluginInstance);
    }

    /**
     * Unzips a plugin from a JAR file into a directory. If the JAR file
     * isn't a plugin, this method will do nothing.
     *
     * @param file the JAR file
     * @param dir  the directory to extract the plugin to.
     */
    private void unzipPlugin( File file, File dir )
    {
        Log.warning("Unzipping plugin from " + file.getAbsolutePath() + " to " + dir.getAbsolutePath());
        try
        {
            ZipFile zipFile = new JarFile( file );
            // Ensure that this JAR is a plugin.
            if ( zipFile.getEntry( "plugin.xml" ) == null )
            {
                return;
            }
            dir.mkdir();
            for ( Enumeration<? extends ZipEntry> e = zipFile.entries(); e.hasMoreElements(); )
            {
                JarEntry entry = (JarEntry) e.nextElement();
                File entryFile = new File( dir, entry.getName() );

                //Fix Zip Slip Vulnerability
                if (!entryFile.toPath().normalize().startsWith(dir.toPath().normalize())) {
                    throw new RuntimeException("Bad zip entry");
                }
                
                // Ignore any MANIFEST.MF entries.
                if (entry.getName().toUpperCase().endsWith("MANIFEST.MF"))
                {
                    continue;
                }
                if ( !entry.isDirectory() )
                {
                    entryFile.getParentFile().mkdirs();
                    FileOutputStream out = new FileOutputStream( entryFile );
                    InputStream zin = zipFile.getInputStream( entry );
                    byte[] b = new byte[ 512 ];
                    int len;
                    while ( ( len = zin.read( b ) ) != -1 )
                    {
                        out.write( b, 0, len );
                    }
                    out.flush();
                    out.close();
                    zin.close();
                }
            }
            zipFile.close();
        }
        catch ( Throwable e )
        {
            Log.error( "Error unzipping plugin", e );
        }
    }

    /**
     * Returns a collection of all public plugins.
     *
     * @return the collection of public plugins.
     */
    public List<PublicPlugin> getPublicPlugins()
    {
        return publicPlugins;
    }

    private void uninstall( File pluginDir )
    {
        Log.warning("Uninstalling plugin: " + pluginDir);
        try
        {
            Files.walkFileTree( pluginDir.toPath(), new SimpleFileVisitor<Path>()
            {
                @Override
                public FileVisitResult visitFile( Path file, BasicFileAttributes attrs ) throws IOException
                {
                    Files.delete( file );
                    return FileVisitResult.CONTINUE;
                }

                @Override
                public FileVisitResult postVisitDirectory( Path dir, IOException exc ) throws IOException
                {
                    Files.delete( dir );
                    return FileVisitResult.CONTINUE;
                }
            } );
        }
        catch ( IOException e )
        {
            Log.error( "An unexpected exception occurred while trying to uninstall a plugin from: " + pluginDir, e );
        }
   }

    /**
     * Removes and uninstall a plugin from Spark.
     *
     * @param plugin the plugin to uninstall.
     */
    public void removePublicPlugin( PublicPlugin plugin )
    {
        Log.warning("Remove plugin: " + plugin.getName());
        for ( PublicPlugin publicPlugin : getPublicPlugins() )
        {
            if ( plugin.getName().equals( publicPlugin.getName() ) )
            {
                uninstall( plugin.getPluginDir() );
                publicPlugins.remove( plugin );
            }
        }
    }

    /**
     * Returns true if the specified plugin is installed.
     *
     * @param plugin the <code>PublicPlugin</code> plugin to check.
     * @return true if installed.
     */
    public boolean isInstalled( PublicPlugin plugin )
    {
        for ( PublicPlugin publicPlugin : getPublicPlugins() )
        {
            if ( plugin.getName().equals( publicPlugin.getName() ) )
            {
                return true;
            }
        }

        return false;
    }

    /**
     * Checks the plugin for the required operating system.
     *
     * @param plugin the Plugin element to check.
     * @return true if the operating system is ok for the plugin to run on.
     */
    private boolean isOperatingSystemOK( Node plugin )
    {
        try
        {
            final Element osElement = (Element) plugin.selectSingleNode( "os" );
            if ( osElement != null )
            {
                String operatingSystem = osElement.getText();
                // If no OS specified, then assume all of them are supported
                if (isBlank(operatingSystem)) {
                    return true;
                }
                final String currentOS = JiveInfo.getOS().toLowerCase();
                // Iterate through comma-delimited string
                StringTokenizer tkn = new StringTokenizer( operatingSystem, "," );
                while ( tkn.hasMoreTokens() )
                {
                    String os = tkn.nextToken().toLowerCase();
                    if (currentOS.contains(os)) {
                        return true;
                    }
                }
                Log.warning( "Unable to load plugin " + plugin.selectSingleNode( "name" ).getText() + " due to invalid operating system. Required OS = " + operatingSystem );
                return false;
            }
        }
        catch ( Exception e )
        {
            Log.error("An exception occurred while trying to determine operating system compatibility of plugin '" + plugin + "'", e);
        }
        return true;
    }
}
