From 8fee20eddb06dcae6b06f4bad986eca47b4ae995 Mon Sep 17 00:00:00 2001 From: Francisco Solis Date: Wed, 20 Sep 2023 21:09:02 -0300 Subject: [PATCH] patch(entrypoint-system): improved entrypoint system & beta update * Improved entrypoint system * This is a beta release to prepare modules and extensions --- .../simplecoreapi/global/SimpleCoreAPI.kt | 27 +++--- .../global/modules/ModuleManager.kt | 2 +- .../global/utils/SoftwareType.kt | 4 +- .../standalone/EntrypointLoader.kt | 96 ++++++++++++++----- .../simplecoreapi/standalone/ModuleLoader.kt | 86 ++++++++++------- .../standalone/StandaloneLoader.kt | 11 ++- .../modules/ModuleInteroperabilityTest.kt | 48 ++++++++++ 7 files changed, 198 insertions(+), 76 deletions(-) create mode 100644 src/test/kotlin/xyz/theprogramsrc/simplecoreapi/global/modules/ModuleInteroperabilityTest.kt diff --git a/src/main/kotlin/xyz/theprogramsrc/simplecoreapi/global/SimpleCoreAPI.kt b/src/main/kotlin/xyz/theprogramsrc/simplecoreapi/global/SimpleCoreAPI.kt index dd21795f..f9c884ae 100644 --- a/src/main/kotlin/xyz/theprogramsrc/simplecoreapi/global/SimpleCoreAPI.kt +++ b/src/main/kotlin/xyz/theprogramsrc/simplecoreapi/global/SimpleCoreAPI.kt @@ -33,16 +33,23 @@ class SimpleCoreAPI(val logger: ILogger) { mkdirs() } + /** + * Checks if the current [SoftwareType] is the one specified + * @param softwareType The [SoftwareType] to check + * @return true if the current [SoftwareType] is the one specified + */ + fun isRunningSoftwareType(softwareType: SoftwareType) = softwareType.check() + /** * The given module is added to the required modules list. * If the module is not found, it will be downloaded and automatically loaded. * - * @param id The module id + * @param id The module id. Should be in the format author/repo */ fun requireModule(id: String) { assert(id.split("/").size == 2) { "Invalid repositoryId format. It should be /"} - val isStanalone = StandaloneLoader.isRunning - val moduleFile = if(isStanalone) { + val isStandalone = isRunningSoftwareType(SoftwareType.STANDALONE) || isRunningSoftwareType(SoftwareType.UNKNOWN) + val moduleFile = if(isStandalone) { File(dataFolder("modules"), "${id.split("/")[1]}.jar") } else { File(File("plugins/"), "${id.split("/")[1]}.jar") @@ -53,7 +60,7 @@ class SimpleCoreAPI(val logger: ILogger) { } val downloaded = ModuleManager.downloadModule(id) ?: throw RuntimeException("Module $id could not be downloaded!") - if(isStanalone) { + if(isStandalone) { return // Is automatically loaded later } @@ -102,10 +109,11 @@ class SimpleCoreAPI(val logger: ILogger) { * @param message The message to print. You can use '{time}' to replace with the amount of time in ms * @param block The block to execute */ - fun measureLoad(message: String, block: () -> Unit) { + fun measureLoad(message: String, block: () -> OBJECT): OBJECT { val now = System.currentTimeMillis() - block() + val obj = block() logger.info(message.replace("{time}", "${System.currentTimeMillis() - now}ms")) + return obj } /** @@ -125,11 +133,4 @@ class SimpleCoreAPI(val logger: ILogger) { * @return The version of SimpleCoreAPI */ fun getVersion(): String = "@version@" - - /** - * Checks if the current [SoftwareType] is the one specified - * @param softwareType The [SoftwareType] to check - * @return true if the current [SoftwareType] is the one specified - */ - fun isRunningSoftwareType(softwareType: SoftwareType) = softwareType.check() } \ No newline at end of file diff --git a/src/main/kotlin/xyz/theprogramsrc/simplecoreapi/global/modules/ModuleManager.kt b/src/main/kotlin/xyz/theprogramsrc/simplecoreapi/global/modules/ModuleManager.kt index 5fac9f9a..eff58e2e 100644 --- a/src/main/kotlin/xyz/theprogramsrc/simplecoreapi/global/modules/ModuleManager.kt +++ b/src/main/kotlin/xyz/theprogramsrc/simplecoreapi/global/modules/ModuleManager.kt @@ -30,7 +30,7 @@ object ModuleManager { // Sort by created_at (newest first) and filter by file name ending with .jar val asset = assets.sortedByDescending { it.asJsonObject.get("created_at").asString }.firstOrNull { it.asJsonObject.get("name").asString.endsWith(".jar") } ?: throw NullPointerException("No jar file found in the latest release of $moduleId") val downloadUrl = asset.asJsonObject.get("browser_download_url").asString - val file = File(if(SimpleCoreAPI.instance.let { it.isRunningSoftwareType(SoftwareType.STANDALONE) || it.isRunningSoftwareType(SoftwareType.UNKNOWN) }) SimpleCoreAPI.dataFolder("modules/") else File("plugins/"), moduleId.substringAfterLast("/")) + val file = File(if(SimpleCoreAPI.let { it.isRunningSoftwareType(SoftwareType.STANDALONE) || it.isRunningSoftwareType(SoftwareType.UNKNOWN) }) SimpleCoreAPI.dataFolder("modules/") else File("plugins/"), asset.asJsonObject.get("name").asString) if(!file.exists()){ file.createNewFile() } diff --git a/src/main/kotlin/xyz/theprogramsrc/simplecoreapi/global/utils/SoftwareType.kt b/src/main/kotlin/xyz/theprogramsrc/simplecoreapi/global/utils/SoftwareType.kt index 7f628423..324e49f2 100644 --- a/src/main/kotlin/xyz/theprogramsrc/simplecoreapi/global/utils/SoftwareType.kt +++ b/src/main/kotlin/xyz/theprogramsrc/simplecoreapi/global/utils/SoftwareType.kt @@ -90,5 +90,7 @@ enum class SoftwareType(val check: () -> Boolean = { false }, val display: Strin StandaloneLoader.isRunning }, "Standalone"), - UNKNOWN; + UNKNOWN(check = { + true + }, "Unknown"); } \ No newline at end of file diff --git a/src/main/kotlin/xyz/theprogramsrc/simplecoreapi/standalone/EntrypointLoader.kt b/src/main/kotlin/xyz/theprogramsrc/simplecoreapi/standalone/EntrypointLoader.kt index 57eb68c8..839571c3 100644 --- a/src/main/kotlin/xyz/theprogramsrc/simplecoreapi/standalone/EntrypointLoader.kt +++ b/src/main/kotlin/xyz/theprogramsrc/simplecoreapi/standalone/EntrypointLoader.kt @@ -1,39 +1,83 @@ package xyz.theprogramsrc.simplecoreapi.standalone import java.util.Properties +import java.util.zip.ZipInputStream -@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.CONSTRUCTOR) -@Retention(AnnotationRetention.RUNTIME) -annotation class EntryPoint +/** + * Interface that will be used to load the entry point of the app. + */ +interface EntryPoint { + /** + * Called when the app is loaded + */ + fun onLoad() + + /** + * Called when the app is enabled + */ + fun onEnable() + + /** + * Called when the app is disabled + */ + fun onDisable() +} + +/** + * Class that manages the entry point of the app. It will be in charge of running the onLoad, onEnable and onDisable methods of the class importing [EntryPoint]. + */ class EntrypointLoader { + companion object { + private var entryPoint: EntryPoint? = null + + /** + * Manually register the entrypoint. + * Currently, this is used for testing purposes, but if you have issues with the entrypoint not being loaded, you can use this method to register it manually. + * + * @param clazz The entrypoint class. It must implement [EntryPoint] + */ + fun registerEntrypoint(clazz: Class) { + entryPoint = clazz.getConstructor().newInstance() as EntryPoint + } + } + private var enabled: Boolean = false init { - // First get the resource 'module.properties' located at the root of the jar file - val moduleProperties = EntrypointLoader::class.java.getResourceAsStream("/module.properties") - if(moduleProperties != null) { - // Now read the 'entrypoint' property - val entrypoint = (Properties().let { - it.load(moduleProperties) - it.getProperty("entrypoint") - } ?: "").replace("\"", "") - - assert(entrypoint.isNotBlank()) { "Entrypoint cannot be blank!" } - - // Now load the class - val clazz = this::class.java.classLoader.loadClass(entrypoint) - - // Now check if the class itself is an entrypoint, if it is, initialize it, if not check for the first method that is an entrypoint - if(clazz.isAnnotationPresent(EntryPoint::class.java)){ - clazz.getConstructor().newInstance() - } else { - clazz.methods.forEach { method -> - if(method.isAnnotationPresent(EntryPoint::class.java)){ - method.invoke(null) - return@forEach - } + if(entryPoint == null) { + // First get the resource 'module.properties' located at the root of the jar file + val moduleProperties = EntrypointLoader::class.java.getResourceAsStream("/module.properties") + if(moduleProperties != null) { + // Now read the 'entrypoint' property + val entrypoint = (Properties().let { + it.load(moduleProperties) + it.getProperty("entrypoint") + } ?: "").replace("\"", "") + + assert(entrypoint.isNotBlank()) { "Entrypoint cannot be blank!" } + + // Now load the class + val clazz = this::class.java.classLoader.loadClass(entrypoint) + + // Now check if the class itself is an entrypoint, if it is, initialize it, if not check for the first method that is an entrypoint + if(clazz.isAssignableFrom(EntryPoint::class.java)){ + entryPoint = clazz.getConstructor().newInstance() as EntryPoint } } } + + entryPoint?.onLoad() + } + + fun enable() { + assert(!enabled) { "App already enabled! Please avoid calling this method more than once." } + entryPoint?.onEnable() + enabled = true + } + + fun disable() { + assert(enabled) { "App already disabled! Please avoid calling this method more than once." } + entryPoint?.onDisable() + enabled = false } } diff --git a/src/main/kotlin/xyz/theprogramsrc/simplecoreapi/standalone/ModuleLoader.kt b/src/main/kotlin/xyz/theprogramsrc/simplecoreapi/standalone/ModuleLoader.kt index c3925bd2..aae948ab 100644 --- a/src/main/kotlin/xyz/theprogramsrc/simplecoreapi/standalone/ModuleLoader.kt +++ b/src/main/kotlin/xyz/theprogramsrc/simplecoreapi/standalone/ModuleLoader.kt @@ -6,6 +6,7 @@ import java.io.File import java.net.URLClassLoader import java.util.Properties import java.util.Stack +import java.util.function.Consumer import java.util.jar.JarFile import java.util.jar.JarInputStream @@ -14,6 +15,7 @@ data class ModuleDescription( val version: String, val author: String, val main: String, + val moduleId: String, val dependencies: List = emptyList(), ) @@ -48,40 +50,60 @@ class ModuleLoader { } // Load all files from the modules folder - (modulesFolder.listFiles() ?: emptyArray()).filter { it.isFile && it.extension == "jar" }.forEach { - // Read the file and check if there's a module.properties file - val jarFile = JarFile(it.absolutePath) - val moduleProperties = jarFile.getJarEntry("module.properties")?.let { entry -> - jarFile.getInputStream(entry).use { stream -> - stream.bufferedReader().use { reader -> - val props = Properties() - props.load(reader) - props + fun loadDescriptionFiles() { + (modulesFolder.listFiles() ?: emptyArray()).filter { it.isFile && it.extension == "jar" }.forEach { + // Read the file and check if there's a module.properties file + val jarFile = JarFile(it.absolutePath) + val moduleProperties = jarFile.getJarEntry("module.properties")?.let { entry -> + jarFile.getInputStream(entry).use { stream -> + stream.bufferedReader().use { reader -> + val props = Properties() + props.load(reader) + props + } } + } ?: return@forEach + if(files.containsKey(it)) return@forEach // Already loaded this file + + // Check if the module.properties file contains the required fields + for (requiredField in arrayOf("name", "version", "author", "main", "module-id")) { + assert(moduleProperties.containsKey(requiredField)) { "Module ${it.nameWithoutExtension} is missing the required field '$requiredField'." } + } + // Add the file to the files map + files[it] = ModuleDescription( + name = moduleProperties.getProperty("name").let { name -> + if(name.startsWith('"') && name.endsWith('"')) name.substring(1, name.length - 1) else name + }, + version = moduleProperties.getProperty("version").let { version -> + if(version.startsWith('"') && version.endsWith('"')) version.substring(1, version.length - 1) else version + }, + author = moduleProperties.getProperty("author").let { author -> + if(author.startsWith('"') && author.endsWith('"')) author.substring(1, author.length - 1) else author + }, + main = moduleProperties.getProperty("main").let { main -> + if(main.startsWith('"') && main.endsWith('"')) main.substring(1, main.length - 1) else main + }, + dependencies = moduleProperties.getProperty("dependencies")?.let { dependencies -> + if(dependencies.startsWith('"') && dependencies.endsWith('"')) dependencies.substring(1, dependencies.length - 1) else dependencies + }?.split(",") ?: emptyList(), + moduleId = moduleProperties.getProperty("module-id").let { moduleId -> + if(moduleId.startsWith('"') && moduleId.endsWith('"')) moduleId.substring(1, moduleId.length - 1) else moduleId + } + ) + } + } + + // Load the description files + loadDescriptionFiles() + + // Now make sure all files have their dependencies + files.values.forEach { description -> + description.dependencies.forEach { dependencyId -> + if(!files.any { it.value.moduleId == dependencyId }) { + ModuleManager.downloadModule(dependencyId) + loadDescriptionFiles() } - } ?: return@forEach - - // Check if the module.properties file contains the required fields - if(!moduleProperties.containsKey("name") || !moduleProperties.containsKey("version") || !moduleProperties.containsKey("author") || !moduleProperties.containsKey("main")) return@forEach - - // Add the file to the files map - files[it] = ModuleDescription( - name = moduleProperties.getProperty("name").let { name -> - if(name.startsWith('"') && name.endsWith('"')) name.substring(1, name.length - 1) else name - }, - version = moduleProperties.getProperty("version").let { version -> - if(version.startsWith('"') && version.endsWith('"')) version.substring(1, version.length - 1) else version - }, - author = moduleProperties.getProperty("author").let { author -> - if(author.startsWith('"') && author.endsWith('"')) author.substring(1, author.length - 1) else author - }, - main = moduleProperties.getProperty("main").let { main -> - if(main.startsWith('"') && main.endsWith('"')) main.substring(1, main.length - 1) else main - }, - dependencies = moduleProperties.getProperty("dependencies")?.let { dependencies -> - if(dependencies.startsWith('"') && dependencies.endsWith('"')) dependencies.substring(1, dependencies.length - 1) else dependencies - }?.split(",") ?: emptyList() - ) + } } // Now we need to order the modules diff --git a/src/main/kotlin/xyz/theprogramsrc/simplecoreapi/standalone/StandaloneLoader.kt b/src/main/kotlin/xyz/theprogramsrc/simplecoreapi/standalone/StandaloneLoader.kt index 82a59977..8b08b86f 100644 --- a/src/main/kotlin/xyz/theprogramsrc/simplecoreapi/standalone/StandaloneLoader.kt +++ b/src/main/kotlin/xyz/theprogramsrc/simplecoreapi/standalone/StandaloneLoader.kt @@ -23,12 +23,17 @@ class StandaloneLoader { isRunning = true val simpleCoreAPI = SimpleCoreAPI(JavaLogger(Logger.getAnonymousLogger())) + val entrypoint = simpleCoreAPI.measureLoad("Loaded entrypoint") { + EntrypointLoader() + } + simpleCoreAPI.measureLoad("Loaded modules") { ModuleLoader() // Load modules } - simpleCoreAPI.measureLoad("Loaded entrypoint") { - EntrypointLoader() - } + entrypoint.enable() + Runtime.getRuntime().addShutdownHook(Thread { + entrypoint.disable() + }) } } \ No newline at end of file diff --git a/src/test/kotlin/xyz/theprogramsrc/simplecoreapi/global/modules/ModuleInteroperabilityTest.kt b/src/test/kotlin/xyz/theprogramsrc/simplecoreapi/global/modules/ModuleInteroperabilityTest.kt new file mode 100644 index 00000000..bb91a93e --- /dev/null +++ b/src/test/kotlin/xyz/theprogramsrc/simplecoreapi/global/modules/ModuleInteroperabilityTest.kt @@ -0,0 +1,48 @@ +package xyz.theprogramsrc.simplecoreapi.global.modules + +import org.apache.commons.io.FileUtils +import org.junit.jupiter.api.AfterAll +import xyz.theprogramsrc.simplecoreapi.global.SimpleCoreAPI +import xyz.theprogramsrc.simplecoreapi.standalone.EntryPoint +import xyz.theprogramsrc.simplecoreapi.standalone.EntrypointLoader +import xyz.theprogramsrc.simplecoreapi.standalone.StandaloneLoader +import java.io.File + + +// This will test if modules are able to call methods from other modules. +internal class ModuleInteroperabilityTest { + + //@Test + fun `Test if module can call methods from other modules`() { + // First we register the entrypoint + EntrypointLoader.registerEntrypoint(MockApp::class.java) + + // Start the standalone loader. + StandaloneLoader() + } + + companion object { + + @AfterAll + @JvmStatic + fun tearDown() { + arrayOf("SimpleCoreAPI/", "plugins/").map { File(it) }.filter{ it.exists() }.forEach { FileUtils.forceDelete(it) } + } + } +} + +class MockApp: EntryPoint { + + override fun onLoad() { + println("onLoad") + SimpleCoreAPI.requireModule("TheProgramSrc/SimpleCore-TranslationsModule") // Require the TranslationsModule + } + + override fun onEnable() { + println("onEnable") + } + + override fun onDisable() { + println("onDisable") + } +} \ No newline at end of file