diff --git a/addons/build.gradle.kts b/addons/build.gradle.kts index cfd0e0f0..de96fe50 100644 --- a/addons/build.gradle.kts +++ b/addons/build.gradle.kts @@ -7,7 +7,6 @@ project.group = "${rootProject.name}.addons" project.version = "0.5.0" dependencies { - compileOnly("ch.qos.logback:logback-classic:1.5.20") - api(project(":fusion-files")) + compileOnly(libs.log4j) } \ No newline at end of file diff --git a/addons/src/main/java/com/ryderbelserion/fusion/addons/AddonClassLoader.java b/addons/src/main/java/com/ryderbelserion/fusion/addons/AddonClassLoader.java deleted file mode 100644 index 5d454e17..00000000 --- a/addons/src/main/java/com/ryderbelserion/fusion/addons/AddonClassLoader.java +++ /dev/null @@ -1,191 +0,0 @@ -package com.ryderbelserion.fusion.addons; - -import com.ryderbelserion.fusion.addons.interfaces.IAddon; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import java.net.MalformedURLException; -import java.net.URL; -import java.net.URLClassLoader; -import java.nio.file.Path; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -/** - * Handles class loaders related to addons. - */ -public class AddonClassLoader extends URLClassLoader { - - private final Map> classes = new ConcurrentHashMap<>(); - - private final AddonManager manager; - private final IAddon addon; - private final Path path; - - private boolean isDisabling; - private String name; - - /** - * Constructs an instance of the addon class loader - * - * @param manager the addon manager - * @param path the path for the addon's directory - * @param group the entry point - * @param name the name of the addon - * @throws IllegalStateException throws {@link IllegalStateException} - * @throws MalformedURLException throws {@link MalformedURLException} - */ - public AddonClassLoader(@NotNull final AddonManager manager, @NotNull final Path path, @NotNull final String group, @NotNull final String name) throws IllegalStateException, MalformedURLException { - super(new URL[]{path.toUri().toURL()}, manager.getClass().getClassLoader()); - - this.manager = manager; - this.name = name.isBlank() ? path.getFileName().toString() : name; - this.path = path; - - Class mainClass; - - try { - mainClass = Class.forName(group, true, this); - - this.classes.put(mainClass.getName(), mainClass); - } catch (final ClassNotFoundException exception) { - throw new IllegalStateException(String.format("Could not find main class for addon %s!", name), exception); - } - - Class addonClass; - - try { - addonClass = mainClass.asSubclass(IAddon.class); - } catch (final Exception exception) { - throw new IllegalStateException(String.format("%s does not implement iAddon!", group), exception); - } - - try { - this.addon = addonClass.getDeclaredConstructor().newInstance(); - this.addon.setLoader(this); - this.addon.setName(this.name = name); - this.addon.setGroup(group); - } catch (final Exception exception) { - throw new IllegalStateException(String.format("Failed to load main class for addon %s!", name), exception); - } - } - - /** - * Finds and loads the class with the specified name from the URL search - * path. Any URLs referring to JAR files are loaded and opened as needed - * until the class is found. - * - * @param name the name of the class - * @return the resulting class - * @throws ClassNotFoundException if the class could not be found, - * or if the loader is closed. - * @throws NullPointerException if {@code name} is {@code null}. - */ - @Override - protected Class findClass(@NotNull final String name) throws ClassNotFoundException { - return findClass(name, true); - } - - /** - * Finds and loads the class with the specified name from the URL search - * path. Any URLs referring to JAR files are loaded and opened as needed - * until the class is found. - * - * @param name the name of the class - * @param isGlobal true or false - * @return the resulting class - * @throws ClassNotFoundException if the class could not be found, - * or if the loader is closed. - * @throws NullPointerException if {@code name} is {@code null}. - */ - public Class findClass(@NotNull final String name, final boolean isGlobal) throws ClassNotFoundException { - if (this.isDisabling()) { - throw new ClassNotFoundException("This class loader is disabling!"); - } - - if (this.classes.containsKey(name)) { - return this.classes.get(name); - } - - Class classObject = null; - - if (isGlobal) { - classObject = this.manager.findClass(name); - } - - if (classObject == null) { - classObject = super.findClass(name); - - if (classObject != null) { - this.manager.setClass(name, classObject); - } - } - - return classObject; - } - - /** - * Removes all classes from the class path. - */ - public void removeClasses() { - if (!this.isDisabling()) { - throw new IllegalStateException("Cannot remove class when the loader isn't disabling!"); - } - - this.manager.removeAll(this.classes); - this.classes.clear(); - } - - /** - * Sets the disable status of the addon. - * - * @param isDisabling true or false - */ - public void setDisabling(final boolean isDisabling) { - this.isDisabling = isDisabling; - } - - /** - * Checks if the addon is disabling. - * - * @return true or false - */ - public boolean isDisabling() { - return this.isDisabling; - } - - /** - * Fetches an instance of {@link AddonManager}. - * - * @return the instance of {@link AddonManager} - */ - public @NotNull AddonManager getManager() { - return this.manager; - } - - /** - * Fetches an instance of {@link IAddon}. - * - * @return the instance of {@link IAddon} - */ - public @Nullable IAddon getAddon() { - return this.addon; - } - - /** - * The name of the class or addon. - * - * @return the name of the class or addon - */ - public @NotNull String getName() { - return this.name; - } - - /** - * The path of the class or addon. - * - * @return the path of the class or addon - */ - public @NotNull Path getPath() { - return this.path; - } -} \ No newline at end of file diff --git a/addons/src/main/java/com/ryderbelserion/fusion/addons/AddonManager.java b/addons/src/main/java/com/ryderbelserion/fusion/addons/AddonManager.java deleted file mode 100644 index 0aaf441a..00000000 --- a/addons/src/main/java/com/ryderbelserion/fusion/addons/AddonManager.java +++ /dev/null @@ -1,378 +0,0 @@ -package com.ryderbelserion.fusion.addons; - -import com.ryderbelserion.fusion.addons.interfaces.IAddon; -import com.ryderbelserion.fusion.addons.objects.Addon; -import org.jetbrains.annotations.NotNull; -import java.io.IOException; -import java.io.InputStream; -import java.nio.file.FileSystem; -import java.nio.file.FileSystems; -import java.nio.file.FileVisitResult; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.SimpleFileVisitor; -import java.nio.file.attribute.BasicFileAttributes; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.Properties; -import java.util.concurrent.ConcurrentHashMap; - -/** - * Manages addons, including loading it into the class path, and caching it all. - */ -public class AddonManager { - - private final Map> classMap = new ConcurrentHashMap<>(); - private final Map loaders = new ConcurrentHashMap<>(); - - private final Path folder; - private final Path path; - - /** - * The default constructor to create an instance of {@link AddonManager} - * - * @param path the root path - */ - public AddonManager(@NotNull final Path path) { - this.folder = path.resolve("addons"); - - this.path = path; - } - - /** - * Purges all classes found in the map passed through. - * - * @param classes the map of classes - */ - public void removeAll(@NotNull final Map> classes) { - classes.keySet().forEach(this.classMap::remove); - - for (final Class object : classes.values()) { - if (this.classMap.containsValue(object)) { - throw new IllegalStateException(String.format("Class %s did not get removed.", object.getName())); - } - } - } - - /** - * Adds a class path to the concurrent cash. - * - * @param name the name of the class - * @param classObject the class object - */ - public void setClass(@NotNull final String name, @NotNull final Class classObject) { - this.classMap.put(name, classObject); - } - - /** - * Retrieves a class object by name. - * - * @param name the name of the class - * @return {@link Class} - */ - public Class findClass(@NotNull final String name) { - Class classObject = null; - - if (this.classMap.containsKey(name)) { - classObject = this.classMap.get(name); - } - - if (classObject == null) { - for (final AddonClassLoader loader : this.loaders.values()) { - try { - if ((classObject = loader.findClass(name, false)) != null) { - this.classMap.put(name, classObject); - - break; - } - } catch (final Exception ignored) {} - } - } - - return classObject; - } - - /** - * Creates the addons folder, loops through files, and loads all addons into the class path. - * - * @param depth defines how far we should check for jar/zip files - * @return {@link AddonManager} - * @throws IllegalStateException throws an exception if the directory cannot be created - */ - public @NotNull AddonManager load(final int depth) throws IllegalStateException { - try { - Files.createDirectories(this.folder); - } catch (final IOException exception) { - throw new IllegalStateException(String.format("Could not create folder %s!", this.folder), exception); - } - - final List addons = getFiles(this.folder, List.of( - ".jar", - ".zip" - ), depth); - - addons.forEach(this::loadAddon); - - return this; - } - - /** - * Retrieves an instance of the addon by using the class object - * - * @param classObject the instance - * @return {@code Optional} - * @param the extended class - */ - public Optional getAddonInstance(@NotNull final Class classObject) { - return this.loaders.values().stream().map(AddonClassLoader::getAddon).filter(Objects::nonNull).filter(addon -> addon.getClass().equals(classObject)).map(classObject::cast).findAny(); - } - - /** - * Retrieves an instance of the addon by name. - * - * @param name the name of the addon - * @return {@code Optional} an optional containing the addon instance - */ - public Optional getAddonInstance(@NotNull final String name) { - if (this.loaders.containsKey(name)) { - return Optional.ofNullable(this.loaders.get(name).getAddon()); - } - - return Optional.empty(); - } - - /** - * Reloads an addon. - * - * @param addon the addon instance - * @param the addon instance - * @throws IllegalStateException throws an exception if the addon could not be reloaded. - * @return {@code IAddon} - */ - public IAddon reloadAddon(@NotNull final T addon) throws IllegalStateException { - if (addon.isEnabled()) { - addon.disable(); - } - - final Path path = addon.getLoader().getPath(); - - IAddon foundKey; - - try (AddonClassLoader loader = this.loadAddon(path)) { - final IAddon key = loader.getAddon(); - - if (key != null) { - key.enable(this.folder.resolve(key.getName())); - } - - foundKey = key; - } catch (final Exception exception) { - throw new IllegalStateException(String.format("Could not reload addon %s!", addon), exception); - } - - return foundKey; - } - - /** - * Unloads an addon. - * - * @param classObject the addon instance - * @param the class instance - * @throws IllegalStateException throws an exception if the addon could not be reloaded. - */ - public void unloadAddon(@NotNull final Class classObject) { - getAddonInstance(classObject).ifPresent(this::unloadAddon); - } - - /** - * Reloads an addon. - * - * @param addon {@link IAddon} - */ - public void reloadAddonConfig(@NotNull final IAddon addon) { - addon.onDisable(); - addon.onEnable(); - } - - /** - * Unloads an addon. - * - * @param addon {@link T} the addon instance - * @param the class instance - * @throws IllegalStateException throws an exception if the addon is not found. - */ - public void unloadAddon(@NotNull final T addon) throws IllegalStateException { - if (addon.isEnabled()) { - addon.disable(); - } - - final AddonClassLoader classLoader = addon.getLoader(); - - classLoader.setDisabling(true); - classLoader.removeClasses(); - - final String name = addon.getName(); - - if (!this.loaders.containsKey(name)) { - throw new IllegalStateException(String.format("Cannot find class loader by name %s, Panicking!", name)); - } - - classLoader.setDisabling(false); - - //noinspection EmptyTryBlock - try (final AddonClassLoader loader = this.loaders.remove(name)) { - - } catch (final IOException ignored) {} - } - - /** - * Loads an addon by path. - * - * @param path the relative path object - * @return {@link AddonClassLoader} the class loader - * @throws IllegalStateException throws an exception if the configuration is invalid, or if an addon already exists with that name. - */ - public @NotNull AddonClassLoader loadAddon(@NotNull final Path path) throws IllegalStateException { - final Addon addon = getProperties(path); - - final String group = addon.getMain(); - final String name = addon.getName(); - - if (group.isEmpty() || group.equals("N/A")) { - throw new IllegalStateException(String.format("Addon %s does not have a group key.", group)); - } - - if (name.isEmpty() || name.equals("N/A")) { - throw new IllegalStateException(String.format("Addon %s does not have a name key.", name)); - } - - if (this.loaders.containsKey(name)) { - throw new IllegalStateException(String.format("Addon with the name %s already been loaded.", name)); - } - - if (this.classMap.containsKey(name)) { - throw new IllegalStateException(String.format("Addon with the name %s main class has already been loaded.", name)); - } - - AddonClassLoader loader; - - try { - loader = new AddonClassLoader(this, path, group, name); - } catch (final Exception exception) { - throw new IllegalStateException(String.format("Failed to load %s!", name), exception); - } - - this.loaders.put(name, loader); - - return loader; - } - - /** - * Get a collection of addons, mapping to getAddon while filtering objects if they are null. - * - * @return {@code Collection} a list of addons - */ - public @NotNull Collection getAddons() { - return this.loaders.values().stream().map(AddonClassLoader::getAddon).filter(Objects::nonNull).toList(); - } - - /** - * Enable all addons. Does NOT load any addons. - * - * @return {@link AddonManager} - */ - public @NotNull AddonManager enableAddons() { - this.loaders.values().stream().map(AddonClassLoader::getAddon).filter(Objects::nonNull).filter(addOn -> !addOn.isEnabled()).forEach(addon -> addon.enable(this.folder.resolve(addon.getName()))); - - return this; - } - - /** - * Disable all addons. Does NOT unload them. - * - * @return {@link AddonManager} - */ - public @NotNull AddonManager disableAddons() { - this.loaders.values().stream().map(AddonClassLoader::getAddon).filter(Objects::nonNull).filter(IAddon::isEnabled).forEach(IAddon::disable); - - return this; - } - - /** - * Disables all addons, and purges the caches. - */ - public void purge() { - disableAddons(); - - this.classMap.clear(); - this.loaders.clear(); - } - - /** - * The path of the addon manager. - * - * @return {@link Path} - */ - public @NotNull final Path getPath() { - return this.path; - } - - /** - * Fetches properties using jar file and inputstreams. - * - * @param path the relative path - * @return {@link Addon} - */ - private @NotNull Addon getProperties(@NotNull final Path path) { - final Properties properties = new Properties(); - - try (final FileSystem entry = FileSystems.newFileSystem(path, (ClassLoader) null); final InputStream stream = Files.newInputStream(entry.getPath("addon.properties"))) { - properties.load(stream); - } catch (final IOException exception) { - throw new IllegalStateException("Failed to load addon.properties!", exception); - } - - return new Addon(properties); - } - - /** - * Retrieves a list of paths from the relative path, including the given extensions. - * This method searches up to the specified depth within the directory structure. - * - * @param path the directory to scan for files - * @param extensions the list of file extensions to be searched for (e.g., ".yml") - * @param depth the maximum depth level to search within subdirectories - * @return a list of files - */ - private List getFiles(@NotNull final Path path, @NotNull final List extensions, final int depth) { - final List files = new ArrayList<>(); - - if (Files.isDirectory(path)) { // check if resolved path is a folder to loop through! - try { - Files.walkFileTree(path, new HashSet<>(), Math.max(depth, 1), new SimpleFileVisitor<>() { - @Override - public @NotNull FileVisitResult visitFile(@NotNull final Path path, @NotNull final BasicFileAttributes attributes) { - final String name = path.getFileName().toString(); - - extensions.forEach(extension -> { - if (name.endsWith(extension)) { - files.add(path); - } - }); - - return FileVisitResult.CONTINUE; - } - }); - } catch (final IOException exception) { - throw new IllegalStateException("Failed to get a list of files", exception); - } - } - - return files; - } -} \ No newline at end of file diff --git a/addons/src/main/java/com/ryderbelserion/fusion/addons/ExtensionManager.java b/addons/src/main/java/com/ryderbelserion/fusion/addons/ExtensionManager.java new file mode 100644 index 00000000..da0ab51b --- /dev/null +++ b/addons/src/main/java/com/ryderbelserion/fusion/addons/ExtensionManager.java @@ -0,0 +1,89 @@ +package com.ryderbelserion.fusion.addons; + +import com.ryderbelserion.fusion.addons.utils.FileUtils; +import com.ryderbelserion.fusion.addons.api.Extension; +import com.ryderbelserion.fusion.addons.api.interfaces.IExtensionManager; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +public class ExtensionManager implements IExtensionManager { + + private final Logger logger = LoggerFactory.getLogger(ExtensionManager.class); + + private final Map extensions = new ConcurrentHashMap<>(); + + private final Path parent; // parent path i.e. the extensions folder + + public ExtensionManager(@NotNull final Path parent) { + this.parent = parent; + } + + @Override + public void init(final int depth) { + try { + Files.createDirectories(this.parent); + } catch (final IOException exception) { + throw new IllegalStateException("Could not create folder %s!".formatted(this.parent), exception); + } + + final List paths = FileUtils.getFiles(this.parent, List.of(".jar"), depth); + + this.logger.warn("Initializing extensions..."); + + for (final Path path : paths) { + loadExtension(path); + } + + this.logger.warn("Initialized {} extension(s)", this.extensions.size()); + } + + @Override + public void loadExtension(@NotNull final Path path) { + final Extension extension = new Extension(); + + extension.init(this.parent, path); + + final String name = extension.getName(); + + if (this.extensions.containsKey(name)) { + throw new IllegalStateException("Cannot have 2 extensions with the same name! Extension Name: %s".formatted(name)); + } + + this.extensions.put(name, extension); + } + + @Override + public void disableExtension(@NotNull final Extension extension) { + if (!extension.isEnabled()) return; + + extension.onDisable(); + + extension.setEnabled(false); + } + + @Override + public final boolean isExtensionEnabled(@NotNull final String name) { + final Optional extension = getExtension(name); + + return extension.map(Extension::isEnabled).orElse(false); + } + + @Override + public Optional getExtension(@NotNull final String name) { + return Optional.ofNullable(this.extensions.get(name)); + } + + @Override + public void purge() { + this.extensions.values().forEach(extension -> extension.setEnabled(false)); + this.extensions.clear(); + } +} \ No newline at end of file diff --git a/addons/src/main/java/com/ryderbelserion/fusion/addons/api/Extension.java b/addons/src/main/java/com/ryderbelserion/fusion/addons/api/Extension.java new file mode 100644 index 00000000..09948c60 --- /dev/null +++ b/addons/src/main/java/com/ryderbelserion/fusion/addons/api/Extension.java @@ -0,0 +1,53 @@ +package com.ryderbelserion.fusion.addons.api; + +import com.ryderbelserion.fusion.addons.api.interfaces.IExtension; +import com.ryderbelserion.fusion.addons.entrypoint.classloaders.SimpleExtensionClassLoader; +import com.ryderbelserion.fusion.addons.exceptions.InvalidExtensionException; +import org.jetbrains.annotations.NotNull; +import java.io.IOException; +import java.nio.file.Path; + +public class Extension extends IExtension { + + private SimpleExtensionClassLoader classLoader; + + public Extension() {} + + @Override + public void init(@NotNull final Path parent, @NotNull final Path path) { + super.init(parent, path); + + try { + this.classLoader = new SimpleExtensionClassLoader( + path, + parent, + this, + getClass().getClassLoader() + ); + } catch (final IOException | InvalidExtensionException exception) { + throw new RuntimeException(exception); + } + + setEnabled(true); + } + + private boolean isEnabled = false; + + @Override + public void setEnabled(final boolean isEnabled) { + if (this.isEnabled != isEnabled) { + this.isEnabled = isEnabled; + + if (this.isEnabled) { + onEnable(); + } else { + onDisable(); + } + } + } + + @Override + public final boolean isEnabled() { + return this.isEnabled; + } +} \ No newline at end of file diff --git a/addons/src/main/java/com/ryderbelserion/fusion/addons/api/ExtensionMeta.java b/addons/src/main/java/com/ryderbelserion/fusion/addons/api/ExtensionMeta.java new file mode 100644 index 00000000..81541219 --- /dev/null +++ b/addons/src/main/java/com/ryderbelserion/fusion/addons/api/ExtensionMeta.java @@ -0,0 +1,86 @@ +package com.ryderbelserion.fusion.addons.api; + +import com.ryderbelserion.fusion.addons.api.interfaces.IExtensionMeta; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Properties; + +public abstract class ExtensionMeta implements IExtensionMeta { + + private String version; + private Logger logger; + private String main; + private String name; + + private Path path; + + public void init(@NotNull final Path parent, @NotNull final Path path) { + final Properties properties = new Properties(); + + try (final FileSystem entry = FileSystems.newFileSystem(path, (ClassLoader) null); final InputStream stream = Files.newInputStream(entry.getPath("addon.properties"))) { + properties.load(stream); + } catch (final IOException exception) { + throw new IllegalStateException("Failed to load addon.properties!", exception); + } + + final String pathName = path.getFileName().toString(); + + this.version = properties.getProperty("version", "N/A"); + this.main = properties.getProperty("main", "N/A"); + this.name = properties.getProperty("name", pathName); + + if (this.main.isEmpty() || this.main.equals("N/A")) { + throw new IllegalStateException("Extension group cannot be empty for %s.".formatted(pathName)); + } + + if (this.name.isEmpty()) { + throw new IllegalStateException("Extension name cannot be empty for %s.".formatted(pathName)); + } + + this.logger = LoggerFactory.getLogger(this.name); + + this.logger.warn("Loading the extension {}.", this.name); + + this.path = parent.resolve(this.name); + + if (!Files.exists(this.path)) { + try { + Files.createDirectory(this.path); + } catch (final IOException ignored) { + this.logger.warn("Failed to create directory {}", this.path); + } + } + } + + @Override + public Path getDataDirectory() { + return this.path; + } + + @Override + public String getMainClass() { + return this.main; + } + + @Override + public String getVersion() { + return this.version; + } + + @Override + public Logger getLogger() { + return this.logger; + } + + @Override + public String getName() { + return this.name; + } +} \ No newline at end of file diff --git a/addons/src/main/java/com/ryderbelserion/fusion/addons/api/interfaces/IExtension.java b/addons/src/main/java/com/ryderbelserion/fusion/addons/api/interfaces/IExtension.java new file mode 100644 index 00000000..89412150 --- /dev/null +++ b/addons/src/main/java/com/ryderbelserion/fusion/addons/api/interfaces/IExtension.java @@ -0,0 +1,19 @@ +package com.ryderbelserion.fusion.addons.api.interfaces; + +import com.ryderbelserion.fusion.addons.api.ExtensionMeta; + +public abstract class IExtension extends ExtensionMeta { + + public IExtension() {} + + public void onEnable() {} + + public void onDisable() {} + + public void onReload() {} + + public abstract void setEnabled(final boolean isEnabled); + + public abstract boolean isEnabled(); + +} \ No newline at end of file diff --git a/addons/src/main/java/com/ryderbelserion/fusion/addons/api/interfaces/IExtensionManager.java b/addons/src/main/java/com/ryderbelserion/fusion/addons/api/interfaces/IExtensionManager.java new file mode 100644 index 00000000..eadd595b --- /dev/null +++ b/addons/src/main/java/com/ryderbelserion/fusion/addons/api/interfaces/IExtensionManager.java @@ -0,0 +1,22 @@ +package com.ryderbelserion.fusion.addons.api.interfaces; + +import com.ryderbelserion.fusion.addons.api.Extension; +import org.jetbrains.annotations.NotNull; +import java.nio.file.Path; +import java.util.Optional; + +public interface IExtensionManager { + + void init(final int depth); + + void loadExtension(@NotNull final Path path); + + void disableExtension(@NotNull final Extension extension); + + boolean isExtensionEnabled(@NotNull final String name); + + Optional getExtension(@NotNull final String name); + + void purge(); + +} \ No newline at end of file diff --git a/addons/src/main/java/com/ryderbelserion/fusion/addons/api/interfaces/IExtensionMeta.java b/addons/src/main/java/com/ryderbelserion/fusion/addons/api/interfaces/IExtensionMeta.java new file mode 100644 index 00000000..568ca066 --- /dev/null +++ b/addons/src/main/java/com/ryderbelserion/fusion/addons/api/interfaces/IExtensionMeta.java @@ -0,0 +1,18 @@ +package com.ryderbelserion.fusion.addons.api.interfaces; + +import org.slf4j.Logger; +import java.nio.file.Path; + +public interface IExtensionMeta { + + Path getDataDirectory(); + + String getMainClass(); + + String getVersion(); + + Logger getLogger(); + + String getName(); + +} \ No newline at end of file diff --git a/addons/src/main/java/com/ryderbelserion/fusion/addons/entrypoint/classloaders/SimpleExtensionClassLoader.java b/addons/src/main/java/com/ryderbelserion/fusion/addons/entrypoint/classloaders/SimpleExtensionClassLoader.java new file mode 100644 index 00000000..47b8e349 --- /dev/null +++ b/addons/src/main/java/com/ryderbelserion/fusion/addons/entrypoint/classloaders/SimpleExtensionClassLoader.java @@ -0,0 +1,89 @@ +package com.ryderbelserion.fusion.addons.entrypoint.classloaders; + +import com.ryderbelserion.fusion.addons.api.interfaces.IExtension; +import com.ryderbelserion.fusion.addons.api.interfaces.IExtensionMeta; +import com.ryderbelserion.fusion.addons.exceptions.InvalidExtensionException; +import org.jetbrains.annotations.NotNull; +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Path; +import java.util.Collection; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.jar.JarFile; + +public class SimpleExtensionClassLoader extends URLClassLoader { + + private final Map> classes = new ConcurrentHashMap<>(); + + protected final IExtension extension; + protected final JarFile jarFile; + protected final Path source; + protected final Path path; + protected final URL url; + + public SimpleExtensionClassLoader(@NotNull final Path path, @NotNull final Path source, + @NotNull final IExtensionMeta extension, @NotNull final ClassLoader loader) throws IOException, InvalidExtensionException { + super(path.getFileName().toString(), new URL[]{path.toUri().toURL()}, loader); + + this.jarFile = new JarFile(path.toFile()); + this.source = source; + this.path = path; + + this.url = this.path.toUri().toURL(); + + Class jarClass; + + try { + jarClass = Class.forName(extension.getMainClass(), true, this); + + this.classes.put(jarClass.getName(), jarClass); + } catch (final ClassNotFoundException exception) { + throw new InvalidExtensionException("Could not find main class %s,".formatted(extension.getMainClass()), exception); + } + + Class extensionClass; + + try { + extensionClass = jarClass.asSubclass(IExtension.class); + } catch (final Exception exception) { + throw new InvalidExtensionException("Main Class %s must extend Extension!".formatted(extension.getMainClass()), exception); + } + + Constructor constructor; + + try { + constructor = extensionClass.getDeclaredConstructor(); + } catch (final NoSuchMethodException exception) { + throw new InvalidExtensionException("Main Class %s must have a no-args constructor".formatted(extension.getMainClass()), exception); + } + + try { + this.extension = constructor.newInstance(); + } catch (InstantiationException e) { + throw new InvalidExtensionException("Main Class %s must not be abstract!".formatted(extension.getMainClass())); + } catch (IllegalAccessException e) { + throw new InvalidExtensionException("Main Class %s must be accessible!".formatted(extension.getMainClass())); + } catch (InvocationTargetException e) { + throw new InvalidExtensionException("Exception initializing main class %s!".formatted(extension.getMainClass())); + } + } + + @Override + protected Class findClass(String name) throws ClassNotFoundException { + Class result = this.classes.get(name); + + if (result == null) { + this.classes.put(name, result = super.findClass(name)); + } + + return result; + } + + public @NotNull Collection> getClasses() { + return this.classes.values(); + } +} \ No newline at end of file diff --git a/addons/src/main/java/com/ryderbelserion/fusion/addons/exceptions/InvalidExtensionException.java b/addons/src/main/java/com/ryderbelserion/fusion/addons/exceptions/InvalidExtensionException.java new file mode 100644 index 00000000..b694dc1e --- /dev/null +++ b/addons/src/main/java/com/ryderbelserion/fusion/addons/exceptions/InvalidExtensionException.java @@ -0,0 +1,14 @@ +package com.ryderbelserion.fusion.addons.exceptions; + +import org.jetbrains.annotations.NotNull; + +public class InvalidExtensionException extends Exception { + + public InvalidExtensionException(@NotNull final String message, @NotNull final Throwable cause) { + super(message, cause); + } + + public InvalidExtensionException(@NotNull final String message) { + super(message); + } +} \ No newline at end of file diff --git a/addons/src/main/java/com/ryderbelserion/fusion/addons/interfaces/IAddon.java b/addons/src/main/java/com/ryderbelserion/fusion/addons/interfaces/IAddon.java deleted file mode 100644 index a4ef4600..00000000 --- a/addons/src/main/java/com/ryderbelserion/fusion/addons/interfaces/IAddon.java +++ /dev/null @@ -1,192 +0,0 @@ -package com.ryderbelserion.fusion.addons.interfaces; - -import com.ryderbelserion.fusion.addons.AddonClassLoader; -import com.ryderbelserion.fusion.files.FileManager; -import org.jetbrains.annotations.NotNull; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; - -/** - * The addon class, handles all things related to addons. - */ -public abstract class IAddon { - - /** - * The addon class, handles all things related to addons. - */ - public IAddon() { - - } - - private FileManager fileManager; - private AddonClassLoader loader; - private boolean isEnabled; - private Logger logger; - private String name; - private Path folder; - private String group; - - /** - * Enables the addon. - */ - public void onEnable() { - setEnabled(true); - - if (this.folder != null && !Files.exists(this.folder)) { - try { - Files.createDirectory(this.folder); - } catch (final IOException exception) { - throw new IllegalStateException("Cannot enable the addon, the folder %s did not get created.".formatted(this.folder)); - } - - this.fileManager = new FileManager(this.folder); - } - } - - /** - * Disables the addon. - */ - public void onDisable() { - setEnabled(false); - } - - public void onReload() { - if (this.folder != null && !Files.exists(this.folder)) { - try { - Files.createDirectory(this.folder); - } catch (final IOException exception) { - throw new IllegalStateException("Cannot enable the addon, the folder %s did not get created.".formatted(this.folder)); - } - } - } - - /** - * Enables an addon, this includes adding it to the class path. - * - * @param folder the addon's folder - */ - public void enable(@NotNull final Path folder) { - if (this.isEnabled()) { - throw new IllegalStateException("Cannot enable the addon when it's already enabled"); - } - - this.folder = folder; - - this.onEnable(); - } - - /** - * Disables the addon, this includes removing it from the class path. - */ - public void disable() { - if (!this.isEnabled()) { - throw new IllegalStateException("Cannot disable the addon when it's not enabled"); - } - - this.onDisable(); - } - - /** - * Sets the addon class loader - * - * @param loader {@link AddonClassLoader} - */ - public void setLoader(@NotNull final AddonClassLoader loader) { - this.loader = loader; - } - - /** - * Retrieves an instance of the addon class loader. - * - * @return {@link AddonClassLoader} - */ - public @NotNull AddonClassLoader getLoader() { - return this.loader; - } - - /** - * Sets the addon to be active. - * - * @param enabled true or false - */ - public void setEnabled(final boolean enabled) { - this.isEnabled = enabled; - } - - /** - * Checks if the addon is enabled or not. - * - * @return true or false - */ - public boolean isEnabled() { - return this.isEnabled; - } - - /** - * Sets the name of the addon, and creates a logger impl. - * - * @param name the name of the addon - */ - public void setName(@NotNull final String name) { - this.name = name; - } - - /** - * Gets the group i.e. the domain - * - * @param group the group - */ - public void setGroup(@NotNull final String group) { - this.logger = LoggerFactory.getLogger(group); - this.group = group; - } - - /** - * Get an instance of the FileManager. - * - * @return the file manager - */ - public @NotNull final FileManager getFileManager() { - return this.fileManager; - } - - /** - * Gets an instance of the logger. - * - * @return the logger instance of this addon - */ - public @NotNull Logger getLogger() { - return this.logger; - } - - /** - * Retrieves the name of the addon. - * - * @return the name of the addon - */ - public @NotNull String getName() { - return this.name; - } - - /** - * Retrieves the group path. - * - * @return the group path of the addon - */ - public @NotNull String getGroup() { - return this.group; - } - - /** - * Retrieves the folder of the addon. - * - * @return the folder of the addon - */ - public @NotNull Path getFolder() { - return this.folder; - } -} \ No newline at end of file diff --git a/addons/src/main/java/com/ryderbelserion/fusion/addons/objects/Addon.java b/addons/src/main/java/com/ryderbelserion/fusion/addons/objects/Addon.java deleted file mode 100644 index dc40ef29..00000000 --- a/addons/src/main/java/com/ryderbelserion/fusion/addons/objects/Addon.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.ryderbelserion.fusion.addons.objects; - -import org.jetbrains.annotations.NotNull; -import java.util.Properties; - -/** - * This holds information relating to addons that are created, It pulls information from property files. - */ -public class Addon { - - private final String main; - private final String name; - - /** - * Builds an addon object. - * - * @param properties the properties to pull information from - */ - public Addon(@NotNull final Properties properties) { - this.main = properties.getProperty("main", "N/A"); - this.name = properties.getProperty("name", "N/A"); - } - - /** - * Gets the addon domain i.e. com.ryderbelserion which is the class path. - * - * @return the addon domain - */ - public @NotNull final String getMain() { - return this.main; - } - - /** - * Gets the name of the addon i.e. beans. - * - * @return the name of the addon - */ - public @NotNull final String getName() { - return this.name; - } -} \ No newline at end of file diff --git a/addons/src/main/java/com/ryderbelserion/fusion/addons/utils/FileUtils.java b/addons/src/main/java/com/ryderbelserion/fusion/addons/utils/FileUtils.java new file mode 100644 index 00000000..f4838144 --- /dev/null +++ b/addons/src/main/java/com/ryderbelserion/fusion/addons/utils/FileUtils.java @@ -0,0 +1,43 @@ +package com.ryderbelserion.fusion.addons.utils; + +import org.jetbrains.annotations.NotNull; + +import java.io.IOException; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; + +public class FileUtils { + + public static List getFiles(@NotNull final Path path, @NotNull final List extensions, final int depth) { + final List files = new ArrayList<>(); + + if (Files.isDirectory(path)) { // check if resolved path is a folder to loop through! + try { + Files.walkFileTree(path, new HashSet<>(), Math.max(depth, 1), new SimpleFileVisitor<>() { + @Override + public @NotNull FileVisitResult visitFile(@NotNull final Path path, @NotNull final BasicFileAttributes attributes) { + final String name = path.getFileName().toString(); + + extensions.forEach(extension -> { + if (name.endsWith(extension)) { + files.add(path); + } + }); + + return FileVisitResult.CONTINUE; + } + }); + } catch (final IOException exception) { + throw new IllegalStateException("Failed to get a list of files", exception); + } + } + + return files; + } +} \ No newline at end of file diff --git a/examples/extension/build.gradle.kts b/examples/extension/build.gradle.kts new file mode 100644 index 00000000..a0ffa73e --- /dev/null +++ b/examples/extension/build.gradle.kts @@ -0,0 +1,8 @@ +plugins { + `config-java` +} + +dependencies { + compileOnly(project(":fusion-addons")) + compileOnly(libs.log4j) +} \ No newline at end of file diff --git a/examples/extension/src/main/java/me/corecraft/currency/Currency.java b/examples/extension/src/main/java/me/corecraft/currency/Currency.java new file mode 100644 index 00000000..8a407261 --- /dev/null +++ b/examples/extension/src/main/java/me/corecraft/currency/Currency.java @@ -0,0 +1,42 @@ +package me.corecraft.currency; + +import com.ryderbelserion.fusion.addons.api.Extension; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +public class Currency extends Extension { + + @Override + public void onEnable() { + final Path path = getDataDirectory(); + + if (!Files.exists(path)) { + try { + Files.createDirectory(path); + } catch (final IOException exception) { + throw new IllegalStateException("Cannot enable the extension, the folder %s did not get created.".formatted(path)); + } + } + + getLogger().warn("The extension is enabling!"); + } + + @Override + public void onDisable() { + getLogger().warn("The extension is disabled!"); + } + + @Override + public void onReload() { + final Path path = getDataDirectory(); + + if (!Files.exists(path)) { + try { + Files.createDirectory(path); + } catch (final IOException exception) { + throw new IllegalStateException("Cannot enable the extension, the folder %s did not get created.".formatted(path)); + } + } + } +} \ No newline at end of file diff --git a/examples/extension/src/main/resources/addon.properties b/examples/extension/src/main/resources/addon.properties new file mode 100644 index 00000000..ef892127 --- /dev/null +++ b/examples/extension/src/main/resources/addon.properties @@ -0,0 +1,4 @@ +name=Currency +main=me.corecraft.currency.Currency +version=0.1.0 +description=A currency module \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6cf62bf7..c39cbdeb 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -18,6 +18,7 @@ nexo = "1.6.0" # https://github.com/Nexo-MC fix-javadoc = "1.19" # https://github.com/mfnalex/gradle-fix-javadoc-plugin run-paper = "3.0.2" # https://github.com/jpenilla/run-task shadow = "9.2.2" # https://github.com/GradleUp/shadow +log4j = "2.12.4" # https://logging.apache.org/log4j/2.12.x/maven-artifacts.html [plugins] # https://github.com/mfnalex/gradle-fix-javadoc-plugin @@ -35,6 +36,8 @@ configurate-gson = { group = "org.spongepowered", name = "configurate-gson", ver velocity = { group = "com.velocitypowered", name = "velocity-api", version.ref = "velocity" } # https://github.com/GradleUp/shadow shadow = { module = "com.gradleup.shadow:shadow-gradle-plugin", version.ref = "shadow" } +# https://logging.apache.org/log4j/2.12.x/maven-artifacts.html +log4j = { module = "org.apache.logging.log4j:log4j-slf4j-impl", version.ref = "log4j" } # https://github.com/JetBrains/java-annotations annotations = { module = "org.jetbrains:annotations", version.ref = "annotations" } # https://github.com/mojang/brigadier diff --git a/plugin/build.gradle.kts b/plugin/build.gradle.kts index 0c519554..d682137a 100644 --- a/plugin/build.gradle.kts +++ b/plugin/build.gradle.kts @@ -5,6 +5,7 @@ plugins { project.group = "${rootProject.group}.paper" dependencies { + api(project(":fusion-addons")) api(project(":fusion-paper")) } diff --git a/plugin/src/main/java/com/ryderbelserion/fusion/Fusion.java b/plugin/src/main/java/com/ryderbelserion/fusion/Fusion.java index 062f7f15..c1326d12 100644 --- a/plugin/src/main/java/com/ryderbelserion/fusion/Fusion.java +++ b/plugin/src/main/java/com/ryderbelserion/fusion/Fusion.java @@ -1,5 +1,6 @@ package com.ryderbelserion.fusion; +import com.ryderbelserion.fusion.addons.ExtensionManager; import com.ryderbelserion.fusion.paper.FusionPaper; import com.ryderbelserion.fusion.paper.builders.ItemBuilder; import org.bukkit.entity.Player; @@ -13,17 +14,21 @@ public class Fusion extends JavaPlugin implements Listener { - private final FusionPaper fusion; + //private final FusionPaper fusion; - public Fusion(@NotNull final FusionPaper fusion) { - this.fusion = fusion; - } + //public Fusion(@NotNull final FusionPaper fusion) { + // //this.fusion = fusion; + //} @Override public void onEnable() { - this.fusion.setPlugin(this).init(); + final ExtensionManager manager = new ExtensionManager(getDataPath().resolve("extensions")); + + manager.init(1); + + //this.fusion.setPlugin(this).init(); - getServer().getPluginManager().registerEvents(this, this); + //getServer().getPluginManager().registerEvents(this, this); } @EventHandler diff --git a/plugin/src/main/java/com/ryderbelserion/fusion/FusionLoader.java b/plugin/src/main/java/com/ryderbelserion/fusion/FusionLoader.java index 642d2afe..be0b850a 100644 --- a/plugin/src/main/java/com/ryderbelserion/fusion/FusionLoader.java +++ b/plugin/src/main/java/com/ryderbelserion/fusion/FusionLoader.java @@ -1,13 +1,11 @@ package com.ryderbelserion.fusion; -import com.ryderbelserion.fusion.core.utils.StringUtils; import com.ryderbelserion.fusion.paper.FusionPaper; import io.papermc.paper.plugin.bootstrap.BootstrapContext; import io.papermc.paper.plugin.bootstrap.PluginBootstrap; import io.papermc.paper.plugin.bootstrap.PluginProviderContext; import org.bukkit.plugin.java.JavaPlugin; import org.jetbrains.annotations.NotNull; -import java.util.List; public class FusionLoader implements PluginBootstrap { @@ -17,16 +15,16 @@ public class FusionLoader implements PluginBootstrap { public void bootstrap(@NotNull BootstrapContext context) { this.fusion = new FusionPaper(context); - this.fusion.log("info", "We are starting the server!"); + /*this.fusion.log("info", "We are starting the server!"); this.fusion.log("info", StringUtils.toString(List.of( "beans", "alpha" - ))); + )));*/ } @Override public @NotNull JavaPlugin createPlugin(@NotNull PluginProviderContext context) { - return new Fusion(this.fusion); + return new Fusion(); } } \ No newline at end of file diff --git a/plugin/src/main/resources/paper-plugin.yml b/plugin/src/main/resources/paper-plugin.yml index d9990d4f..757a2cf0 100644 --- a/plugin/src/main/resources/paper-plugin.yml +++ b/plugin/src/main/resources/paper-plugin.yml @@ -1,6 +1,6 @@ name: 'Fusion' main: 'com.ryderbelserion.fusion.Fusion' -bootstrapper: 'com.ryderbelserion.fusion.FusionLoader' +#bootstrapper: 'com.ryderbelserion.fusion.FusionLoader' version: '1.0.0' api-version: '1.21.10' diff --git a/settings.gradle.kts b/settings.gradle.kts index fb30f3b5..b00f5d0b 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -24,7 +24,9 @@ listOf( listOf( "minecraft/kyori" to "kyori", - "minecraft/paper" to "paper" + "minecraft/paper" to "paper", + + "examples/extension" to "extension" ).forEach { includeProject(it.first, it.second) }