package net.minestom.server.extensions; import net.minestom.server.extras.selfmodification.MinestomExtensionClassLoader; 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.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; /** Points to sponge mixin config in resources folder. */ private String mixinConfig; /** People who have made this extension. */ private String[] authors; /** All code modifiers (the classes they point to) */ private String[] codeModifiers; /** List of extension names that this depends on. */ private String[] dependencies; /** List of Repositories and URLs that this depends on. */ private ExternalDependencies externalDependencies; /** A list of any missing code modifiers to be used for logging. */ private final List missingCodeModifiers = new LinkedList<>(); /** If this extension couldn't load its mixin configuration. */ private boolean failedToLoadMixin = false; /** 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; /** The class loader that powers it. */ transient private MinestomExtensionClassLoader minestomExtensionClassLoader; @NotNull public String getName() { return name; } @NotNull public String getEntrypoint() { return entrypoint; } @NotNull public String getVersion() { return version; } @NotNull public String getMixinConfig() { return mixinConfig; } @NotNull public String[] getAuthors() { return authors; } @NotNull public String[] getCodeModifiers() { if (codeModifiers == null) { codeModifiers = new String[0]; } return codeModifiers; } @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 MinestomExtensionClassLoader removeMinestomExtensionClassLoader() { MinestomExtensionClassLoader oldClassLoader = getMinestomExtensionClassLoader(); setMinestomExtensionClassLoader(null); return oldClassLoader; } public void setMinestomExtensionClassLoader(@Nullable MinestomExtensionClassLoader minestomExtensionClassLoader) { this.minestomExtensionClassLoader = minestomExtensionClassLoader; } @Nullable public MinestomExtensionClassLoader getMinestomExtensionClassLoader() { return this.minestomExtensionClassLoader; } /** * Ensures that all properties of this extension are properly set if they aren't * * TODO this is an impure function. * * @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.mixinConfig == null) { extension.mixinConfig = ""; } if (extension.authors == null) { extension.authors = new String[0]; } if (extension.codeModifiers == null) { extension.codeModifiers = 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(); } } public void addMissingCodeModifier(String codeModifierClass) { missingCodeModifiers.add(codeModifierClass); } public void setFailedToLoadMixinFlag() { failedToLoadMixin = true; } public List getMissingCodeModifiers() { return missingCodeModifiers; } public boolean hasFailedToLoadMixin() { return failedToLoadMixin; } /** * 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 = ""; } } }