From 107bf41a76e868a0e839e8d70201a4e9758ae71b Mon Sep 17 00:00:00 2001 From: Pingex Date: Thu, 21 Jul 2016 19:04:06 +0200 Subject: [PATCH] Basic modularity system: plugin preloading. --- .gitignore | 1 + dcf.properties.example | 3 + .../dcf/DiscordCommandableFramework.java | 6 + .../net/pingex/dcf/core/Configuration.java | 15 +- .../net/pingex/dcf/modularity/IPlugin.java | 30 ++++ .../net/pingex/dcf/modularity/Plugin.java | 29 ++++ .../pingex/dcf/modularity/PluginLoader.java | 130 ++++++++++++++++++ .../pingex/dcf/modularity/PluginState.java | 37 +++++ .../pingex/dcf/modularity/PluginWrapper.java | 104 ++++++++++++++ .../pingex/dcf/modularity/package-info.java | 4 + 10 files changed, 358 insertions(+), 1 deletion(-) create mode 100644 src/main/java/net/pingex/dcf/modularity/IPlugin.java create mode 100644 src/main/java/net/pingex/dcf/modularity/Plugin.java create mode 100644 src/main/java/net/pingex/dcf/modularity/PluginLoader.java create mode 100644 src/main/java/net/pingex/dcf/modularity/PluginState.java create mode 100644 src/main/java/net/pingex/dcf/modularity/PluginWrapper.java create mode 100644 src/main/java/net/pingex/dcf/modularity/package-info.java diff --git a/.gitignore b/.gitignore index 8ff4ec1..0d4473e 100644 --- a/.gitignore +++ b/.gitignore @@ -70,3 +70,4 @@ gradle-app.setting # gradle/wrapper/gradle-wrapper.properties dcf.properties +plugins/ diff --git a/dcf.properties.example b/dcf.properties.example index 3b1681c..4b9d503 100644 --- a/dcf.properties.example +++ b/dcf.properties.example @@ -1,6 +1,9 @@ # Bot display name general.bot_name = DCF Bot +# Plugins directory location +generel.plugins_dir = plugins + # Initial connection token... discord.token = tokenGoesHere diff --git a/src/main/java/net/pingex/dcf/DiscordCommandableFramework.java b/src/main/java/net/pingex/dcf/DiscordCommandableFramework.java index c94d097..1c7cbf1 100644 --- a/src/main/java/net/pingex/dcf/DiscordCommandableFramework.java +++ b/src/main/java/net/pingex/dcf/DiscordCommandableFramework.java @@ -2,6 +2,7 @@ package net.pingex.dcf; import net.pingex.dcf.core.Configuration; import net.pingex.dcf.core.GatewayConnectionsManager; +import net.pingex.dcf.modularity.PluginLoader; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import sx.blah.discord.api.ClientBuilder; @@ -21,12 +22,17 @@ public class DiscordCommandableFramework */ public static void main(String[] args) { + // Load configuration Configuration.load(); Configuration.init(); + LOGGER.info("Hello World"); LOGGER.info(Configuration.BOT_NAME); LOGGER.info("Framework version {}", DiscordCommandableFramework.class.getPackage().getImplementationVersion() != null ? DiscordCommandableFramework.class.getPackage().getImplementationVersion() : "UNKNOWN"); + // Load plugins + PluginLoader.getInstance().discoverPluginsDirectory(); + // Set up initial connection ClientBuilder builder = new ClientBuilder(); if(Configuration.isConnectionToken()) builder.withToken(Configuration.CONNECTION_TOKEN); diff --git a/src/main/java/net/pingex/dcf/core/Configuration.java b/src/main/java/net/pingex/dcf/core/Configuration.java index 4ac348d..b222387 100644 --- a/src/main/java/net/pingex/dcf/core/Configuration.java +++ b/src/main/java/net/pingex/dcf/core/Configuration.java @@ -9,6 +9,7 @@ import org.apache.commons.configuration2.builder.fluent.Parameters; import org.apache.commons.configuration2.ex.ConfigurationException; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import java.io.File; /** * Configuration manager @@ -78,9 +79,14 @@ public class Configuration */ public static String CONNECTION_PASSWORD = null; + /** + * Plugins directory + */ + public static String PLUGINS_DIR = "plugins"; + /** * Tells if the bot is configured to connect using a token or an username/password tuple. - * @return + * @return Whether the main connection is a bot, or not */ public static boolean isConnectionToken() { @@ -93,7 +99,9 @@ public class Configuration public static void init() { BOT_NAME = store.getString("general.bot_name", BOT_NAME); + PLUGINS_DIR = store.getString("generel.plugins_dir", PLUGINS_DIR); + // Validate main connection username/password or token if(isConnectionToken()) CONNECTION_TOKEN = store.getString("discord.token"); // We imply that the token exists, as its existence is already checked in isConnectionToken() else @@ -106,5 +114,10 @@ public class Configuration CONNECTION_USERNAME = store.getString("discord.username"); CONNECTION_PASSWORD = store.getString("discord.password"); } + + // Validate plugins directory + File testingPath = new File(PLUGINS_DIR); + if(!testingPath.exists()) LOGGER.error("Specified plugins path doesn't exist."); + else if(!testingPath.isDirectory()) LOGGER.error("Specified plugins directory is not a directory."); } } diff --git a/src/main/java/net/pingex/dcf/modularity/IPlugin.java b/src/main/java/net/pingex/dcf/modularity/IPlugin.java new file mode 100644 index 0000000..1340cde --- /dev/null +++ b/src/main/java/net/pingex/dcf/modularity/IPlugin.java @@ -0,0 +1,30 @@ +package net.pingex.dcf.modularity; + +import org.apache.commons.configuration2.ImmutableConfiguration; + +/** + * Basic declaration of a plugin + */ +public interface IPlugin +{ + /** + * Load basic parameters, instantiate some objects, load configuration + * @param configuration Configuration from dcf.properties + */ + void load(ImmutableConfiguration configuration); + + /** + * Power up objects, shedule tasks, and so on + */ + void run(); + + /** + * Stop everything, doesn't mean we'll unload the plugin + */ + void stop(); + + /** + * Unload the plugin, clean up, etc + */ + void unload(); +} diff --git a/src/main/java/net/pingex/dcf/modularity/Plugin.java b/src/main/java/net/pingex/dcf/modularity/Plugin.java new file mode 100644 index 0000000..24dded3 --- /dev/null +++ b/src/main/java/net/pingex/dcf/modularity/Plugin.java @@ -0,0 +1,29 @@ +package net.pingex.dcf.modularity; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation containing some details about annotated {@code IPlugin}. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface Plugin +{ + /** + * Unique plugin id. + */ + String id(); + + /** + * Version of the plugin. + */ + String version(); + + /** + * Plugin short description + */ + String description() default "No description provided."; +} diff --git a/src/main/java/net/pingex/dcf/modularity/PluginLoader.java b/src/main/java/net/pingex/dcf/modularity/PluginLoader.java new file mode 100644 index 0000000..5f892e7 --- /dev/null +++ b/src/main/java/net/pingex/dcf/modularity/PluginLoader.java @@ -0,0 +1,130 @@ +package net.pingex.dcf.modularity; + +import net.pingex.dcf.core.Configuration; +import org.apache.commons.io.FilenameUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import java.io.IOException; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.HashMap; +import java.util.Map; +import java.util.jar.Attributes; +import java.util.jar.JarFile; + +/** + * Class that load the plugins, a very powerful thingy. + */ +public class PluginLoader +{ + /** + * Singleton instance + */ + private static PluginLoader INSTANCE = new PluginLoader(); + + /** + * Logger + */ + private static Logger LOGGER = LogManager.getLogger(PluginLoader.class); + + /** + * All plugins + */ + Map plugins; + + /** + * Gives the singleton instance + * @return The singleton instance + */ + public static PluginLoader getInstance() + { + return INSTANCE; + } + + /** + * Enforce singleton with a private constructor. + */ + private PluginLoader() + { + plugins = new HashMap<>(); + } + + /** + * Discover all plugins contained in .jar files in the plugins directory. + */ + public void discoverPluginsDirectory() + { + LOGGER.debug("Discovering all IPlugin classes in plugins directory."); + + try + { + Files.walk(Paths.get(Configuration.PLUGINS_DIR)) + .filter(Files::isRegularFile) + .filter(path -> FilenameUtils.getExtension(path.toString()).equals("jar")) + .forEach(path -> { + try + { + loadJarFile(path); + } + catch(IOException e) + { + LOGGER.catching(e); + } + }); + } + catch(IOException e) + { + LOGGER.error("IO error while discovering plugins.", e); + } + } + + /** + * Look in a .jar file for IPlugins. + */ + public void loadJarFile(Path target) throws IOException + { + LOGGER.debug("Looking up {} for plugins.", target.getFileName()); + + // Get DCF-Plugins manifest attribute + Attributes jarAttributes = new JarFile(target.toFile()).getManifest().getMainAttributes(); + LOGGER.trace("DCF-Plugins attribute: {}", jarAttributes.getValue("DCF-Plugins")); + if(jarAttributes.getValue("DCF-Plugins") == null) + { + LOGGER.debug("Jar file has no `DCF-Plugins` attribute in its manifest. Ignoring archive."); + return; + } + + String[] pluginsToLoad = jarAttributes.getValue("DCF-Plugins").split(" "); + LOGGER.debug("Manifest returned {} plugins to load.", pluginsToLoad.length); + URLClassLoader classLoader = URLClassLoader.newInstance(new URL[]{target.toUri().toURL()}); + + for(String item : pluginsToLoad) + { + LOGGER.debug("Attempting to load {}.", item); + + Class clazz; + try + { + clazz = Class.forName(item, true, classLoader); + } + catch(ClassNotFoundException e) // Class with that name doesn't exist. + { + LOGGER.debug("No class with name {} was found.", item); + return; + } + + // Class doesn't implement IPlugin, or doesn't have @Plugin annotation. + if(!clazz.isAnnotationPresent(Plugin.class) || !IPlugin.class.isAssignableFrom(clazz)) + { + LOGGER.debug("Class does not implement IPlugin or doesn't gave @Plugin annotation."); + return; + } + + LOGGER.debug("Plugin {} {} preloaded.", clazz.getAnnotation(Plugin.class).id(), clazz.getAnnotation(Plugin.class).version()); + plugins.put(clazz.getAnnotation(Plugin.class).id(), new PluginWrapper(clazz.asSubclass(IPlugin.class), classLoader)); + } + } +} \ No newline at end of file diff --git a/src/main/java/net/pingex/dcf/modularity/PluginState.java b/src/main/java/net/pingex/dcf/modularity/PluginState.java new file mode 100644 index 0000000..2b441d9 --- /dev/null +++ b/src/main/java/net/pingex/dcf/modularity/PluginState.java @@ -0,0 +1,37 @@ +package net.pingex.dcf.modularity; + +/** + * Every possible plugin states + */ +public enum PluginState +{ + /** + * The plugin does not have an associated {@code IPlugin} instance. + */ + VOID, + + /** + * The plugin is unloaded, ie. the loader did not run {@code load()}, or executed {@code unload()} on the associated instance. + */ + UNLOADED, + + /** + * The loader ran {@code load()}. + */ + LOADED, + + /** + * The loader ran {@code run()}. + */ + RUNNING, + + /** + * The plugin has been running once, but has now stopped. + */ + STOPPED, + + /** + * The plugin fucked up somewhere. + */ + FAILED +} diff --git a/src/main/java/net/pingex/dcf/modularity/PluginWrapper.java b/src/main/java/net/pingex/dcf/modularity/PluginWrapper.java new file mode 100644 index 0000000..c5a3ba9 --- /dev/null +++ b/src/main/java/net/pingex/dcf/modularity/PluginWrapper.java @@ -0,0 +1,104 @@ +package net.pingex.dcf.modularity; + +import java.net.URLClassLoader; + +/** + * A class wrapping a {@code IPlugin} class. + */ +public class PluginWrapper +{ + /** + * Unique ID of the plugin. + */ + private String id; + + /** + * Version of this plugin. + */ + private String version; + + /** + * Plugin short description + */ + private String description; + + /** + * Matching plugin class. + */ + private Class pluginClass; + + /** + * The associated class loader. + */ + private URLClassLoader classLoader; + + /** + * The plugin main instance. + */ + private IPlugin instance; + + /** + * The current state of this plugin. + */ + private PluginState state; + + public PluginWrapper(Class pluginClass, URLClassLoader classLoader) + { + this.id = pluginClass.getAnnotation(Plugin.class).id(); + this.version = pluginClass.getAnnotation(Plugin.class).version(); + this.description = pluginClass.getAnnotation(Plugin.class).description(); + this.pluginClass = pluginClass; + this.classLoader = classLoader; + this.instance = null; + this.state = PluginState.VOID; + } + + // GETTERS + + public String getId() + { + return id; + } + + public String getVersion() + { + return version; + } + + public String getDescription() + { + return description; + } + + public Class getPluginClass() + { + return pluginClass; + } + + public URLClassLoader getClassLoader() + { + return classLoader; + } + + public IPlugin getInstance() + { + return instance; + } + + public PluginState getState() + { + return state; + } + + // SETTERS + + public void setInstance(IPlugin instance) + { + this.instance = instance; + } + + public void setState(PluginState state) + { + this.state = state; + } +} diff --git a/src/main/java/net/pingex/dcf/modularity/package-info.java b/src/main/java/net/pingex/dcf/modularity/package-info.java new file mode 100644 index 0000000..e105c0c --- /dev/null +++ b/src/main/java/net/pingex/dcf/modularity/package-info.java @@ -0,0 +1,4 @@ +/** + * Contains all classes needed to load modules + */ +package net.pingex.dcf.modularity; \ No newline at end of file