package io.shockah.skylark.plugin;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
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.ArrayList;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import org.apache.commons.io.IOUtils;
import org.pircbotx.hooks.Listener;
import org.pircbotx.hooks.managers.ListenerManager;
import io.shockah.json.JSONObject;
import io.shockah.json.JSONParser;
import io.shockah.skylark.App;
import io.shockah.skylark.ServerManager;
import io.shockah.skylark.UnexpectedException;
import io.shockah.skylark.util.FileUtils;
import io.shockah.skylark.util.ReadWriteList;
public class PluginManager {
public static final Path LIBS_PATH = Paths.get("libs");
public static final Path PLUGIN_PATH = Paths.get("plugins");
public final App app;
public ClassLoader pluginClassLoader = null;
public ReadWriteList<Plugin.Info> pluginInfos = new ReadWriteList<>(new ArrayList<>());
public ReadWriteList<Plugin> plugins = new ReadWriteList<>(new ArrayList<>());
public ReadWriteList<BotManagerService.Factory> botManagerServiceFactories = new ReadWriteList<>(new ArrayList<>());
public ReadWriteList<BotManagerService> botManagerServices = new ReadWriteList<>(new ArrayList<>());
public ReadWriteList<BotService.Factory> botServiceFactories = new ReadWriteList<>(new ArrayList<>());
public ReadWriteList<BotService> botServices = new ReadWriteList<>(new ArrayList<>());
public PluginManager(App app) {
this.app = app;
}
public Path getLibsPath() {
return LIBS_PATH;
}
public Path getPluginPath() {
return PLUGIN_PATH;
}
public void reload() {
unload();
load();
}
protected void unload() {
plugins.iterate(plugin -> {
plugin.onUnload();
System.out.println(String.format("Unloaded plugin: %s", plugin.info.packageName()));
});
clearListenerPlugins();
clearServices();
plugins.clear();
pluginInfos.clear();
pluginClassLoader = null;
}
protected void load() {
List<Plugin.Info> infos = findPlugins();
infos = dependencySort(infos);
pluginInfos.addAll(infos);
pluginClassLoader = createClassLoader(pluginInfos);
pluginInfos.iterate(pluginInfo -> {
if (shouldEnable(pluginInfo)) {
Plugin plugin = loadPlugin(pluginClassLoader, pluginInfo);
if (plugin != null) {
try {
setupRequiredDependencyFields(plugin);
plugin.onLoad();
plugins.add(plugin);
if (plugin instanceof ListenerPlugin)
setupListenerPlugin((ListenerPlugin)plugin);
setupServices(plugin);
System.out.println(String.format("Loaded plugin: %s", pluginInfo.packageName()));
} catch (Exception e) {
throw new UnexpectedException(e);
}
}
}
});
plugins.iterate(plugin -> {
setupOptionalDependencyFields(plugin);
});
plugins.iterate(plugin -> {
plugin.onAllPluginsLoaded();
});
}
protected Plugin loadPlugin(ClassLoader classLoader, Plugin.Info info) {
try {
Class<?> clazz = classLoader.loadClass(info.baseClass());
Constructor<?> ctor = clazz.getConstructor(PluginManager.class, Plugin.Info.class);
return (Plugin)ctor.newInstance(this, info);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
@SuppressWarnings("unchecked")
protected void setupRequiredDependencyFields(Plugin plugin) {
for (Field field : plugin.getClass().getDeclaredFields()) {
try {
Plugin.Dependency dependencyAnnotation = field.getAnnotation(Plugin.Dependency.class);
if (dependencyAnnotation != null) {
if (dependencyAnnotation.value().equals("")) {
Class<? extends Plugin> clazz = (Class<? extends Plugin>)field.getType();
if (clazz == Plugin.class)
continue;
Plugin dependency = getPluginWithClass(clazz);
if (dependency != null) {
field.setAccessible(true);
field.set(plugin, dependency);
plugin.onDependencyLoaded(plugin);
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
protected void setupOptionalDependencyFields(Plugin plugin) {
for (Field field : plugin.getClass().getDeclaredFields()) {
try {
Plugin.Dependency dependencyAnnotation = field.getAnnotation(Plugin.Dependency.class);
if (dependencyAnnotation != null) {
if (!dependencyAnnotation.value().equals("")) {
Plugin dependency = getPluginWithPackageName(dependencyAnnotation.value());
if (dependency != null) {
field.setAccessible(true);
field.set(plugin, dependency);
plugin.onDependencyLoaded(plugin);
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
protected void setupListenerPlugin(ListenerPlugin plugin) {
app.serverManager.botManagers.iterate(botManager -> {
botManager.bots.iterate(bot -> {
bot.getConfiguration().getListenerManager().addListener(plugin.listener);
});
});
}
protected void clearListenerPlugins() {
app.serverManager.botManagers.iterate(botManager -> {
botManager.bots.iterate(bot -> {
ListenerManager manager = bot.getConfiguration().getListenerManager();
for (Listener listener : manager.getListeners()) {
if (listener instanceof ListenerPlugin.MyListener)
manager.removeListener(listener);
}
});
});
}
protected void setupServices(Plugin plugin) {
if (plugin instanceof BotManagerService.Factory) {
BotManagerService.Factory factory = (BotManagerService.Factory)plugin;
botManagerServiceFactories.add(factory);
ServerManager serverManager = app.serverManager;
serverManager.botManagers.iterate(botManager -> {
BotManagerService service = factory.createService(botManager);
botManager.services.add(service);
botManagerServices.add(service);
});
}
if (plugin instanceof BotService.Factory) {
BotService.Factory factory = (BotService.Factory)plugin;
botServiceFactories.add(factory);
ServerManager serverManager = app.serverManager;
serverManager.botManagers.iterate(botManager -> {
botManager.bots.iterate(bot -> {
BotService service = factory.createService(bot);
bot.services.add(service);
botServices.add(service);
});
});
}
}
protected void clearServices() {
ServerManager serverManager = app.serverManager;
serverManager.botManagers.iterate(botManager -> {
botManager.services.clear();
botManager.bots.iterate(bot -> {
bot.services.clear();
});
});
botManagerServiceFactories.clear();
botManagerServices.clear();
botServiceFactories.clear();
botServices.clear();
}
@SuppressWarnings("unchecked")
public <T extends Plugin> T getPluginWithClass(Class<T> clazz) {
return (T)plugins.filterFirst(plugin -> clazz.isInstance(plugin));
}
@SuppressWarnings("unchecked")
public <T extends Plugin> T getPluginWithPackageName(String name) {
return (T)plugins.filterFirst(plugin -> plugin.info.packageName().equals(name));
}
protected List<Plugin.Info> findPlugins() {
List<Plugin.Info> infos = new ArrayList<>();
try {
for (Path path : Files.newDirectoryStream(getPluginPath(), path -> path.getFileName().toString().endsWith(".jar"))) {
Path tmpPath = FileUtils.copyAsTrueTempFile(path);
try (ZipFile zf = new ZipFile(tmpPath.toFile())) {
ZipEntry ze = zf.getEntry("plugin.json");
if (ze == null)
continue;
JSONObject pluginJson = new JSONParser().parseObject(new String(IOUtils.toByteArray(zf.getInputStream(ze)), "UTF-8"));
infos.add(new Plugin.Info(pluginJson, tmpPath.toUri().toURL()));
} catch (Exception e) {
throw new UnexpectedException(e);
}
}
} catch (Exception e) {
throw new UnexpectedException(e);
}
return infos;
}
protected List<Plugin.Info> dependencySort(List<Plugin.Info> input) {
input = new ArrayList<>(input);
List<Plugin.Info> output = new ArrayList<>(input.size());
List<String> loadedPackageNames = new ArrayList<>(input.size());
while (!input.isEmpty()) {
int oldSize = input.size();
for (int i = 0; i < input.size(); i++) {
Plugin.Info info = input.get(i);
boolean allDependenciesLoaded = true;
for (String dependency : info.dependsOn()) {
if (!loadedPackageNames.contains(dependency)) {
allDependenciesLoaded = false;
break;
}
}
if (allDependenciesLoaded) {
loadedPackageNames.add(info.packageName());
output.add(info);
input.remove(i--);
}
}
if (oldSize == input.size()) {
//TODO: log plugins with missing dependencies (the ones left in $input)
break;
}
}
return output;
}
protected ClassLoader createClassLoader(ReadWriteList<Plugin.Info> infos) {
List<URL> urls = new ArrayList<>();
try {
for (Path path : Files.newDirectoryStream(getLibsPath(), path -> path.getFileName().toString().endsWith(".jar"))) {
Path tmpPath = FileUtils.copyAsTrueTempFile(path);
urls.add(tmpPath.toUri().toURL());
}
} catch (Exception e) {
throw new UnexpectedException(e);
}
infos.iterate(info -> urls.add(info.url));
return new URLClassLoader(urls.toArray(new URL[0]));
}
protected boolean shouldEnable(Plugin.Info info) {
return info.enabledByDefault();
}
}