diff --git a/api/src/main/java/net/luckperms/api/event/extension/ExtensionLoadEvent.java b/api/src/main/java/net/luckperms/api/event/extension/ExtensionLoadEvent.java new file mode 100644 index 000000000..006e65314 --- /dev/null +++ b/api/src/main/java/net/luckperms/api/event/extension/ExtensionLoadEvent.java @@ -0,0 +1,47 @@ +/* + * This file is part of LuckPerms, licensed under the MIT License. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.luckperms.api.event.extension; + +import net.luckperms.api.event.LuckPermsEvent; +import net.luckperms.api.event.Param; +import net.luckperms.api.extension.Extension; + +import org.checkerframework.checker.nullness.qual.NonNull; + +/** + * Called when an {@link Extension} is loaded. + */ +public interface ExtensionLoadEvent extends LuckPermsEvent { + + /** + * Gets the extension that was loaded. + * + * @return the extension + */ + @Param(0) + @NonNull Extension getExtension(); + +} diff --git a/api/src/main/java/net/luckperms/api/extension/Extension.java b/api/src/main/java/net/luckperms/api/extension/Extension.java new file mode 100644 index 000000000..ad92a5f97 --- /dev/null +++ b/api/src/main/java/net/luckperms/api/extension/Extension.java @@ -0,0 +1,50 @@ +/* + * This file is part of LuckPerms, licensed under the MIT License. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.luckperms.api.extension; + +import net.luckperms.api.LuckPerms; + +/** + * Represents a simple extension "plugin" for LuckPerms. + * + *

Yes, that's right. A plugin for a plugin.

+ * + *

Extensions should either declare a no-arg constructor, or a constructor + * that accepts a single {@link LuckPerms} parameter as it's only argument.

+ */ +public interface Extension { + + /** + * Loads the extension. + */ + void load(); + + /** + * Unloads the extension. + */ + void unload(); + +} diff --git a/api/src/main/java/net/luckperms/api/extension/ExtensionManager.java b/api/src/main/java/net/luckperms/api/extension/ExtensionManager.java new file mode 100644 index 000000000..b0954b8df --- /dev/null +++ b/api/src/main/java/net/luckperms/api/extension/ExtensionManager.java @@ -0,0 +1,61 @@ +/* + * This file is part of luckperms, licensed under the MIT License. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.luckperms.api.extension; + +import org.checkerframework.checker.nullness.qual.NonNull; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Collection; + +/** + * Manages extensions. + */ +public interface ExtensionManager { + + /** + * Loads the given extension. + * + * @param extension the extension to load + */ + void loadExtension(Extension extension); + + /** + * Loads the extension at the given path. + * + * @param path the path to the extension + * @throws IOException if the extension could not be loaded + */ + @NonNull Extension loadExtension(Path path) throws IOException; + + /** + * Gets a collection of all loaded extensions. + * + * @return the loaded extensions + */ + @NonNull Collection getLoadedExtensions(); + +} diff --git a/common/src/main/java/me/lucko/luckperms/common/event/EventFactory.java b/common/src/main/java/me/lucko/luckperms/common/event/EventFactory.java index 61fac60b5..f258f0321 100644 --- a/common/src/main/java/me/lucko/luckperms/common/event/EventFactory.java +++ b/common/src/main/java/me/lucko/luckperms/common/event/EventFactory.java @@ -48,6 +48,7 @@ import net.luckperms.api.event.Cancellable; import net.luckperms.api.event.LuckPermsEvent; import net.luckperms.api.event.cause.CreationCause; import net.luckperms.api.event.cause.DeletionCause; +import net.luckperms.api.event.extension.ExtensionLoadEvent; import net.luckperms.api.event.group.GroupCacheLoadEvent; import net.luckperms.api.event.group.GroupCreateEvent; import net.luckperms.api.event.group.GroupDataRecalculateEvent; @@ -82,6 +83,7 @@ import net.luckperms.api.event.user.UserFirstLoginEvent; import net.luckperms.api.event.user.UserLoadEvent; import net.luckperms.api.event.user.track.UserDemoteEvent; import net.luckperms.api.event.user.track.UserPromoteEvent; +import net.luckperms.api.extension.Extension; import net.luckperms.api.model.DataType; import net.luckperms.api.model.PlayerSaveResult; import net.luckperms.api.node.Node; @@ -133,6 +135,10 @@ public final class EventFactory { return (T) GeneratedEventSpec.lookup(eventClass).newInstance(this.eventBus.getApiProvider(), params); } + public void handleExtensionLoad(Extension extension) { + post(ExtensionLoadEvent.class, () -> generate(ExtensionLoadEvent.class, extension)); + } + public void handleGroupCacheLoad(Group group, GroupCachedDataManager data) { post(GroupCacheLoadEvent.class, () -> generate(GroupCacheLoadEvent.class, group.getApiDelegate(), data)); } diff --git a/common/src/main/java/me/lucko/luckperms/common/extension/SimpleExtensionManager.java b/common/src/main/java/me/lucko/luckperms/common/extension/SimpleExtensionManager.java new file mode 100644 index 000000000..bcf03e4e5 --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/extension/SimpleExtensionManager.java @@ -0,0 +1,183 @@ +/* + * This file is part of LuckPerms, licensed under the MIT License. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package me.lucko.luckperms.common.extension; + +import com.google.gson.JsonElement; + +import me.lucko.luckperms.common.plugin.LuckPermsPlugin; +import me.lucko.luckperms.common.util.gson.GsonProvider; + +import net.luckperms.api.LuckPerms; +import net.luckperms.api.extension.Extension; +import net.luckperms.api.extension.ExtensionManager; + +import org.checkerframework.checker.nullness.qual.NonNull; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class SimpleExtensionManager implements ExtensionManager, AutoCloseable { + private final LuckPermsPlugin plugin; + private final Set extensions = new HashSet<>(); + + public SimpleExtensionManager(LuckPermsPlugin plugin) { + this.plugin = plugin; + } + + @Override + public void close() { + for (LoadedExtension extension : this.extensions) { + try { + extension.instance.unload(); + if (extension.classLoader != null) { + extension.classLoader.close(); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + @Override + public void loadExtension(Extension extension) { + if (this.extensions.stream().anyMatch(e -> e.instance.equals(extension))) { + return; + } + this.extensions.add(new LoadedExtension(extension, null, null)); + extension.load(); + this.plugin.getEventFactory().handleExtensionLoad(extension); + } + + public void loadExtensions(Path directory) { + if (!Files.exists(directory) || !Files.isDirectory(directory)) { + return; + } + + try (Stream stream = Files.list(directory)) { + stream.forEach(path -> { + try { + loadExtension(path); + } catch (IOException e) { + e.printStackTrace(); + } + }); + } catch (IOException e) { + e.printStackTrace(); + } + } + + @Override + public @NonNull Extension loadExtension(Path path) throws IOException { + if (this.extensions.stream().anyMatch(e -> path.equals(e.path))) { + throw new IllegalStateException("Extension at path " + path.toString() + " already loaded."); + } + + if (!Files.exists(path)) { + throw new NoSuchFileException("No file at " + path); + } + + URLClassLoader classLoader = new URLClassLoader(new URL[]{path.toUri().toURL()}, getClass().getClassLoader()); + String className; + + try (InputStream in = classLoader.getResourceAsStream("extension.json")) { + if (in == null) { + throw new RuntimeException("extension.json not present in " + path.toString()); + } + try (BufferedReader reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8))) { + JsonElement parsed = GsonProvider.parser().parse(reader); + className = parsed.getAsJsonObject().get("class").getAsString(); + } + } + + if (className == null) { + throw new IllegalArgumentException("class is null"); + } + + Class extensionClass; + try { + extensionClass = classLoader.loadClass(className).asSubclass(Extension.class); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + + Extension extension = null; + + try { + Constructor constructor = extensionClass.getConstructor(LuckPerms.class); + extension = constructor.newInstance(this.plugin.getApiProvider()); + } catch (NoSuchMethodException e) { + // ignore + } catch (IllegalAccessException | InstantiationException | InvocationTargetException e) { + throw new RuntimeException(e); + } + + if (extension == null) { + try { + Constructor constructor = extensionClass.getConstructor(); + extension = constructor.newInstance(); + } catch (NoSuchMethodException | IllegalAccessException | InstantiationException | InvocationTargetException e) { + throw new RuntimeException(e); + } + } + + this.extensions.add(new LoadedExtension(extension, classLoader, path)); + extension.load(); + this.plugin.getEventFactory().handleExtensionLoad(extension); + return extension; + } + + @Override + public @NonNull Collection getLoadedExtensions() { + return this.extensions.stream().map(e -> e.instance).collect(Collectors.toSet()); + } + + private static final class LoadedExtension { + private final Extension instance; + private final URLClassLoader classLoader; + private final Path path; + + private LoadedExtension(Extension instance, URLClassLoader classLoader, Path path) { + this.instance = instance; + this.classLoader = classLoader; + this.path = path; + } + } +} diff --git a/common/src/main/java/me/lucko/luckperms/common/plugin/AbstractLuckPermsPlugin.java b/common/src/main/java/me/lucko/luckperms/common/plugin/AbstractLuckPermsPlugin.java index 2dcae61af..b55bb89fb 100644 --- a/common/src/main/java/me/lucko/luckperms/common/plugin/AbstractLuckPermsPlugin.java +++ b/common/src/main/java/me/lucko/luckperms/common/plugin/AbstractLuckPermsPlugin.java @@ -38,6 +38,7 @@ import me.lucko.luckperms.common.dependencies.Dependency; import me.lucko.luckperms.common.dependencies.DependencyManager; import me.lucko.luckperms.common.event.AbstractEventBus; import me.lucko.luckperms.common.event.EventFactory; +import me.lucko.luckperms.common.extension.SimpleExtensionManager; import me.lucko.luckperms.common.inheritance.InheritanceHandler; import me.lucko.luckperms.common.locale.LocaleManager; import me.lucko.luckperms.common.locale.message.Message; @@ -82,6 +83,7 @@ public abstract class AbstractLuckPermsPlugin implements LuckPermsPlugin { private CalculatorFactory calculatorFactory; private LuckPermsApiProvider apiProvider; private EventFactory eventFactory; + private SimpleExtensionManager extensionManager; /** * Performs the initial actions to load the plugin @@ -166,6 +168,10 @@ public abstract class AbstractLuckPermsPlugin implements LuckPermsPlugin { ApiRegistrationUtil.registerProvider(this.apiProvider); registerApiOnPlatform(this.apiProvider); + // setup extension manager + this.extensionManager = new SimpleExtensionManager(this); + this.extensionManager.loadExtensions(getBootstrap().getConfigDirectory().resolve("extensions")); + // schedule update tasks int mins = getConfiguration().get(ConfigKeys.SYNC_TIME); if (mins > 0) { @@ -194,6 +200,9 @@ public abstract class AbstractLuckPermsPlugin implements LuckPermsPlugin { this.permissionRegistry.stop(); this.verboseHandler.stop(); + // unload extensions + this.extensionManager.close(); + // remove any hooks into the platform removePlatformHooks(); @@ -332,6 +341,11 @@ public abstract class AbstractLuckPermsPlugin implements LuckPermsPlugin { return this.apiProvider; } + @Override + public SimpleExtensionManager getExtensionManager() { + return this.extensionManager; + } + @Override public EventFactory getEventFactory() { return this.eventFactory; diff --git a/common/src/main/java/me/lucko/luckperms/common/plugin/LuckPermsPlugin.java b/common/src/main/java/me/lucko/luckperms/common/plugin/LuckPermsPlugin.java index 3ac8a705a..3fffa7eab 100644 --- a/common/src/main/java/me/lucko/luckperms/common/plugin/LuckPermsPlugin.java +++ b/common/src/main/java/me/lucko/luckperms/common/plugin/LuckPermsPlugin.java @@ -34,6 +34,7 @@ import me.lucko.luckperms.common.config.LuckPermsConfiguration; import me.lucko.luckperms.common.context.ContextManager; import me.lucko.luckperms.common.dependencies.DependencyManager; import me.lucko.luckperms.common.event.EventFactory; +import me.lucko.luckperms.common.extension.SimpleExtensionManager; import me.lucko.luckperms.common.inheritance.InheritanceHandler; import me.lucko.luckperms.common.locale.LocaleManager; import me.lucko.luckperms.common.messaging.InternalMessagingService; @@ -146,6 +147,13 @@ public interface LuckPermsPlugin { */ LuckPermsApiProvider getApiProvider(); + /** + * Gets the extension manager. + * + * @return the extension manager + */ + SimpleExtensionManager getExtensionManager(); + /** * Gets the command manager *