cn.nukkit.plugin.PluginManager Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of powernukkit Show documentation
Show all versions of powernukkit Show documentation
A Minecraft Bedrock Edition server software implementation made in Java from scratch which supports all new features.
package cn.nukkit.plugin;
import cn.nukkit.Server;
import cn.nukkit.api.PowerNukkitDifference;
import cn.nukkit.api.PowerNukkitOnly;
import cn.nukkit.api.Since;
import cn.nukkit.command.PluginCommand;
import cn.nukkit.command.SimpleCommandMap;
import cn.nukkit.event.*;
import cn.nukkit.permission.Permissible;
import cn.nukkit.permission.Permission;
import cn.nukkit.utils.PluginException;
import cn.nukkit.utils.Utils;
import co.aikar.timings.Timing;
import co.aikar.timings.Timings;
import io.netty.util.internal.EmptyArrays;
import lombok.extern.log4j.Log4j2;
import java.io.File;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.*;
import java.util.regex.Pattern;
/**
* @author MagicDroidX
*/
@Log4j2
public class PluginManager {
private final Server server;
private final SimpleCommandMap commandMap;
protected final Map plugins = new LinkedHashMap<>();
protected final Map permissions = new HashMap<>();
protected final Map defaultPerms = new HashMap<>();
protected final Map defaultPermsOp = new HashMap<>();
protected final Map> permSubs = new HashMap<>();
protected final Set defSubs = Collections.newSetFromMap(new WeakHashMap<>());
protected final Set defSubsOp = Collections.newSetFromMap(new WeakHashMap<>());
protected final Map fileAssociations = new HashMap<>();
public PluginManager(Server server, SimpleCommandMap commandMap) {
this.server = server;
this.commandMap = commandMap;
}
public Plugin getPlugin(String name) {
if (this.plugins.containsKey(name)) {
return this.plugins.get(name);
}
return null;
}
public boolean registerInterface(Class extends PluginLoader> loaderClass) {
if (loaderClass != null) {
try {
Constructor constructor = loaderClass.getDeclaredConstructor(Server.class);
constructor.setAccessible(true);
this.fileAssociations.put(loaderClass.getName(), (PluginLoader) constructor.newInstance(this.server));
return true;
} catch (Exception e) {
return false;
}
}
return false;
}
@PowerNukkitOnly
@Since("1.3.0.0-PN")
public void loadPowerNukkitPlugins() {
PluginLoader pluginLoader = fileAssociations.get(JavaPluginLoader.class.getName());
PowerNukkitPlugin plugin = PowerNukkitPlugin.getInstance();
Map info = new HashMap<>();
info.put("name", "PowerNukkit");
info.put("version", server.getNukkitVersion());
info.put("website", "https://github.com/PowerNukkit/PowerNukkit");
info.put("main", PowerNukkitPlugin.class.getName());
File file;
try {
file = new File(Server.class.getProtectionDomain().getCodeSource().getLocation().toURI());
} catch (Exception e) {
file = new File(".");
}
PluginDescription description = new PluginDescription(info);
plugin.init(pluginLoader, server, description, new File("PowerNukkit"), file);
plugins.put(description.getName(), plugin);
enablePlugin(plugin);
}
public Map getPlugins() {
return plugins;
}
public Plugin loadPlugin(String path) {
return this.loadPlugin(path, null);
}
public Plugin loadPlugin(File file) {
return this.loadPlugin(file, null);
}
public Plugin loadPlugin(String path, Map loaders) {
return this.loadPlugin(new File(path), loaders);
}
public Plugin loadPlugin(File file, Map loaders) {
for (PluginLoader loader : (loaders == null ? this.fileAssociations : loaders).values()) {
for (Pattern pattern : loader.getPluginFilters()) {
if (pattern.matcher(file.getName()).matches()) {
PluginDescription description = loader.getPluginDescription(file);
if (description != null) {
try {
Plugin plugin = loader.loadPlugin(file);
if (plugin != null) {
this.plugins.put(plugin.getDescription().getName(), plugin);
List pluginCommands = this.parseYamlCommands(plugin);
if (!pluginCommands.isEmpty()) {
this.commandMap.registerAll(plugin.getDescription().getName(), pluginCommands);
}
return plugin;
}
} catch (Exception e) {
log.fatal("Could not load plugin", e);
return null;
}
}
}
}
}
return null;
}
public Map loadPlugins(String dictionary) {
return this.loadPlugins(new File(dictionary));
}
public Map loadPlugins(File dictionary) {
return this.loadPlugins(dictionary, null);
}
public Map loadPlugins(String dictionary, List newLoaders) {
return this.loadPlugins(new File(dictionary), newLoaders);
}
public Map loadPlugins(File dictionary, List newLoaders) {
return this.loadPlugins(dictionary, newLoaders, false);
}
public Map loadPlugins(File dictionary, List newLoaders, boolean includeDir) {
if (dictionary.isDirectory()) {
Map plugins = new LinkedHashMap<>();
Map loadedPlugins = new LinkedHashMap<>();
Map> dependencies = new LinkedHashMap<>();
Map> softDependencies = new LinkedHashMap<>();
Map loaders = new LinkedHashMap<>();
if (newLoaders != null) {
for (String key : newLoaders) {
if (this.fileAssociations.containsKey(key)) {
loaders.put(key, this.fileAssociations.get(key));
}
}
} else {
loaders = this.fileAssociations;
}
for (final PluginLoader loader : loaders.values()) {
for (File file : dictionary.listFiles((dir, name) -> {
for (Pattern pattern : loader.getPluginFilters()) {
if (pattern.matcher(name).matches()) {
return true;
}
}
return false;
})) {
if (file.isDirectory() && !includeDir) {
continue;
}
try {
PluginDescription description = loader.getPluginDescription(file);
if (description != null) {
String name = description.getName();
if (plugins.containsKey(name) || this.getPlugin(name) != null) {
log.error(this.server.getLanguage().translateString("nukkit.plugin.duplicateError", name));
continue;
}
boolean compatible = false;
for (String version : description.getCompatibleAPIs()) {
try {
//Check the format: majorVersion.minorVersion.patch
if (!Pattern.matches("^[0-9]+\\.[0-9]+\\.[0-9]+$", version)) {
throw new IllegalArgumentException("The getCompatibleAPI version don't match the format majorVersion.minorVersion.patch");
}
} catch (NullPointerException | IllegalArgumentException e) {
log.error(this.server.getLanguage().translateString("nukkit.plugin.loadError", new String[]{name, "Wrong API format"}), e);
continue;
}
String[] versionArray = version.split("\\.");
String[] apiVersion = this.server.getApiVersion().split("\\.");
//Completely different API version
if (!Objects.equals(Integer.valueOf(versionArray[0]), Integer.valueOf(apiVersion[0]))) {
continue;
}
//If the plugin requires new API features, being backwards compatible
if (Integer.parseInt(versionArray[1]) > Integer.parseInt(apiVersion[1])) {
continue;
}
compatible = true;
break;
}
if (!compatible) {
log.error(this.server.getLanguage().translateString("nukkit.plugin.loadError", new String[]{name, "%nukkit.plugin.incompatibleAPI"}));
}
plugins.put(name, file);
softDependencies.put(name, description.getSoftDepend());
dependencies.put(name, description.getDepend());
for (String before : description.getLoadBefore()) {
if (softDependencies.containsKey(before)) {
softDependencies.get(before).add(name);
} else {
List list = new ArrayList<>();
list.add(name);
softDependencies.put(before, list);
}
}
}
} catch (Exception e) {
log.error(this.server.getLanguage().translateString("nukkit.plugin" +
".fileError", file.getName(), dictionary.toString(), Utils
.getExceptionMessage(e)), e);
}
}
}
while (!plugins.isEmpty()) {
boolean missingDependency = true;
for (String name : new ArrayList<>(plugins.keySet())) {
File file = plugins.get(name);
if (dependencies.containsKey(name)) {
for (String dependency : new ArrayList<>(dependencies.get(name))) {
if (loadedPlugins.containsKey(dependency) || this.getPlugin(dependency) != null) {
dependencies.get(name).remove(dependency);
} else if (!plugins.containsKey(dependency)) {
log.fatal(this.server.getLanguage().translateString("nukkit" +
".plugin.loadError", name, "%nukkit.plugin.unknownDependency", dependency));
break;
}
}
if (dependencies.get(name).isEmpty()) {
dependencies.remove(name);
}
}
if (softDependencies.containsKey(name)) {
softDependencies.get(name).removeIf(dependency ->
loadedPlugins.containsKey(dependency) || this.getPlugin(dependency) != null);
if (softDependencies.get(name).isEmpty()) {
softDependencies.remove(name);
}
}
if (!dependencies.containsKey(name) && !softDependencies.containsKey(name)) {
plugins.remove(name);
missingDependency = false;
Plugin plugin = this.loadPlugin(file, loaders);
if (plugin != null) {
loadedPlugins.put(name, plugin);
} else {
log.fatal(this.server.getLanguage().translateString("nukkit.plugin.genericLoadError", name));
}
}
}
if (missingDependency) {
for (String name : new ArrayList<>(plugins.keySet())) {
File file = plugins.get(name);
if (!dependencies.containsKey(name)) {
softDependencies.remove(name);
plugins.remove(name);
missingDependency = false;
Plugin plugin = this.loadPlugin(file, loaders);
if (plugin != null) {
loadedPlugins.put(name, plugin);
} else {
log.fatal(this.server.getLanguage().translateString("nukkit.plugin.genericLoadError", name));
}
}
}
if (missingDependency) {
for (String name : plugins.keySet()) {
log.fatal(this.server.getLanguage().translateString("nukkit.plugin.loadError", new String[]{name, "%nukkit.plugin.circularDependency"}));
}
plugins.clear();
}
}
}
return loadedPlugins;
} else {
return new HashMap<>();
}
}
public Permission getPermission(String name) {
if (this.permissions.containsKey(name)) {
return this.permissions.get(name);
}
return null;
}
public boolean addPermission(Permission permission) {
if (!this.permissions.containsKey(permission.getName())) {
this.permissions.put(permission.getName(), permission);
this.calculatePermissionDefault(permission);
return true;
}
return false;
}
public void removePermission(String name) {
this.permissions.remove(name);
}
public void removePermission(Permission permission) {
this.removePermission(permission.getName());
}
public Map getDefaultPermissions(boolean op) {
if (op) {
return this.defaultPermsOp;
} else {
return this.defaultPerms;
}
}
public void recalculatePermissionDefaults(Permission permission) {
if (this.permissions.containsKey(permission.getName())) {
this.defaultPermsOp.remove(permission.getName());
this.defaultPerms.remove(permission.getName());
this.calculatePermissionDefault(permission);
}
}
private void calculatePermissionDefault(Permission permission) {
Timings.permissionDefaultTimer.startTiming();
if (permission.getDefault().equals(Permission.DEFAULT_OP) || permission.getDefault().equals(Permission.DEFAULT_TRUE)) {
this.defaultPermsOp.put(permission.getName(), permission);
this.dirtyPermissibles(true);
}
if (permission.getDefault().equals(Permission.DEFAULT_NOT_OP) || permission.getDefault().equals(Permission.DEFAULT_TRUE)) {
this.defaultPerms.put(permission.getName(), permission);
this.dirtyPermissibles(false);
}
Timings.permissionDefaultTimer.startTiming();
}
private void dirtyPermissibles(boolean op) {
for (Permissible p : this.getDefaultPermSubscriptions(op)) {
p.recalculatePermissions();
}
}
public void subscribeToPermission(String permission, Permissible permissible) {
if (!this.permSubs.containsKey(permission)) {
this.permSubs.put(permission, Collections.newSetFromMap(new WeakHashMap<>()));
}
this.permSubs.get(permission).add(permissible);
}
public void unsubscribeFromPermission(String permission, Permissible permissible) {
if (this.permSubs.containsKey(permission)) {
this.permSubs.get(permission).remove(permissible);
if (this.permSubs.get(permission).size() == 0) {
this.permSubs.remove(permission);
}
}
}
public Set getPermissionSubscriptions(String permission) {
if (this.permSubs.containsKey(permission)) {
return new HashSet<>(this.permSubs.get(permission));
}
return new HashSet<>();
}
public void subscribeToDefaultPerms(boolean op, Permissible permissible) {
if (op) {
this.defSubsOp.add(permissible);
} else {
this.defSubs.add(permissible);
}
}
public void unsubscribeFromDefaultPerms(boolean op, Permissible permissible) {
if (op) {
this.defSubsOp.remove(permissible);
} else {
this.defSubs.remove(permissible);
}
}
public Set getDefaultPermSubscriptions(boolean op) {
if (op) {
return new HashSet<>(this.defSubsOp);
} else {
return new HashSet<>(this.defSubs);
}
}
public Map getPermissions() {
return permissions;
}
public boolean isPluginEnabled(Plugin plugin) {
if (plugin != null && this.plugins.containsKey(plugin.getDescription().getName())) {
return plugin.isEnabled();
} else {
return false;
}
}
public void enablePlugin(Plugin plugin) {
if (!plugin.isEnabled()) {
try {
for (Permission permission : plugin.getDescription().getPermissions()) {
this.addPermission(permission);
}
plugin.getPluginLoader().enablePlugin(plugin);
} catch (Throwable e) {
log.fatal("An error occurred while enabling the plugin {}, {}, {}",
plugin.getDescription().getName(), plugin.getDescription().getVersion(), plugin.getDescription().getMain(), e);
this.disablePlugin(plugin);
}
}
}
protected List parseYamlCommands(Plugin plugin) {
List pluginCmds = new ArrayList<>();
for (Map.Entry entry : plugin.getDescription().getCommands().entrySet()) {
String key = (String) entry.getKey();
Object data = entry.getValue();
if (key.contains(":")) {
log.fatal(this.server.getLanguage().translateString("nukkit.plugin.commandError", new String[]{key, plugin.getDescription().getFullName()}));
continue;
}
if (data instanceof Map) {
PluginCommand newCmd = new PluginCommand<>(key, plugin);
if (((Map) data).containsKey("description")) {
newCmd.setDescription((String) ((Map) data).get("description"));
}
if (((Map) data).containsKey("usage")) {
newCmd.setUsage((String) ((Map) data).get("usage"));
}
if (((Map) data).containsKey("aliases")) {
Object aliases = ((Map) data).get("aliases");
if (aliases instanceof List) {
List aliasList = new ArrayList<>();
for (String alias : (List) aliases) {
if (alias.contains(":")) {
log.fatal(this.server.getLanguage().translateString("nukkit.plugin.aliasError", new String[]{alias, plugin.getDescription().getFullName()}));
continue;
}
aliasList.add(alias);
}
newCmd.setAliases(aliasList.toArray(EmptyArrays.EMPTY_STRINGS));
}
}
if (((Map) data).containsKey("permission")) {
newCmd.setPermission((String) ((Map) data).get("permission"));
}
if (((Map) data).containsKey("permission-message")) {
newCmd.setPermissionMessage((String) ((Map) data).get("permission-message"));
}
pluginCmds.add(newCmd);
}
}
return pluginCmds;
}
@PowerNukkitDifference(info = "Makes sure the PowerNukkitPlugin is never disabled", since = "1.3.0.0-PN")
public void disablePlugins() {
ListIterator plugins = new ArrayList<>(this.getPlugins().values()).listIterator(this.getPlugins().size());
while (plugins.hasPrevious()) {
Plugin previous = plugins.previous();
if (previous != PowerNukkitPlugin.getInstance()) {
this.disablePlugin(previous);
}
}
}
public void disablePlugin(Plugin plugin) {
if (PowerNukkitPlugin.getInstance() == plugin) {
throw new UnsupportedOperationException("The PowerNukkit plugin can't be disabled.");
}
if (plugin.isEnabled()) {
try {
plugin.getPluginLoader().disablePlugin(plugin);
} catch (Exception e) {
log.fatal("An error occurred while disabling the plugin {}, {}, {}",
plugin.getDescription().getName(), plugin.getDescription().getVersion(), plugin.getDescription().getMain(), e);
}
this.server.getScheduler().cancelTask(plugin);
HandlerList.unregisterAll(plugin);
for (Permission permission : plugin.getDescription().getPermissions()) {
this.removePermission(permission);
}
}
}
public void clearPlugins() {
this.disablePlugins();
this.plugins.clear();
this.fileAssociations.clear();
this.permissions.clear();
this.defaultPerms.clear();
this.defaultPermsOp.clear();
}
public void callEvent(Event event) {
try {
for (RegisteredListener registration : getEventListeners(event.getClass()).getRegisteredListeners()) {
if (!registration.getPlugin().isEnabled()) {
continue;
}
try {
registration.callEvent(event);
} catch (Exception e) {
log.error(this.server.getLanguage().translateString("nukkit.plugin.eventError", event.getEventName(), registration.getPlugin().getDescription().getFullName(), e.getMessage(), registration.getListener().getClass().getName()), e);
}
}
} catch (IllegalAccessException e) {
log.error("An error has occurred while calling the event {}", event, e);
}
}
public void registerEvents(Listener listener, Plugin plugin) {
if (!plugin.isEnabled()) {
throw new PluginException("Plugin attempted to register " + listener.getClass().getName() + " while not enabled");
}
Map, Set> ret = new HashMap<>();
Set methods;
try {
Method[] publicMethods = listener.getClass().getMethods();
Method[] privateMethods = listener.getClass().getDeclaredMethods();
methods = new HashSet<>(publicMethods.length + privateMethods.length, 1.0f);
Collections.addAll(methods, publicMethods);
Collections.addAll(methods, privateMethods);
} catch (NoClassDefFoundError e) {
plugin.getLogger().error("Plugin " + plugin.getDescription().getFullName() + " has failed to register events for " + listener.getClass() + " because " + e.getMessage() + " does not exist.");
return;
}
for (final Method method : methods) {
final EventHandler eh = method.getAnnotation(EventHandler.class);
if (eh == null) continue;
if (method.isBridge() || method.isSynthetic()) {
continue;
}
final Class> checkClass;
if (method.getParameterTypes().length != 1 || !Event.class.isAssignableFrom(checkClass = method.getParameterTypes()[0])) {
plugin.getLogger().error(plugin.getDescription().getFullName() + " attempted to register an invalid EventHandler method signature \"" + method.toGenericString() + "\" in " + listener.getClass());
continue;
}
final Class extends Event> eventClass = checkClass.asSubclass(Event.class);
method.setAccessible(true);
for (Class> clazz = eventClass; Event.class.isAssignableFrom(clazz); clazz = clazz.getSuperclass()) {
// This loop checks for extending deprecated events
if (clazz.getAnnotation(Deprecated.class) != null) {
if (Boolean.parseBoolean(String.valueOf(this.server.getConfig("settings.deprecated-verbpse", true)))) {
log.warn(this.server.getLanguage().translateString("nukkit.plugin.deprecatedEvent", plugin.getName(), clazz.getName(), listener.getClass().getName() + "." + method.getName() + "()"));
}
break;
}
}
this.registerEvent(eventClass, listener, eh.priority(), new MethodEventExecutor(method), plugin, eh.ignoreCancelled());
}
}
public void registerEvent(Class extends Event> event, Listener listener, EventPriority priority, EventExecutor executor, Plugin plugin) throws PluginException {
this.registerEvent(event, listener, priority, executor, plugin, false);
}
public void registerEvent(Class extends Event> event, Listener listener, EventPriority priority, EventExecutor executor, Plugin plugin, boolean ignoreCancelled) throws PluginException {
if (!plugin.isEnabled()) {
throw new PluginException("Plugin attempted to register " + event + " while not enabled");
}
try {
Timing timing = Timings.getPluginEventTiming(event, listener, executor, plugin);
this.getEventListeners(event).register(new RegisteredListener(listener, executor, priority, plugin, ignoreCancelled, timing));
} catch (IllegalAccessException e) {
log.error("An error occurred while registering the event listener event:{}, listener:{} for plugin:{} version:{}",
event, listener, plugin.getDescription().getName(), plugin.getDescription().getVersion(), e);
}
}
private HandlerList getEventListeners(Class extends Event> type) throws IllegalAccessException {
try {
Method method = getRegistrationClass(type).getDeclaredMethod("getHandlers");
method.setAccessible(true);
return (HandlerList) method.invoke(null);
} catch (NullPointerException e) {
throw new IllegalArgumentException("getHandlers method in " + type.getName() + " was not static!", e);
} catch (Exception e) {
IllegalAccessException illegalAccessException = new IllegalAccessException(Utils.getExceptionMessage(e));
illegalAccessException.addSuppressed(e);
throw illegalAccessException;
}
}
private Class extends Event> getRegistrationClass(Class extends Event> clazz) throws IllegalAccessException {
try {
clazz.getDeclaredMethod("getHandlers");
return clazz;
} catch (NoSuchMethodException e) {
if (clazz.getSuperclass() != null
&& !clazz.getSuperclass().equals(Event.class)
&& Event.class.isAssignableFrom(clazz.getSuperclass())) {
return getRegistrationClass(clazz.getSuperclass().asSubclass(Event.class));
} else {
throw new IllegalAccessException("Unable to find handler list for event " + clazz.getName() + ". Static getHandlers method required!");
}
}
}
}