ch.jamiete.hilda.plugins.PluginManager.java Source code

Java tutorial

Introduction

Here is the source code for ch.jamiete.hilda.plugins.PluginManager.java

Source

/*******************************************************************************
 * Copyright 2017 jamietech
 *
 * 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 ch.jamiete.hilda.plugins;

import java.io.File;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.logging.Level;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import org.apache.commons.io.IOUtils;
import com.google.gson.Gson;
import ch.jamiete.hilda.Hilda;
import ch.jamiete.hilda.Sanity;

public class PluginManager {
    private final List<HildaPlugin> plugins = Collections.synchronizedList(new ArrayList<HildaPlugin>());
    private final Hilda hilda;

    public PluginManager(final Hilda hilda) {
        this.hilda = hilda;
    }

    public void disablePlugins() {
        ExecutorService executor = Executors.newSingleThreadExecutor();

        synchronized (this.plugins) {
            final Iterator<HildaPlugin> iterator = this.plugins.iterator();

            while (iterator.hasNext()) {
                final HildaPlugin entry = iterator.next();

                Future<?> future = executor.submit(() -> {
                    try {
                        entry.onDisable();
                    } catch (final Exception e) {
                        Hilda.getLogger().log(Level.WARNING, "Encountered an exception while disabling plugin "
                                + entry.getPluginData().getName(), e);
                    }
                });

                try {
                    future.get(30, TimeUnit.SECONDS);
                } catch (InterruptedException | ExecutionException | TimeoutException e) {
                    Hilda.getLogger().log(Level.WARNING, "Plugin " + entry.getPluginData().getName()
                            + " took too long disabling; ceased executing its code", e);
                }
            }
        }

        executor.shutdown();

        try {
            executor.awaitTermination(5, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            Hilda.getLogger().log(Level.WARNING, "Encountered an exception during the plugin disable grace period",
                    e);
        }
    }

    public void enablePlugins() {
        synchronized (this.plugins) {
            final Iterator<HildaPlugin> iterator = this.plugins.iterator();

            while (iterator.hasNext()) {
                final HildaPlugin entry = iterator.next();

                try {
                    entry.onEnable();
                    Hilda.getLogger().info("Enabled " + entry.getPluginData().getName() + " v"
                            + entry.getPluginData().getVersion() + " by " + entry.getPluginData().getAuthor());
                } catch (final Exception e) {
                    Hilda.getLogger()
                            .log(Level.WARNING, "Encountered an exception while enabling plugin "
                                    + entry.getPluginData().getName() + " v" + entry.getPluginData().getVersion(),
                                    e);
                    this.plugins.remove(entry);
                }
            }
        }
    }

    /**
     * Gets the plugin with that name or null if there is none.
     * @param name Name to test
     * @return Plugin with that name
     */
    public HildaPlugin getPlugin(final String name) {
        return this.plugins.stream().filter(p -> p.getPluginData().getName().equalsIgnoreCase(name)).findFirst()
                .orElse(null);
    }

    /**
     * Gets a list of the plugins tracked by the manager.
     *
     * @return An unmodifiable list
     */
    public List<HildaPlugin> getPlugins() {
        return Collections.unmodifiableList(this.plugins);
    }

    private boolean isLoaded(final PluginData data) {
        return this.plugins.stream().filter(p -> p.getPluginData().equals(data)).findAny().isPresent();
    }

    private boolean isLoaded(final String name) {
        return this.plugins.stream().filter(p -> p.getPluginData().name.equals(name)).findAny().isPresent();
    }

    private boolean loadPlugin(final PluginData data) {
        if (this.isLoaded(data)) {
            return true;
        }

        try {
            final URLClassLoader sysloader = (URLClassLoader) ClassLoader.getSystemClassLoader();
            final Class<URLClassLoader> sysclass = URLClassLoader.class;
            final Method method = sysclass.getDeclaredMethod("addURL", new Class[] { URL.class });
            method.setAccessible(true);
            method.invoke(sysloader, new Object[] { data.pluginFile.toURI().toURL() });

            final Class<?> mainClass = Class.forName(data.mainClass);

            if (mainClass != null) {
                if (!HildaPlugin.class.isAssignableFrom(mainClass)) {
                    Hilda.getLogger().severe("Could not load plugin " + data.getName()
                            + " because its main class did not implement HildaPlugin!");
                    return false;
                }

                final HildaPlugin newPlugin = (HildaPlugin) mainClass.getConstructor(Hilda.class)
                        .newInstance(this.hilda);

                final Field pluginDataField = HildaPlugin.class.getDeclaredField("pluginData");
                pluginDataField.setAccessible(true);
                pluginDataField.set(newPlugin, data);

                this.plugins.add(newPlugin);

                try {
                    newPlugin.onLoad();
                } catch (final Exception e) {
                    Hilda.getLogger().log(Level.WARNING, "Encountered exception when calling load method of plugin "
                            + data.name + ". It may not have properly loaded and may cause errors.", e);
                }

                Hilda.getLogger().info("Loaded plugin " + data.name);
                return true;
            }
        } catch (final Exception ex) {
            Hilda.getLogger().log(Level.WARNING, "Encountered exception when loading plugin " + data.name, ex);
        }

        return false;
    }

    /**
     * Attempts to load plugin data from the {@code plugin.json} file.
     * @param file
     * @return the plugin data or {@code null} if no data could be loaded
     * @throws IllegalArgumentException if any of the conditions of a plugin data file are not met
     */
    private PluginData loadPluginData(final File file) {
        PluginData data = null;

        try {
            final ZipFile zipFile = new ZipFile(file);
            final Enumeration<? extends ZipEntry> entries = zipFile.entries();

            while (entries.hasMoreElements()) {
                final ZipEntry entry = entries.nextElement();
                final InputStream stream = zipFile.getInputStream(entry);

                if (entry.getName().equals("plugin.json")) {
                    data = new Gson().fromJson(IOUtils.toString(stream, Charset.defaultCharset()),
                            PluginData.class);

                    Sanity.nullCheck(data.name, "A plugin must define its name.");
                    Sanity.nullCheck(data.mainClass, "A plugin must define its main class.");
                    Sanity.nullCheck(data.version, "A plugin must define its version.");
                    Sanity.nullCheck(data.author, "A plugin must define its author.");

                    data.pluginFile = file;

                    if (data.dependencies == null) {
                        data.dependencies = new String[0];
                    }
                }
            }

            zipFile.close();
        } catch (final Exception ex) {
            Hilda.getLogger().log(Level.SEVERE,
                    "Encountered exception when trying to load plugin JSON for " + file.getName(), ex);
        }

        return data;
    }

    public void loadPlugins() {
        final File pluginsDir = new File("plugins");

        if (!pluginsDir.exists() || !pluginsDir.isDirectory()) {
            Hilda.getLogger().info("Starting without plugins!");
            return;
        }

        final Map<String, PluginData> jars = new HashMap<String, PluginData>();
        final Map<String, List<String>> dependencies = new HashMap<String, List<String>>();
        final Map<String, List<String>> load_after = new HashMap<String, List<String>>();

        // Load all data about valid plugins
        for (final File file : pluginsDir.listFiles()) {
            PluginData data = null;

            if (file.isFile() && file.getName().endsWith(".jar")) {
                try {
                    data = this.loadPluginData(file);

                    if (data == null) {
                        Hilda.getLogger().warning("Failed to load plugin data for " + file.getName());
                        continue;
                    } else {
                        jars.put(data.name, data);
                    }
                } catch (final Exception e) {
                    Hilda.getLogger().log(Level.WARNING, "Failed to load plugin data for " + file.getName(), e);
                    continue;
                }
            } else {
                continue;
            }

            if (data.dependencies != null && data.dependencies.length > 0) {
                dependencies.put(data.name, new ArrayList<String>(Arrays.asList(data.dependencies)));
            }

            if (data.load_after != null && data.load_after.length > 0) {
                load_after.put(data.name, new ArrayList<String>(Arrays.asList(data.load_after)));
            }
        }

        // Remove any load after value that isn't known to the plugin loader
        for (final List<String> list : load_after.values()) {
            final Iterator<String> values = list.iterator();

            while (values.hasNext()) {
                final String name = values.next();

                if (!jars.containsKey(name)) {
                    values.remove();
                }
            }
        }

        // Attempt to load all jars
        while (!jars.isEmpty()) {
            boolean missing = true;
            Iterator<String> iterator = jars.keySet().iterator();

            while (iterator.hasNext()) {
                final String plugin_name = iterator.next();

                // Remove any dependencies we've already loaded, or fail
                if (dependencies.containsKey(plugin_name)) {
                    final Iterator<String> dependency_iterator = dependencies.get(plugin_name).iterator();

                    while (dependency_iterator.hasNext()) {
                        final String dependency = dependency_iterator.next();

                        if (this.isLoaded(dependency)) {
                            dependency_iterator.remove();
                        } else if (!jars.containsKey(dependency)) {
                            iterator.remove();
                            dependency_iterator.remove();
                            load_after.remove(plugin_name);

                            Hilda.getLogger().warning("Failed to load plugin " + plugin_name
                                    + " because dependency " + dependency + " could not be found!");

                            break;
                        }
                    }

                    if (dependencies.containsKey(plugin_name) && dependencies.get(plugin_name).isEmpty()) {
                        dependencies.remove(plugin_name);
                    }
                }

                // Remove any load after value we've already loaded
                if (load_after.containsKey(plugin_name)) {
                    final Iterator<String> load_after_iterator = load_after.get(plugin_name).iterator();

                    while (load_after_iterator.hasNext()) {
                        final String loadafter = load_after_iterator.next();

                        if (this.isLoaded(loadafter)) {
                            load_after_iterator.remove();
                        }
                    }

                    if (load_after.containsKey(plugin_name) && load_after.get(plugin_name).isEmpty()) {
                        load_after.remove(plugin_name);
                    }
                }

                // No dependencies remain unloaded
                if (!dependencies.containsKey(plugin_name)
                        || load_after.containsKey(plugin_name) && jars.containsKey(plugin_name)) {
                    final boolean successful = this.loadPlugin(jars.get(plugin_name));

                    if (successful) {
                        iterator.remove();
                        missing = false;
                        continue;
                    }
                }
            } // End of jar iteration

            // Try to load anything
            if (missing) {
                iterator = jars.keySet().iterator();

                while (iterator.hasNext()) {
                    final String plugin_name = iterator.next();

                    if (!dependencies.containsKey(plugin_name)) {
                        iterator.remove();
                        load_after.remove(plugin_name);

                        if (this.loadPlugin(jars.get(plugin_name))) {
                            break;
                        }
                    }
                }
            }

            // No plugins without a dependency remain
            if (missing) {
                iterator = jars.keySet().iterator();

                while (iterator.hasNext()) {
                    iterator.remove();
                    Hilda.getLogger()
                            .warning("Absolutely could not load " + iterator.next() + " and have given up trying.");
                }
            }
        }
    }
}