Skip to content

Commit

Permalink
patch(entrypoint-system): improved entrypoint system & beta update
Browse files Browse the repository at this point in the history
* Improved entrypoint system
* This is a beta release to prepare modules and extensions
  • Loading branch information
Im-Fran committed Sep 21, 2023
1 parent 39965a9 commit 8fee20e
Show file tree
Hide file tree
Showing 7 changed files with 198 additions and 76 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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 <author>/<repo>"}
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")
Expand All @@ -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
}

Expand Down Expand Up @@ -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 <OBJECT> 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
}

/**
Expand All @@ -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()
}
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,5 +90,7 @@ enum class SoftwareType(val check: () -> Boolean = { false }, val display: Strin
StandaloneLoader.isRunning
}, "Standalone"),

UNKNOWN;
UNKNOWN(check = {
true
}, "Unknown");
}
Original file line number Diff line number Diff line change
@@ -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<out EntryPoint>) {
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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -14,6 +15,7 @@ data class ModuleDescription(
val version: String,
val author: String,
val main: String,
val moduleId: String,
val dependencies: List<String> = emptyList(),
)

Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
})
}
}
Original file line number Diff line number Diff line change
@@ -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")
}
}

0 comments on commit 8fee20e

Please sign in to comment.