From 29155580042db0d13e96022b92c17074bed8202b Mon Sep 17 00:00:00 2001 From: mworzala Date: Sun, 28 Aug 2022 03:52:30 +0300 Subject: [PATCH] hollow-cube/no-extensions Signed-off-by: mworzala remove extension stuff from demo (cherry picked from commit 6052e726d03d6a27dd11962134328ad474ce45a9) remove extensions (cherry picked from commit 40ba24e43b6eb0f8869d80c30bd47d799c82a094) (cherry picked from commit f4f9a905f74e7bf67fc2af341a25b9e85933abbe) --- .../demo/commands/LoadExtensionCommand.java | 69 -- .../demo/extension/TestExtension.java | 21 - demo/src/main/resources/extension.json | 13 - .../net/minestom/server/MinecraftServer.java | 5 - .../net/minestom/server/ServerProcess.java | 6 - .../minestom/server/ServerProcessImpl.java | 17 - .../extensions/DiscoveredExtension.java | 236 ------ .../minestom/server/extensions/Extension.java | 193 ----- .../extensions/ExtensionClassLoader.java | 91 --- .../server/extensions/ExtensionManager.java | 699 ------------------ .../query/response/FullQueryResponse.java | 12 - 11 files changed, 1362 deletions(-) delete mode 100644 demo/src/main/java/net/minestom/demo/commands/LoadExtensionCommand.java delete mode 100644 demo/src/main/java/net/minestom/demo/extension/TestExtension.java delete mode 100644 demo/src/main/resources/extension.json delete mode 100644 src/main/java/net/minestom/server/extensions/DiscoveredExtension.java delete mode 100644 src/main/java/net/minestom/server/extensions/Extension.java delete mode 100644 src/main/java/net/minestom/server/extensions/ExtensionClassLoader.java delete mode 100644 src/main/java/net/minestom/server/extensions/ExtensionManager.java diff --git a/demo/src/main/java/net/minestom/demo/commands/LoadExtensionCommand.java b/demo/src/main/java/net/minestom/demo/commands/LoadExtensionCommand.java deleted file mode 100644 index f617c818d..000000000 --- a/demo/src/main/java/net/minestom/demo/commands/LoadExtensionCommand.java +++ /dev/null @@ -1,69 +0,0 @@ -package net.minestom.demo.commands; - -import net.kyori.adventure.text.Component; -import net.minestom.server.MinecraftServer; -import net.minestom.server.command.CommandSender; -import net.minestom.server.command.builder.Command; -import net.minestom.server.command.builder.CommandContext; -import net.minestom.server.command.builder.arguments.ArgumentString; -import net.minestom.server.command.builder.arguments.ArgumentType; -import net.minestom.server.command.builder.exception.ArgumentSyntaxException; -import net.minestom.server.extensions.ExtensionManager; - -import java.io.IOException; -import java.nio.file.Path; - -public class LoadExtensionCommand extends Command { - - private final ArgumentString extensionName; - - public LoadExtensionCommand() { - super("load"); - - setDefaultExecutor(this::usage); - - extensionName = ArgumentType.String("extensionName"); - - setArgumentCallback(this::extensionCallback, extensionName); - addSyntax(this::execute, extensionName); - } - - private void usage(CommandSender sender, CommandContext context) { - sender.sendMessage(Component.text("Usage: /load ")); - } - - private void execute(CommandSender sender, CommandContext context) { - final String name = context.get(extensionName); - sender.sendMessage(Component.text("extensionFile = " + name + "....")); - - ExtensionManager extensionManager = MinecraftServer.getExtensionManager(); - Path extensionFolder = extensionManager.getExtensionFolder().toPath().toAbsolutePath(); - Path extensionJar = extensionFolder.resolve(name); - try { - if (!extensionJar.toFile().getCanonicalPath().startsWith(extensionFolder.toFile().getCanonicalPath())) { - sender.sendMessage(Component.text("File name '" + name + "' does not represent a file inside the extensions folder. Will not load")); - return; - } - } catch (IOException e) { - e.printStackTrace(); - sender.sendMessage(Component.text("Failed to load extension: " + e.getMessage())); - return; - } - - try { - boolean managed = extensionManager.loadDynamicExtension(extensionJar.toFile()); - if (managed) { - sender.sendMessage(Component.text("Extension loaded!")); - } else { - sender.sendMessage(Component.text("Failed to load extension, check your logs.")); - } - } catch (Exception e) { - e.printStackTrace(); - sender.sendMessage(Component.text("Failed to load extension: " + e.getMessage())); - } - } - - private void extensionCallback(CommandSender sender, ArgumentSyntaxException exception) { - sender.sendMessage(Component.text("'" + exception.getInput() + "' is not a valid extension name!")); - } -} diff --git a/demo/src/main/java/net/minestom/demo/extension/TestExtension.java b/demo/src/main/java/net/minestom/demo/extension/TestExtension.java deleted file mode 100644 index 053b0edc1..000000000 --- a/demo/src/main/java/net/minestom/demo/extension/TestExtension.java +++ /dev/null @@ -1,21 +0,0 @@ -package net.minestom.demo.extension; - -import net.minestom.server.extensions.Extension; - -public class TestExtension extends Extension { - @Override - public void initialize() { - System.out.println("Initialize test extension"); - - try { - Class.forName("com.mysql.cj.jdbc.Driver", true, getOrigin().getClassLoader()); - } catch (ClassNotFoundException e) { - e.printStackTrace(); - } - } - - @Override - public void terminate() { - System.out.println("Terminate test extension"); - } -} diff --git a/demo/src/main/resources/extension.json b/demo/src/main/resources/extension.json deleted file mode 100644 index efb46608f..000000000 --- a/demo/src/main/resources/extension.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "entrypoint": "net.minestom.demo.extension.TestExtension", - "name": "TestExtension", - "version": "1.0.0", - "externalDependencies": { - "repositories": [ - { "name": "Central", "url": "https://repo1.maven.org/maven2/" } - ], - "artifacts": [ - "mysql:mysql-connector-java:8.0.26" - ] - } -} \ No newline at end of file diff --git a/src/main/java/net/minestom/server/MinecraftServer.java b/src/main/java/net/minestom/server/MinecraftServer.java index 059ebc19b..3d1c47063 100644 --- a/src/main/java/net/minestom/server/MinecraftServer.java +++ b/src/main/java/net/minestom/server/MinecraftServer.java @@ -6,7 +6,6 @@ import net.minestom.server.adventure.bossbar.BossBarManager; import net.minestom.server.command.CommandManager; import net.minestom.server.event.GlobalEventHandler; import net.minestom.server.exception.ExceptionManager; -import net.minestom.server.extensions.ExtensionManager; import net.minestom.server.gamedata.tags.TagManager; import net.minestom.server.instance.InstanceManager; import net.minestom.server.instance.block.BlockManager; @@ -298,10 +297,6 @@ public final class MinecraftServer { return serverProcess.advancement(); } - public static ExtensionManager getExtensionManager() { - return serverProcess.extension(); - } - public static TagManager getTagManager() { return serverProcess.tag(); } diff --git a/src/main/java/net/minestom/server/ServerProcess.java b/src/main/java/net/minestom/server/ServerProcess.java index 89ec44a64..bf677eafb 100644 --- a/src/main/java/net/minestom/server/ServerProcess.java +++ b/src/main/java/net/minestom/server/ServerProcess.java @@ -5,7 +5,6 @@ import net.minestom.server.adventure.bossbar.BossBarManager; import net.minestom.server.command.CommandManager; import net.minestom.server.event.GlobalEventHandler; import net.minestom.server.exception.ExceptionManager; -import net.minestom.server.extensions.ExtensionManager; import net.minestom.server.gamedata.tags.TagManager; import net.minestom.server.instance.Chunk; import net.minestom.server.instance.InstanceManager; @@ -96,11 +95,6 @@ public interface ServerProcess extends Snapshotable { */ @NotNull BossBarManager bossBar(); - /** - * Loads and handle extensions. - */ - @NotNull ExtensionManager extension(); - /** * Handles registry tags. */ diff --git a/src/main/java/net/minestom/server/ServerProcessImpl.java b/src/main/java/net/minestom/server/ServerProcessImpl.java index 761fef7e5..cbd509f54 100644 --- a/src/main/java/net/minestom/server/ServerProcessImpl.java +++ b/src/main/java/net/minestom/server/ServerProcessImpl.java @@ -9,7 +9,6 @@ import net.minestom.server.event.EventDispatcher; import net.minestom.server.event.GlobalEventHandler; import net.minestom.server.event.server.ServerTickMonitorEvent; import net.minestom.server.exception.ExceptionManager; -import net.minestom.server.extensions.ExtensionManager; import net.minestom.server.gamedata.tags.TagManager; import net.minestom.server.instance.Chunk; import net.minestom.server.instance.Instance; @@ -47,7 +46,6 @@ final class ServerProcessImpl implements ServerProcess { private final static Logger LOGGER = LoggerFactory.getLogger(ServerProcessImpl.class); private final ExceptionManager exception; - private final ExtensionManager extension; private final ConnectionManager connection; private final PacketProcessor packetProcessor; private final PacketListenerManager packetListener; @@ -74,7 +72,6 @@ final class ServerProcessImpl implements ServerProcess { public ServerProcessImpl() throws IOException { this.exception = new ExceptionManager(); - this.extension = new ExtensionManager(this); this.connection = new ConnectionManager(); this.packetProcessor = new PacketProcessor(); this.packetListener = new PacketListenerManager(this); @@ -162,11 +159,6 @@ final class ServerProcessImpl implements ServerProcess { return bossBar; } - @Override - public @NotNull ExtensionManager extension() { - return extension; - } - @Override public @NotNull TagManager tag() { return tag; @@ -208,13 +200,8 @@ final class ServerProcessImpl implements ServerProcess { throw new IllegalStateException("Server already started"); } - extension.start(); - extension.gotoPreInit(); - LOGGER.info("Starting " + MinecraftServer.getBrandName() + " server."); - extension.gotoInit(); - // Init server try { server.init(socketAddress); @@ -226,8 +213,6 @@ final class ServerProcessImpl implements ServerProcess { // Start server server.start(); - extension.gotoPostInit(); - LOGGER.info(MinecraftServer.getBrandName() + " server started successfully."); if (MinecraftServer.isTerminalEnabled()) { @@ -242,8 +227,6 @@ final class ServerProcessImpl implements ServerProcess { if (!stopped.compareAndSet(false, true)) return; LOGGER.info("Stopping " + MinecraftServer.getBrandName() + " server."); - LOGGER.info("Unloading all extensions."); - extension.shutdown(); scheduler.shutdown(); connection.shutdown(); server.stop(); diff --git a/src/main/java/net/minestom/server/extensions/DiscoveredExtension.java b/src/main/java/net/minestom/server/extensions/DiscoveredExtension.java deleted file mode 100644 index 1239990db..000000000 --- a/src/main/java/net/minestom/server/extensions/DiscoveredExtension.java +++ /dev/null @@ -1,236 +0,0 @@ -package net.minestom.server.extensions; - -import com.google.gson.JsonObject; -import net.minestom.server.utils.validate.Check; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.File; -import java.net.URL; -import java.nio.file.Path; -import java.util.LinkedList; -import java.util.List; - -/** - * Represents an extension from an `extension.json` that is capable of powering an Extension object. - * - * This has no constructor as its properties are set via GSON. - */ -public final class DiscoveredExtension { - /** Static logger for this class. */ - public static final Logger LOGGER = LoggerFactory.getLogger(DiscoveredExtension.class); - - /** The regex that this name must pass. If it doesn't, it will not be accepted. */ - public static final String NAME_REGEX = "[A-Za-z][_A-Za-z0-9]+"; - - /** Name of the DiscoveredExtension. Unique for all extensions. */ - private String name; - - /** Main class of this DiscoveredExtension, must extend Extension. */ - private String entrypoint; - - /** Version of this extension, highly reccomended to set it. */ - private String version; - - /** People who have made this extension. */ - private String[] authors; - - /** List of extension names that this depends on. */ - private String[] dependencies; - - /** List of Repositories and URLs that this depends on. */ - private ExternalDependencies externalDependencies; - - /** - * Extra meta on the object. - * Do NOT use as configuration: - * - * Meta is meant to handle properties that will - * be accessed by other extensions, not accessed by itself - */ - private JsonObject meta; - - /** All files of this extension */ - transient List files = new LinkedList<>(); - - /** The load status of this extension -- LOAD_SUCCESS is the only good one. */ - transient LoadStatus loadStatus = LoadStatus.LOAD_SUCCESS; - - /** The original jar this is from. */ - transient private File originalJar; - - transient private Path dataDirectory; - - /** The class loader that powers it. */ - transient private ExtensionClassLoader classLoader; - - @NotNull - public String getName() { - return name; - } - - @NotNull - public String getEntrypoint() { - return entrypoint; - } - - @NotNull - public String getVersion() { - return version; - } - - @NotNull - public String[] getAuthors() { - return authors; - } - - @NotNull - public String[] getDependencies() { - return dependencies; - } - - @NotNull - public ExternalDependencies getExternalDependencies() { - return externalDependencies; - } - - public void setOriginalJar(@Nullable File file) { - originalJar = file; - } - - @Nullable - public File getOriginalJar() { - return originalJar; - } - - public @NotNull Path getDataDirectory() { - return dataDirectory; - } - - public void setDataDirectory(@NotNull Path dataDirectory) { - this.dataDirectory = dataDirectory; - } - - void createClassLoader() { - Check.stateCondition(classLoader != null, "Extension classloader has already been created"); - final URL[] urls = this.files.toArray(new URL[0]); - classLoader = new ExtensionClassLoader(this.getName(), urls, this); - } - - @NotNull - public ExtensionClassLoader getClassLoader() { - return classLoader; - } - - /** - * Ensures that all properties of this extension are properly set if they aren't - * - * @param extension The extension to verify - */ - public static void verifyIntegrity(@NotNull DiscoveredExtension extension) { - if (extension.name == null) { - StringBuilder fileList = new StringBuilder(); - for (URL f : extension.files) { - fileList.append(f.toExternalForm()).append(", "); - } - LOGGER.error("Extension with no name. (at {}})", fileList); - LOGGER.error("Extension at ({}) will not be loaded.", fileList); - extension.loadStatus = DiscoveredExtension.LoadStatus.INVALID_NAME; - - // To ensure @NotNull: name = INVALID_NAME - extension.name = extension.loadStatus.name(); - return; - } - - if (!extension.name.matches(NAME_REGEX)) { - LOGGER.error("Extension '{}' specified an invalid name.", extension.name); - LOGGER.error("Extension '{}' will not be loaded.", extension.name); - extension.loadStatus = DiscoveredExtension.LoadStatus.INVALID_NAME; - - // To ensure @NotNull: name = INVALID_NAME - extension.name = extension.loadStatus.name(); - return; - } - - if (extension.entrypoint == null) { - LOGGER.error("Extension '{}' did not specify an entry point (via 'entrypoint').", extension.name); - LOGGER.error("Extension '{}' will not be loaded.", extension.name); - extension.loadStatus = DiscoveredExtension.LoadStatus.NO_ENTRYPOINT; - - // To ensure @NotNull: entrypoint = NO_ENTRYPOINT - extension.entrypoint = extension.loadStatus.name(); - return; - } - - // Handle defaults - // If we reach this code, then the extension will most likely be loaded: - if (extension.version == null) { - LOGGER.warn("Extension '{}' did not specify a version.", extension.name); - LOGGER.warn("Extension '{}' will continue to load but should specify a plugin version.", extension.name); - extension.version = "Unspecified"; - } - - if (extension.authors == null) { - extension.authors = new String[0]; - } - - // No dependencies were specified - if (extension.dependencies == null) { - extension.dependencies = new String[0]; - } - - // No external dependencies were specified; - if (extension.externalDependencies == null) { - extension.externalDependencies = new ExternalDependencies(); - } - - // No meta was provided - if (extension.meta == null) { - extension.meta = new JsonObject(); - } - - } - - @NotNull - public JsonObject getMeta() { - return meta; - } - - /** - * The status this extension has, all are breakpoints. - * - * LOAD_SUCCESS is the only valid one. - */ - enum LoadStatus { - LOAD_SUCCESS("Actually, it did not fail. This message should not have been printed."), - MISSING_DEPENDENCIES("Missing dependencies, check your logs."), - INVALID_NAME("Invalid name."), - NO_ENTRYPOINT("No entrypoint specified."), - FAILED_TO_SETUP_CLASSLOADER("Extension classloader could not be setup."), - LOAD_FAILED("Load failed. See logs for more information."), - ; - - private final String message; - - LoadStatus(@NotNull String message) { - this.message = message; - } - - @NotNull - public String getMessage() { - return message; - } - } - - public static final class ExternalDependencies { - Repository[] repositories = new Repository[0]; - String[] artifacts = new String[0]; - - public static class Repository { - String name = ""; - String url = ""; - } - } -} diff --git a/src/main/java/net/minestom/server/extensions/Extension.java b/src/main/java/net/minestom/server/extensions/Extension.java deleted file mode 100644 index 020f229cb..000000000 --- a/src/main/java/net/minestom/server/extensions/Extension.java +++ /dev/null @@ -1,193 +0,0 @@ -package net.minestom.server.extensions; - -import net.kyori.adventure.text.logger.slf4j.ComponentLogger; -import net.minestom.server.event.Event; -import net.minestom.server.event.EventNode; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.io.IOException; -import java.io.InputStream; -import java.net.URL; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.StandardCopyOption; -import java.util.HashSet; -import java.util.Set; - -public abstract class Extension { - /** - * List of extensions that depend on this extension. - */ - protected final Set dependents = new HashSet<>(); - - protected Extension() { - - } - - public void preInitialize() { - - } - - public abstract void initialize(); - - public void postInitialize() { - - } - - public void preTerminate() { - - } - - public abstract void terminate(); - - public void postTerminate() { - - } - - ExtensionClassLoader getExtensionClassLoader() { - if (getClass().getClassLoader() instanceof ExtensionClassLoader extensionClassLoader) { - return extensionClassLoader; - } - throw new IllegalStateException("Extension class loader is not an ExtensionClassLoader"); - } - - @NotNull - public DiscoveredExtension getOrigin() { - return getExtensionClassLoader().getDiscoveredExtension(); - } - - /** - * Gets the logger for the extension - * - * @return The logger for the extension - */ - @NotNull - public ComponentLogger getLogger() { - return getExtensionClassLoader().getLogger(); - } - - public @NotNull EventNode getEventNode() { - return getExtensionClassLoader().getEventNode(); - } - - public @NotNull Path getDataDirectory() { - return getOrigin().getDataDirectory(); - } - - /** - * Gets a resource from the extension directory, or from inside the jar if it does not - * exist in the extension directory. - *

- * If it does not exist in the extension directory, it will be copied from inside the jar. - *

- * The caller is responsible for closing the returned {@link InputStream}. - * - * @param fileName The file to read - * @return The file contents, or null if there was an issue reading the file. - */ - public @Nullable InputStream getResource(@NotNull String fileName) { - return getResource(Paths.get(fileName)); - } - - /** - * Gets a resource from the extension directory, or from inside the jar if it does not - * exist in the extension directory. - *

- * If it does not exist in the extension directory, it will be copied from inside the jar. - *

- * The caller is responsible for closing the returned {@link InputStream}. - * - * @param target The file to read - * @return The file contents, or null if there was an issue reading the file. - */ - public @Nullable InputStream getResource(@NotNull Path target) { - final Path targetFile = getDataDirectory().resolve(target); - try { - // Copy from jar if the file does not exist in the extension data directory. - if (!Files.exists(targetFile)) { - savePackagedResource(target); - } - - return Files.newInputStream(targetFile); - } catch (IOException ex) { - getLogger().info("Failed to read resource {}.", target, ex); - return null; - } - } - - /** - * Gets a resource from inside the extension jar. - *

- * The caller is responsible for closing the returned {@link InputStream}. - * - * @param fileName The file to read - * @return The file contents, or null if there was an issue reading the file. - */ - public @Nullable InputStream getPackagedResource(@NotNull String fileName) { - try { - final URL url = getOrigin().getClassLoader().getResource(fileName); - if (url == null) { - getLogger().debug("Resource not found: {}", fileName); - return null; - } - - return url.openConnection().getInputStream(); - } catch (IOException ex) { - getLogger().debug("Failed to load resource {}.", fileName, ex); - return null; - } - } - - /** - * Gets a resource from inside the extension jar. - *

- * The caller is responsible for closing the returned {@link InputStream}. - * - * @param target The file to read - * @return The file contents, or null if there was an issue reading the file. - */ - public @Nullable InputStream getPackagedResource(@NotNull Path target) { - return getPackagedResource(target.toString().replace('\\', '/')); - } - - /** - * Copies a resource file to the extension directory, replacing any existing copy. - * - * @param fileName The resource to save - * @return True if the resource was saved successfully, null otherwise - */ - public boolean savePackagedResource(@NotNull String fileName) { - return savePackagedResource(Paths.get(fileName)); - } - - /** - * Copies a resource file to the extension directory, replacing any existing copy. - * - * @param target The resource to save - * @return True if the resource was saved successfully, null otherwise - */ - public boolean savePackagedResource(@NotNull Path target) { - final Path targetFile = getDataDirectory().resolve(target); - try (InputStream is = getPackagedResource(target)) { - if (is == null) { - return false; - } - - Files.createDirectories(targetFile.getParent()); - Files.copy(is, targetFile, StandardCopyOption.REPLACE_EXISTING); - return true; - } catch (IOException ex) { - getLogger().debug("Failed to save resource {}.", target, ex); - return false; - } - } - - /** - * @return A modifiable list of dependents. - */ - public Set getDependents() { - return dependents; - } -} diff --git a/src/main/java/net/minestom/server/extensions/ExtensionClassLoader.java b/src/main/java/net/minestom/server/extensions/ExtensionClassLoader.java deleted file mode 100644 index dd96f46af..000000000 --- a/src/main/java/net/minestom/server/extensions/ExtensionClassLoader.java +++ /dev/null @@ -1,91 +0,0 @@ -package net.minestom.server.extensions; - -import net.kyori.adventure.text.logger.slf4j.ComponentLogger; -import net.minestom.server.MinecraftServer; -import net.minestom.server.event.Event; -import net.minestom.server.event.EventNode; -import org.jetbrains.annotations.NotNull; - -import java.io.InputStream; -import java.net.URL; -import java.net.URLClassLoader; -import java.util.ArrayList; -import java.util.List; - -public final class ExtensionClassLoader extends URLClassLoader { - private final List children = new ArrayList<>(); - private final DiscoveredExtension discoveredExtension; - private EventNode eventNode; - private ComponentLogger logger; - - public ExtensionClassLoader(String name, URL[] urls, DiscoveredExtension discoveredExtension) { - super("Ext_" + name, urls, MinecraftServer.class.getClassLoader()); - this.discoveredExtension = discoveredExtension; - } - - public ExtensionClassLoader(String name, URL[] urls, ClassLoader parent, DiscoveredExtension discoveredExtension) { - super("Ext_" + name, urls, parent); - this.discoveredExtension = discoveredExtension; - } - - @Override - public void addURL(@NotNull URL url) { - super.addURL(url); - } - - public void addChild(@NotNull ExtensionClassLoader loader) { - children.add(loader); - } - - @Override - protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { - try { - return super.loadClass(name, resolve); - } catch (ClassNotFoundException e) { - for (ExtensionClassLoader child : children) { - try { - return child.loadClass(name, resolve); - } catch (ClassNotFoundException ignored) {} - } - throw e; - } - } - - public InputStream getResourceAsStreamWithChildren(@NotNull String name) { - InputStream in = getResourceAsStream(name); - if (in != null) return in; - - for (ExtensionClassLoader child : children) { - InputStream childInput = child.getResourceAsStreamWithChildren(name); - if (childInput != null) - return childInput; - } - - return null; - } - - public DiscoveredExtension getDiscoveredExtension() { - return discoveredExtension; - } - - public EventNode getEventNode() { - if (eventNode == null) { - eventNode = EventNode.all(discoveredExtension.getName()); - MinecraftServer.getGlobalEventHandler().addChild(eventNode); - } - return eventNode; - } - - public ComponentLogger getLogger() { - if (logger == null) { - logger = ComponentLogger.logger(discoveredExtension.getName()); - } - return logger; - } - - void terminate() { - if (eventNode != null) { - MinecraftServer.getGlobalEventHandler().removeChild(eventNode); - } - } -} diff --git a/src/main/java/net/minestom/server/extensions/ExtensionManager.java b/src/main/java/net/minestom/server/extensions/ExtensionManager.java deleted file mode 100644 index d664544c8..000000000 --- a/src/main/java/net/minestom/server/extensions/ExtensionManager.java +++ /dev/null @@ -1,699 +0,0 @@ -package net.minestom.server.extensions; - -import com.google.gson.Gson; -import net.minestom.dependencies.DependencyGetter; -import net.minestom.dependencies.ResolvedDependency; -import net.minestom.dependencies.maven.MavenRepository; -import net.minestom.server.ServerProcess; -import net.minestom.server.utils.validate.Check; -import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.*; -import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; -import java.net.URL; -import java.nio.file.Path; -import java.util.*; -import java.util.stream.Collectors; -import java.util.zip.ZipEntry; -import java.util.zip.ZipFile; - -public class ExtensionManager { - - public final static Logger LOGGER = LoggerFactory.getLogger(ExtensionManager.class); - - public final static String INDEV_CLASSES_FOLDER = "minestom.extension.indevfolder.classes"; - public final static String INDEV_RESOURCES_FOLDER = "minestom.extension.indevfolder.resources"; - private final static Gson GSON = new Gson(); - - private final ServerProcess serverProcess; - - // LinkedHashMaps are HashMaps that preserve order - private final Map extensions = new LinkedHashMap<>(); - private final Map immutableExtensions = Collections.unmodifiableMap(extensions); - - private final File extensionFolder = new File(System.getProperty("minestom.extension.folder", "extensions")); - private final File dependenciesFolder = new File(extensionFolder, ".libs"); - private Path extensionDataRoot = extensionFolder.toPath(); - - private enum State {DO_NOT_START, NOT_STARTED, STARTED, PRE_INIT, INIT, POST_INIT} - private State state = State.NOT_STARTED; - - public ExtensionManager(ServerProcess serverProcess) { - this.serverProcess = serverProcess; - } - - /** - * Gets if the extensions should be loaded during startup. - *

- * Default value is 'true'. - * - * @return true if extensions are loaded in {@link net.minestom.server.MinecraftServer#start(java.net.SocketAddress)} - */ - public boolean shouldLoadOnStartup() { - return state != State.DO_NOT_START; - } - - /** - * Used to specify if you want extensions to be loaded and initialized during startup. - *

- * Only useful before the server start. - * - * @param loadOnStartup true to load extensions on startup, false to do nothing - */ - public void setLoadOnStartup(boolean loadOnStartup) { - Check.stateCondition(state.ordinal() > State.NOT_STARTED.ordinal(), "Extensions have already been initialized"); - this.state = loadOnStartup ? State.NOT_STARTED : State.DO_NOT_START; - } - - @NotNull - public File getExtensionFolder() { - return extensionFolder; - } - - public @NotNull Path getExtensionDataRoot() { - return extensionDataRoot; - } - - public void setExtensionDataRoot(@NotNull Path dataRoot) { - this.extensionDataRoot = dataRoot; - } - - @NotNull - public Collection getExtensions() { - return immutableExtensions.values(); - } - - @Nullable - public Extension getExtension(@NotNull String name) { - return extensions.get(name.toLowerCase()); - } - - public boolean hasExtension(@NotNull String name) { - return extensions.containsKey(name); - } - - // - // Init phases - // - - @ApiStatus.Internal - public void start() { - if (state == State.DO_NOT_START) { - LOGGER.warn("Extension loadOnStartup option is set to false, extensions are therefore neither loaded or initialized."); - return; - } - Check.stateCondition(state != State.NOT_STARTED, "ExtensionManager has already been started"); - loadExtensions(); - state = State.STARTED; - } - - @ApiStatus.Internal - public void gotoPreInit() { - if (state == State.DO_NOT_START) return; - Check.stateCondition(state != State.STARTED, "Extensions have already done pre initialization"); - extensions.values().forEach(Extension::preInitialize); - state = State.PRE_INIT; - } - - @ApiStatus.Internal - public void gotoInit() { - if (state == State.DO_NOT_START) return; - Check.stateCondition(state != State.PRE_INIT, "Extensions have already done initialization"); - extensions.values().forEach(Extension::initialize); - state = State.INIT; - } - - @ApiStatus.Internal - public void gotoPostInit() { - if (state == State.DO_NOT_START) return; - Check.stateCondition(state != State.INIT, "Extensions have already done post initialization"); - extensions.values().forEach(Extension::postInitialize); - state = State.POST_INIT; - } - - // - // Loading - // - - /** - * Loads all extensions in the extension folder into this server. - *

- *

- * Pipeline: - *
- * Finds all .jar files in the extensions folder. - *
- * Per each jar: - *
- * Turns its extension.json into a DiscoveredExtension object. - *
- * Verifies that all properties of extension.json are correctly set. - *

- *

- * It then sorts all those jars by their load order (making sure that an extension's dependencies load before it) - *
- * Note: Cyclic dependencies will stop both extensions from being loaded. - *

- *

- * Afterwards, it loads all external dependencies and adds them to the extension's files - *

- *

- * Then removes any invalid extensions (Invalid being its Load Status isn't SUCCESS) - *

- *

- * After that, it set its classloaders so each extension is self-contained, - *

- *

- * Removes invalid extensions again, - *

- *

- * and loads all of those extensions into Minestom - *
- * (Extension fields are set via reflection after each extension is verified, then loaded.) - *

- *

- * If the extension successfully loads, add it to the global extension Map (Name to Extension) - *

- *

- * And finally make a scheduler to clean observers per extension. - */ - private void loadExtensions() { - // Initialize folders - { - // Make extensions folder if necessary - if (!extensionFolder.exists()) { - if (!extensionFolder.mkdirs()) { - LOGGER.error("Could not find or create the extension folder, extensions will not be loaded!"); - return; - } - } - - // Make dependencies folder if necessary - if (!dependenciesFolder.exists()) { - if (!dependenciesFolder.mkdirs()) { - LOGGER.error("Could not find nor create the extension dependencies folder, extensions will not be loaded!"); - return; - } - } - } - - // Load extensions - { - // Get all extensions and order them accordingly. - List discoveredExtensions = discoverExtensions(); - - // Don't waste resources on doing extra actions if there is nothing to do. - if (discoveredExtensions.isEmpty()) return; - - // Create classloaders for each extension (so that they can be used during dependency resolution) - Iterator extensionIterator = discoveredExtensions.iterator(); - while (extensionIterator.hasNext()) { - DiscoveredExtension discoveredExtension = extensionIterator.next(); - try { - discoveredExtension.createClassLoader(); - } catch (Exception e) { - discoveredExtension.loadStatus = DiscoveredExtension.LoadStatus.FAILED_TO_SETUP_CLASSLOADER; - serverProcess.exception().handleException(e); - LOGGER.error("Failed to load extension {}", discoveredExtension.getName()); - LOGGER.error("Failed to load extension", e); - extensionIterator.remove(); - } - } - - discoveredExtensions = generateLoadOrder(discoveredExtensions); - loadDependencies(discoveredExtensions); - - // remove invalid extensions - discoveredExtensions.removeIf(ext -> ext.loadStatus != DiscoveredExtension.LoadStatus.LOAD_SUCCESS); - - // Load the extensions - for (DiscoveredExtension discoveredExtension : discoveredExtensions) { - try { - loadExtension(discoveredExtension); - } catch (Exception e) { - discoveredExtension.loadStatus = DiscoveredExtension.LoadStatus.LOAD_FAILED; - LOGGER.error("Failed to load extension {}", discoveredExtension.getName()); - serverProcess.exception().handleException(e); - } - } - } - } - - public boolean loadDynamicExtension(@NotNull File jarFile) throws FileNotFoundException { - if (!jarFile.exists()) { - throw new FileNotFoundException("File '" + jarFile.getAbsolutePath() + "' does not exists. Cannot load extension."); - } - - LOGGER.info("Discover dynamic extension from jar {}", jarFile.getAbsolutePath()); - DiscoveredExtension discoveredExtension = discoverFromJar(jarFile); - List extensionsToLoad = Collections.singletonList(discoveredExtension); - return loadExtensionList(extensionsToLoad); - } - - /** - * Loads an extension into Minestom. - * - * @param discoveredExtension The extension. Make sure to verify its integrity, set its class loader, and its files. - * @return An extension object made from this DiscoveredExtension - */ - @Nullable - private Extension loadExtension(@NotNull DiscoveredExtension discoveredExtension) { - // Create Extension (authors, version etc.) - String extensionName = discoveredExtension.getName(); - String mainClass = discoveredExtension.getEntrypoint(); - - ExtensionClassLoader loader = discoveredExtension.getClassLoader(); - - if (extensions.containsKey(extensionName.toLowerCase())) { - LOGGER.error("An extension called '{}' has already been registered.", extensionName); - return null; - } - - Class jarClass; - try { - jarClass = Class.forName(mainClass, true, loader); - } catch (ClassNotFoundException e) { - LOGGER.error("Could not find main class '{}' in extension '{}'.", - mainClass, extensionName, e); - return null; - } - - Class extensionClass; - try { - extensionClass = jarClass.asSubclass(Extension.class); - } catch (ClassCastException e) { - LOGGER.error("Main class '{}' in '{}' does not extend the 'Extension' superclass.", mainClass, extensionName, e); - return null; - } - - Constructor constructor; - try { - constructor = extensionClass.getDeclaredConstructor(); - // Let's just make it accessible, plugin creators don't have to make this public. - constructor.setAccessible(true); - } catch (NoSuchMethodException e) { - LOGGER.error("Main class '{}' in '{}' does not define a no-args constructor.", mainClass, extensionName, e); - return null; - } - Extension extension = null; - try { - extension = constructor.newInstance(); - } catch (InstantiationException e) { - LOGGER.error("Main class '{}' in '{}' cannot be an abstract class.", mainClass, extensionName, e); - return null; - } catch (IllegalAccessException ignored) { - // We made it accessible, should not occur - } catch (InvocationTargetException e) { - LOGGER.error( - "While instantiating the main class '{}' in '{}' an exception was thrown.", - mainClass, - extensionName, - e.getTargetException() - ); - return null; - } - - // add dependents to pre-existing extensions, so that they can easily be found during reloading - for (String dependencyName : discoveredExtension.getDependencies()) { - Extension dependency = extensions.get(dependencyName.toLowerCase()); - if (dependency == null) { - LOGGER.warn("Dependency {} of {} is null? This means the extension has been loaded without its dependency, which could cause issues later.", dependencyName, discoveredExtension.getName()); - } else { - dependency.getDependents().add(discoveredExtension.getName()); - } - } - - // add to a linked hash map, as they preserve order - extensions.put(extensionName.toLowerCase(), extension); - - return extension; - } - - /** - * Get all extensions from the extensions folder and make them discovered. - *

- * It skims the extension folder, discovers and verifies each extension, and returns those created DiscoveredExtensions. - * - * @return A list of discovered extensions from this folder. - */ - private @NotNull List discoverExtensions() { - List extensions = new LinkedList<>(); - - File[] fileList = extensionFolder.listFiles(); - - if (fileList != null) { - // Loop through all files in extension folder - for (File file : fileList) { - - // Ignore folders - if (file.isDirectory()) { - continue; - } - - // Ignore non .jar files - if (!file.getName().endsWith(".jar")) { - continue; - } - - DiscoveredExtension extension = discoverFromJar(file); - if (extension != null && extension.loadStatus == DiscoveredExtension.LoadStatus.LOAD_SUCCESS) { - extensions.add(extension); - } - } - } - - //TODO(mattw): Extract this into its own method to load an extension given classes and resources directory. - //TODO(mattw): Should show a warning if one is set and not the other. It is most likely a mistake. - - // this allows developers to have their extension discovered while working on it, without having to build a jar and put in the extension folder - if (System.getProperty(INDEV_CLASSES_FOLDER) != null && System.getProperty(INDEV_RESOURCES_FOLDER) != null) { - LOGGER.info("Found indev folders for extension. Adding to list of discovered extensions."); - final String extensionClasses = System.getProperty(INDEV_CLASSES_FOLDER); - final String extensionResources = System.getProperty(INDEV_RESOURCES_FOLDER); - try (InputStreamReader reader = new InputStreamReader(new FileInputStream(new File(extensionResources, "extension.json")))) { - DiscoveredExtension extension = GSON.fromJson(reader, DiscoveredExtension.class); - extension.files.add(new File(extensionClasses).toURI().toURL()); - extension.files.add(new File(extensionResources).toURI().toURL()); - extension.setDataDirectory(getExtensionDataRoot().resolve(extension.getName())); - - // Verify integrity and ensure defaults - DiscoveredExtension.verifyIntegrity(extension); - - if (extension.loadStatus == DiscoveredExtension.LoadStatus.LOAD_SUCCESS) { - extensions.add(extension); - } - } catch (IOException e) { - serverProcess.exception().handleException(e); - } - } - return extensions; - } - - /** - * Grabs a discovered extension from a jar. - * - * @param file The jar to grab it from (a .jar is a formatted .zip file) - * @return The created DiscoveredExtension. - */ - private @Nullable DiscoveredExtension discoverFromJar(@NotNull File file) { - try (ZipFile f = new ZipFile(file)) { - - ZipEntry entry = f.getEntry("extension.json"); - - if (entry == null) - throw new IllegalStateException("Missing extension.json in extension " + file.getName() + "."); - - InputStreamReader reader = new InputStreamReader(f.getInputStream(entry)); - - // Initialize DiscoveredExtension from GSON. - DiscoveredExtension extension = GSON.fromJson(reader, DiscoveredExtension.class); - extension.setOriginalJar(file); - extension.files.add(file.toURI().toURL()); - extension.setDataDirectory(getExtensionDataRoot().resolve(extension.getName())); - - // Verify integrity and ensure defaults - DiscoveredExtension.verifyIntegrity(extension); - - return extension; - } catch (IOException e) { - serverProcess.exception().handleException(e); - return null; - } - } - - @NotNull - private List generateLoadOrder(@NotNull List discoveredExtensions) { - // Extension --> Extensions it depends on. - Map> dependencyMap = new HashMap<>(); - - // Put dependencies in dependency map - { - Map extensionMap = new HashMap<>(); - - // go through all the discovered extensions and assign their name in a map. - for (DiscoveredExtension discoveredExtension : discoveredExtensions) { - extensionMap.put(discoveredExtension.getName().toLowerCase(), discoveredExtension); - } - - allExtensions: - // go through all the discovered extensions and get their dependencies as extensions - for (DiscoveredExtension discoveredExtension : discoveredExtensions) { - - List dependencies = new ArrayList<>(discoveredExtension.getDependencies().length); - - // Map the dependencies into DiscoveredExtensions. - for (String dependencyName : discoveredExtension.getDependencies()) { - - DiscoveredExtension dependencyExtension = extensionMap.get(dependencyName.toLowerCase()); - // Specifies an extension we don't have. - if (dependencyExtension == null) { - - // attempt to see if it is not already loaded (happens with dynamic (re)loading) - if (extensions.containsKey(dependencyName.toLowerCase())) { - - dependencies.add(extensions.get(dependencyName.toLowerCase()).getOrigin()); - continue; // Go to the next loop in this dependency loop, this iteration is done. - - } else { - - // dependency isn't loaded, move on. - LOGGER.error("Extension {} requires an extension called {}.", discoveredExtension.getName(), dependencyName); - LOGGER.error("However the extension {} could not be found.", dependencyName); - LOGGER.error("Therefore {} will not be loaded.", discoveredExtension.getName()); - discoveredExtension.loadStatus = DiscoveredExtension.LoadStatus.MISSING_DEPENDENCIES; - continue allExtensions; // the above labeled loop will go to the next extension as this dependency is invalid. - - } - } - // This will add null for an unknown-extension - dependencies.add(dependencyExtension); - - } - - dependencyMap.put( - discoveredExtension, - dependencies - ); - - } - } - - // List containing the load order. - LinkedList sortedList = new LinkedList<>(); - - // TODO actually have to read this - { - // entries with empty lists - List>> loadableExtensions; - - // While there are entries with no more elements (no more dependencies) - while (!( - loadableExtensions = dependencyMap.entrySet().stream().filter(entry -> isLoaded(entry.getValue())).toList() - ).isEmpty() - ) { - // Get all "loadable" (not actually being loaded!) extensions and put them in the sorted list. - for (var entry : loadableExtensions) { - // Add to sorted list. - sortedList.add(entry.getKey()); - // Remove to make the next iterations a little quicker (hopefully) and to find cyclic dependencies. - dependencyMap.remove(entry.getKey()); - - // Remove this dependency from all the lists (if they include it) to make way for next level of extensions. - for (var dependencies : dependencyMap.values()) { - dependencies.remove(entry.getKey()); - } - } - } - } - - // Check if there are cyclic extensions. - if (!dependencyMap.isEmpty()) { - LOGGER.error("Minestom found {} cyclic extensions.", dependencyMap.size()); - LOGGER.error("Cyclic extensions depend on each other and can therefore not be loaded."); - for (var entry : dependencyMap.entrySet()) { - DiscoveredExtension discoveredExtension = entry.getKey(); - LOGGER.error("{} could not be loaded, as it depends on: {}.", - discoveredExtension.getName(), - entry.getValue().stream().map(DiscoveredExtension::getName).collect(Collectors.joining(", "))); - } - - } - - return sortedList; - } - - /** - * Checks if this list of extensions are loaded - * - * @param extensions The list of extensions to check against. - * @return If all of these extensions are loaded. - */ - private boolean isLoaded(@NotNull List extensions) { - return - extensions.isEmpty() // Don't waste CPU on checking an empty array - // Make sure the internal extensions list contains all of these. - || extensions.stream().allMatch(ext -> this.extensions.containsKey(ext.getName().toLowerCase())); - } - - private void loadDependencies(@NotNull List extensions) { - List allLoadedExtensions = new LinkedList<>(extensions); - - for (Extension extension : immutableExtensions.values()) - allLoadedExtensions.add(extension.getOrigin()); - - for (DiscoveredExtension discoveredExtension : extensions) { - try { - DependencyGetter getter = new DependencyGetter(); - DiscoveredExtension.ExternalDependencies externalDependencies = discoveredExtension.getExternalDependencies(); - List repoList = new LinkedList<>(); - for (var repository : externalDependencies.repositories) { - - if (repository.name == null || repository.name.isEmpty()) { - throw new IllegalStateException("Missing 'name' element in repository object."); - } - - if (repository.url == null || repository.url.isEmpty()) { - throw new IllegalStateException("Missing 'url' element in repository object."); - } - - repoList.add(new MavenRepository(repository.name, repository.url)); - } - - getter.addMavenResolver(repoList); - - for (String artifact : externalDependencies.artifacts) { - var resolved = getter.get(artifact, dependenciesFolder); - addDependencyFile(resolved, discoveredExtension); - LOGGER.trace("Dependency of extension {}: {}", discoveredExtension.getName(), resolved); - } - - ExtensionClassLoader extensionClassLoader = discoveredExtension.getClassLoader(); - for (String dependencyName : discoveredExtension.getDependencies()) { - var resolved = extensions.stream() - .filter(ext -> ext.getName().equalsIgnoreCase(dependencyName)) - .findFirst() - .orElseThrow(() -> new IllegalStateException("Unknown dependency '" + dependencyName + "' of '" + discoveredExtension.getName() + "'")); - - ExtensionClassLoader dependencyClassLoader = resolved.getClassLoader(); - - extensionClassLoader.addChild(dependencyClassLoader); - LOGGER.trace("Dependency of extension {}: {}", discoveredExtension.getName(), resolved); - } - } catch (Exception e) { - discoveredExtension.loadStatus = DiscoveredExtension.LoadStatus.MISSING_DEPENDENCIES; - LOGGER.error("Failed to load dependencies for extension {}", discoveredExtension.getName()); - LOGGER.error("Extension '{}' will not be loaded", discoveredExtension.getName()); - LOGGER.error("This is the exception", e); - } - } - } - - private void addDependencyFile(@NotNull ResolvedDependency dependency, @NotNull DiscoveredExtension extension) { - URL location = dependency.getContentsLocation(); - extension.files.add(location); - extension.getClassLoader().addURL(location); - LOGGER.trace("Added dependency {} to extension {} classpath", location.toExternalForm(), extension.getName()); - - // recurse to add full dependency tree - if (!dependency.getSubdependencies().isEmpty()) { - LOGGER.trace("Dependency {} has subdependencies, adding...", location.toExternalForm()); - for (ResolvedDependency sub : dependency.getSubdependencies()) { - addDependencyFile(sub, extension); - } - LOGGER.trace("Dependency {} has had its subdependencies added.", location.toExternalForm()); - } - } - - private boolean loadExtensionList(@NotNull List extensionsToLoad) { - // ensure correct order of dependencies - LOGGER.debug("Reorder extensions to ensure proper load order"); - extensionsToLoad = generateLoadOrder(extensionsToLoad); - loadDependencies(extensionsToLoad); - - // setup new classloaders for the extensions to reload - for (DiscoveredExtension toReload : extensionsToLoad) { - LOGGER.debug("Setting up classloader for extension {}", toReload.getName()); -// toReload.setMinestomExtensionClassLoader(toReload.makeClassLoader()); //TODO: Fix this - } - - List newExtensions = new LinkedList<>(); - for (DiscoveredExtension toReload : extensionsToLoad) { - // reload extensions - LOGGER.info("Actually load extension {}", toReload.getName()); - Extension loadedExtension = loadExtension(toReload); - if (loadedExtension != null) { - newExtensions.add(loadedExtension); - } - } - - if (newExtensions.isEmpty()) { - LOGGER.error("No extensions to load, skipping callbacks"); - return false; - } - - LOGGER.info("Load complete, firing preinit, init and then postinit callbacks"); - // retrigger preinit, init and postinit - newExtensions.forEach(Extension::preInitialize); - newExtensions.forEach(Extension::initialize); - newExtensions.forEach(Extension::postInitialize); - return true; - } - - // - // Shutdown / Unload - // - - /** - * Shutdowns all the extensions by unloading them. - */ - public void shutdown() {// copy names, as the extensions map will be modified via the calls to unload - Set extensionNames = new HashSet<>(extensions.keySet()); - for (String ext : extensionNames) { - if (extensions.containsKey(ext)) { // is still loaded? Because extensions can depend on one another, it might have already been unloaded - unloadExtension(ext); - } - } - } - - private void unloadExtension(@NotNull String extensionName) { - Extension ext = extensions.get(extensionName.toLowerCase()); - - if (ext == null) { - throw new IllegalArgumentException("Extension " + extensionName + " is not currently loaded."); - } - - List dependents = new LinkedList<>(ext.getDependents()); // copy dependents list - - for (String dependentID : dependents) { - Extension dependentExt = extensions.get(dependentID.toLowerCase()); - if (dependentExt != null) { // check if extension isn't already unloaded. - LOGGER.info("Unloading dependent extension {} (because it depends on {})", dependentID, extensionName); - unload(dependentExt); - } - } - - LOGGER.info("Unloading extension {}", extensionName); - unload(ext); - } - - private void unload(@NotNull Extension ext) { - ext.preTerminate(); - ext.terminate(); - - ext.getExtensionClassLoader().terminate(); - - ext.postTerminate(); - - // remove from loaded extensions - String id = ext.getOrigin().getName().toLowerCase(); - extensions.remove(id); - - // cleanup classloader - // TODO: Is it necessary to remove the CLs since this is only called on shutdown? - } -} diff --git a/src/main/java/net/minestom/server/extras/query/response/FullQueryResponse.java b/src/main/java/net/minestom/server/extras/query/response/FullQueryResponse.java index 640248039..ee62e17e4 100644 --- a/src/main/java/net/minestom/server/extras/query/response/FullQueryResponse.java +++ b/src/main/java/net/minestom/server/extras/query/response/FullQueryResponse.java @@ -2,7 +2,6 @@ package net.minestom.server.extras.query.response; import net.kyori.adventure.text.serializer.plain.PlainComponentSerializer; import net.minestom.server.MinecraftServer; -import net.minestom.server.extensions.Extension; import net.minestom.server.extras.query.Query; import net.minestom.server.utils.binary.BinaryWriter; import net.minestom.server.utils.binary.Writeable; @@ -123,17 +122,6 @@ public class FullQueryResponse implements Writeable { .append(' ') .append(MinecraftServer.VERSION_NAME); - if (!MinecraftServer.getExtensionManager().getExtensions().isEmpty()) { - for (Extension extension : MinecraftServer.getExtensionManager().getExtensions()) { - builder.append(extension.getOrigin().getName()) - .append(' ') - .append(extension.getOrigin().getVersion()) - .append("; "); - } - - builder.delete(builder.length() - 2, builder.length()); - } - return builder.toString(); }