Basic modularity system: plugin preloading.

keep-around/d31701866686f66088b78de2e29736ae36e55a68
Pingex aka Raphaël 9 years ago
parent 10376954f9
commit 107bf41a76

1
.gitignore vendored

@ -70,3 +70,4 @@ gradle-app.setting
# gradle/wrapper/gradle-wrapper.properties
dcf.properties
plugins/

@ -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

@ -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);

@ -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.");
}
}

@ -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();
}

@ -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.";
}

@ -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<String, PluginWrapper> 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));
}
}
}

@ -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
}

@ -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<? extends IPlugin> 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<? extends IPlugin> 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<? extends IPlugin> 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;
}
}

@ -0,0 +1,4 @@
/**
* Contains all classes needed to load modules
*/
package net.pingex.dcf.modularity;