diff options
-rw-r--r-- | build.gradle.kts | 8 | ||||
-rw-r--r-- | src/main/kotlin/me/bush/illnamethislater/CancelState.kt | 46 | ||||
-rw-r--r-- | src/main/kotlin/me/bush/illnamethislater/Event.kt | 11 | ||||
-rw-r--r-- | src/main/kotlin/me/bush/illnamethislater/EventBus.kt | 19 | ||||
-rw-r--r-- | src/main/kotlin/me/bush/illnamethislater/Listener.kt | 17 | ||||
-rw-r--r-- | src/main/kotlin/me/bush/illnamethislater/ListenerGroup.kt | 4 | ||||
-rw-r--r-- | src/main/kotlin/me/bush/illnamethislater/ReflectUtil.kt | 57 | ||||
-rw-r--r-- | src/main/kotlin/me/bush/illnamethislater/Util.kt | 83 | ||||
-rw-r--r-- | src/test/kotlin/Main.kt | 66 | ||||
-rw-r--r-- | src/test/kotlin/Test.kt | 151 |
10 files changed, 288 insertions, 174 deletions
diff --git a/build.gradle.kts b/build.gradle.kts index a1450bd..e253db8 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,4 +1,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile +import org.gradle.api.tasks.testing.logging.TestLogEvent.* +import org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL +import kotlin.collections.setOf plugins { kotlin("jvm") version "1.6.10" @@ -14,7 +17,6 @@ repositories { dependencies { testImplementation(kotlin("test")) - // Staying on the version used by 1.12.2 to avoid possible incompatibilities. // 2.15.0 is not vulnerable to any RCE exploits. implementation("org.apache.logging.log4j:log4j-api:2.15.0") implementation("org.apache.logging.log4j:log4j-core:2.15.0") @@ -25,6 +27,10 @@ dependencies { } tasks.test { + testLogging { + exceptionFormat = FULL + showStandardStreams = true + } useJUnitPlatform() } diff --git a/src/main/kotlin/me/bush/illnamethislater/CancelState.kt b/src/main/kotlin/me/bush/illnamethislater/CancelState.kt new file mode 100644 index 0000000..270d88f --- /dev/null +++ b/src/main/kotlin/me/bush/illnamethislater/CancelState.kt @@ -0,0 +1,46 @@ +package me.bush.illnamethislater + +import sun.misc.Unsafe +import kotlin.reflect.KClass +import kotlin.reflect.KMutableProperty1 +import kotlin.reflect.full.declaredMembers +import kotlin.reflect.full.isSubclassOf +import kotlin.reflect.jvm.javaField +import kotlin.reflect.typeOf + +/** + * @author bush + * @since 1.0.0 + */ +internal fun interface CancelledState { + + /** + * Will either return false, cast [event] to [Event] and return [Event.cancelled], or use + * [Unsafe.getBoolean] to get the value of the "cancelled" field of an external event. + * + * @author bush + * @since 1.0.0 + */ + fun isCancelled(event: Any): Boolean + + companion object { + private val UNSAFE = Unsafe::class.declaredMembers.single { it.name == "theUnsafe" }.handleCall() as Unsafe + + // This is really just for interoperability with forge events, but ig it would work with anything + private val CANCELLED_NAMES = arrayOf("canceled", "cancelled") + private val NOT_CANCELLABLE by lazy { CancelledState { false } } + private val OFFSETS = hashMapOf<KClass<*>, Long>() + + fun cancelStateOf(type: KClass<*>, bus: EventBus): CancelledState { + if (type.isSubclassOf(Event::class)) return CancelledState { (it as Event).cancelled } + if (!bus.externalSupport) return NOT_CANCELLABLE + type.allMembers.filter { it.name in CANCELLED_NAMES && it.returnType == typeOf<Boolean>() } + .filterIsInstance<KMutableProperty1<*, *>>().toList().let { + if (it.isEmpty()) return NOT_CANCELLABLE + if (it.size != 1) bus.logger.warn("Multiple possible cancel fields found for event type $type") + OFFSETS[type] = UNSAFE.objectFieldOffset(it[0].javaField) + return CancelledState { UNSAFE.getBoolean(it, OFFSETS[type]!!) } + } + } + } +} diff --git a/src/main/kotlin/me/bush/illnamethislater/Event.kt b/src/main/kotlin/me/bush/illnamethislater/Event.kt index 39360eb..dedd4df 100644 --- a/src/main/kotlin/me/bush/illnamethislater/Event.kt +++ b/src/main/kotlin/me/bush/illnamethislater/Event.kt @@ -1,19 +1,16 @@ package me.bush.illnamethislater /** - * A base class for events that can be cancelled. + * A base class for events that can be cancelled. More information can be found + * [here](https://github.com/therealbush/eventbus-kotlin) * - * If [cancellable] is true, your event can be [cancelled]. - * - * If [cancelled] is true, listeners with lower priority will not receive it unless: + * If [cancellable] is `true`, your event can be [cancelled], where future listeners will not receive it unless: * * They have [Listener.receiveCancelled] set to `true`. - * * A future listener with [Listener.receiveCancelled] sets [cancelled] to `false` + * * A previous listener with [Listener.receiveCancelled] sets [cancelled] back to `false` * * @author bush * @since 1.0.0 */ -// TODO: 3/27/2022 ducks or a way to cancel anything (can't put a custom annotation on forge events) -// store cancellable info at subscribe time, do not calculate it in post abstract class Event { var cancelled = false set(value) { diff --git a/src/main/kotlin/me/bush/illnamethislater/EventBus.kt b/src/main/kotlin/me/bush/illnamethislater/EventBus.kt index 66424f7..01d90cd 100644 --- a/src/main/kotlin/me/bush/illnamethislater/EventBus.kt +++ b/src/main/kotlin/me/bush/illnamethislater/EventBus.kt @@ -4,18 +4,23 @@ import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.Logger import kotlin.reflect.KClass +// TODO: 3/30/2022 Refactor some stuff + /** * [A simple event dispatcher](http://github.com/therealbush/eventbus-kotlin) * * @author bush * @since 1.0.0 */ -class EventBus(private val logger: Logger = LogManager.getLogger()) { +class EventBus( // todo encapsulation?? + internal val logger: Logger = LogManager.getLogger("EVENTBUS"), + internal val externalSupport: Boolean = true +) { private val listeners = hashMapOf<KClass<*>, ListenerGroup>() private val subscribers = mutableSetOf<Any>() /** - * blah blah annotation properties override listener properties + * doc */ infix fun subscribe(subscriber: Any): Boolean { return if (subscriber in subscribers) false @@ -38,15 +43,15 @@ class EventBus(private val logger: Logger = LogManager.getLogger()) { */ fun register(listener: Listener, subscriber: Any = Any()): Any { listener.subscriber = subscriber - listeners.computeIfAbsent(listener.type, ::ListenerGroup).addListener(listener) + listeners.computeIfAbsent(listener.type) { ListenerGroup(CancelledState.cancelStateOf(listener.type, this)) } + .addListener(listener) subscribers += subscriber return subscriber } /** - * + * doc */ - // doc infix fun unsubscribe(subscriber: Any) = subscribers.remove(subscriber).apply { if (this) listeners.entries.removeIf { it.value.removeFrom(subscriber) @@ -55,10 +60,10 @@ class EventBus(private val logger: Logger = LogManager.getLogger()) { } /** - * Posts an event. + * Posts an event. doc */ - // doc infix fun post(event: Any) = listeners[event::class]?.let { group -> + // TODO: 3/30/2022 rewrite this all lol priority isn't even set up yet group.sequential.forEach { if (!group.cancelState.isCancelled(event) || it.receiveCancelled) { it.listener(event) diff --git a/src/main/kotlin/me/bush/illnamethislater/Listener.kt b/src/main/kotlin/me/bush/illnamethislater/Listener.kt index 9e7e25e..f7a2a81 100644 --- a/src/main/kotlin/me/bush/illnamethislater/Listener.kt +++ b/src/main/kotlin/me/bush/illnamethislater/Listener.kt @@ -3,32 +3,35 @@ package me.bush.illnamethislater import kotlin.reflect.KClass /** - * This class is not intended to be used externally, use [listener] instead. - * - * You *could* use this, and it would work fine, however - * [listener] lets us use reified types for a cleaner look. + * This class is not intended to be used externally, use [listener] instead. You *could* use this, + * and it would work fine however you would have to specify the type explicitly. (ew!) * * @author bush * @since 1.0.0 */ class Listener @PublishedApi internal constructor( listener: (Nothing) -> Unit, + internal val type: KClass<*>, internal var priority: Int = 0, internal var parallel: Boolean = false, - internal var receiveCancelled: Boolean = false, - internal val type: KClass<*> + internal var receiveCancelled: Boolean = false ) { @Suppress("UNCHECKED_CAST") + // Generics have no benefit here, + // it is easier just to force cast. internal val listener = listener as (Any) -> Unit internal lateinit var subscriber: Any } /** + * doc later * + * @author bush + * @since 1.0.0 */ inline fun <reified T : Any> listener( priority: Int = 0, parallel: Boolean = false, receiveCancelled: Boolean = false, noinline listener: (T) -> Unit -) = Listener(listener, priority, parallel, receiveCancelled, T::class) +) = Listener(listener, T::class, priority, parallel, receiveCancelled) diff --git a/src/main/kotlin/me/bush/illnamethislater/ListenerGroup.kt b/src/main/kotlin/me/bush/illnamethislater/ListenerGroup.kt index d8003a4..cef2a0c 100644 --- a/src/main/kotlin/me/bush/illnamethislater/ListenerGroup.kt +++ b/src/main/kotlin/me/bush/illnamethislater/ListenerGroup.kt @@ -1,16 +1,14 @@ package me.bush.illnamethislater import java.util.concurrent.CopyOnWriteArrayList -import kotlin.reflect.KClass /** * @author bush * @since 1.0.0 */ -internal class ListenerGroup(type: KClass<*>) { +internal class ListenerGroup(val cancelState: CancelledState) { val parallel = CopyOnWriteArrayList<Listener>() val sequential = CopyOnWriteArrayList<Listener>() - val cancelState = cancelStateOf(type) val isEmpty get() = parallel.isEmpty() && sequential.isEmpty() diff --git a/src/main/kotlin/me/bush/illnamethislater/ReflectUtil.kt b/src/main/kotlin/me/bush/illnamethislater/ReflectUtil.kt new file mode 100644 index 0000000..d03fe58 --- /dev/null +++ b/src/main/kotlin/me/bush/illnamethislater/ReflectUtil.kt @@ -0,0 +1,57 @@ +package me.bush.illnamethislater + +import java.lang.reflect.Modifier +import kotlin.reflect.KCallable +import kotlin.reflect.KClass +import kotlin.reflect.KProperty +import kotlin.reflect.full.allSuperclasses +import kotlin.reflect.full.declaredMembers +import kotlin.reflect.jvm.isAccessible +import kotlin.reflect.jvm.javaField +import kotlin.reflect.jvm.javaGetter +import kotlin.reflect.typeOf + +/** + * Using [KClass.members] only returns public members, and using [KClass.declaredMembers] + * doesn't return inherited members. This returns all members, private and inherited. + * + * @author bush + * @since 1.0.0 + */ +internal val <T : Any> KClass<T>.allMembers + get() = (declaredMembers + allSuperclasses.flatMap { it.declaredMembers }).asSequence() + +/** + * Checks if a [KCallable] is static on the jvm, and handles invocation accordingly. + * I am not aware of a better alternative that works with `object` classes. + * + * @author bush + * @since 1.0.0 + */ +internal fun <R> KCallable<R>.handleCall(receiver: Any? = null): R { + isAccessible = true + return if (static) call() else call(receiver) +} + +/** + * Checks if the calling [KCallable] is a static java field. Because Kotlin likes to be funny, properties + * belonging to `object` classes are static, but their getters are not. If there is a getter (the property + * is not private), we will be accessing that, otherwise we check if the field is static with Java reflection. + * This also lets us support static listeners in Java code. + * + * @author bush + * @since 1.0.0 + */ +internal val KCallable<*>.static + get() = if (this !is KProperty<*> || javaGetter != null) false + else javaField?.let { Modifier.isStatic(it.modifiers) } ?: false + +/** + * Finds all listeners in a class. (properties and methods) + * + * @author bush + * @since 1.0.0 + */ +@Suppress("UNCHECKED_CAST") // This cannot fail +internal inline val KClass<*>.listeners + get() = allMembers.filter { it.returnType == typeOf<Listener>() } as Sequence<KCallable<Listener>> diff --git a/src/main/kotlin/me/bush/illnamethislater/Util.kt b/src/main/kotlin/me/bush/illnamethislater/Util.kt deleted file mode 100644 index ab486ab..0000000 --- a/src/main/kotlin/me/bush/illnamethislater/Util.kt +++ /dev/null @@ -1,83 +0,0 @@ -package me.bush.illnamethislater - -import sun.misc.Unsafe -import java.lang.reflect.Modifier -import kotlin.reflect.* -import kotlin.reflect.full.allSuperclasses -import kotlin.reflect.full.declaredMembers -import kotlin.reflect.full.isSubclassOf -import kotlin.reflect.full.isSubtypeOf -import kotlin.reflect.jvm.javaField -import kotlin.reflect.jvm.javaGetter - -// author bush -// since 1.0.0 - -/** - * Using [KClass.members] only returns public members, and - * using [KClass.declaredMembers] doesn't return inherited - * members. This returns all members, private and inherited. - */ -internal val <T : Any> KClass<T>.allMembers - get() = (declaredMembers + allSuperclasses.flatMap { it.declaredMembers }).asSequence() - -/** - * Checks if a [KCallable] is static on the jvm, and handles invocation accordingly. - * - * I am not aware of a better alternative that works with `object` classes. - */ -internal fun <R> KCallable<R>.handleCall(receiver: Any) = if (static) call() else call(receiver) - -/** - * Checks if the calling [KCallable] is a static java field. - * - * Because kotlin likes to be funny, properties belonging to - * `object` classes are static, but their getters are not. - * - * If there is a getter (the property is not private), - * we will be accessing it through that, so we can stop checking. - * - * Otherwise, we check if the field is static with java reflection. - */ -internal val KCallable<*>.static - get() = if (this !is KProperty<*> || javaGetter != null) false - else javaField?.let { Modifier.isStatic(it.modifiers) } ?: false - -/** - * Finds all listeners in a class. (properties and methods) - */ -@Suppress("UNCHECKED_CAST") // This cannot fail -internal inline val KClass<*>.listeners - get() = allMembers.filter { it.returnType == typeOf<Listener>() } as Sequence<KCallable<Listener>> - -internal fun interface CancelledState { - fun isCancelled(event: Any): Boolean -} - -private val unsafe = Unsafe::class.java.getDeclaredField("theUnsafe").let { - it.isAccessible = true - it.get(null) as Unsafe -} - -private val offsetMap = hashMapOf<KClass<*>, Long>() - -private val possibleNames = arrayOf("canceled", "cancelled") - -private val NOT_CANCELLABLE = CancelledState { false } - -internal fun cancelStateOf(type: KClass<*>) = when { - type.isSubclassOf(Event::class) -> CancelledState { (it as Event).cancelled } - else -> findCancelField(type)?.let { - offsetMap[type] = unsafe.objectFieldOffset(it.javaField) - CancelledState { event -> unsafe.getBoolean(event, offsetMap[type]!!) } - } ?: NOT_CANCELLABLE -} - -private fun findCancelField(type: KClass<*>) = type.allMembers - .filter { it.name in possibleNames && it.returnType == typeOf<Boolean>() } - .filterIsInstance<KMutableProperty1<*, *>>().toList().let { - if (it.isEmpty()) null else { - if (it.size != 1) TODO() // warn - it[0] - } - } diff --git a/src/test/kotlin/Main.kt b/src/test/kotlin/Main.kt deleted file mode 100644 index 7000c2d..0000000 --- a/src/test/kotlin/Main.kt +++ /dev/null @@ -1,66 +0,0 @@ -import me.bush.illnamethislater.* -import org.apache.logging.log4j.Level -import org.apache.logging.log4j.LogManager -import org.apache.logging.log4j.core.config.Configurator - -/** - * @author bush - * @since 1.0.0 - */ -fun main() { - Configurator.setRootLevel(Level.INFO) - - EventBus().run { - - subscribe(Subscriber()) - - post(External()) - -// val key = register(listener<Int> { -// println(it) -// }) -// -// val topLevelListenerKey = register(topLevelListener()) -// -// unsubscribe(key) -// -// unsubscribe(topLevelListenerKey) -// -// debugInfo() - } - -} - -fun topLevelListener() = listener<Int> { - println("topLevelListener(): $it") -} - -class Subscriber { - - val listener0 = listener<External>(500) { - println("listener 0") - println(it.canceled) - it.canceled = true - } - - val listener1 = listener<External>(250, receiveCancelled = true) { - println("listener 1") - println(it.canceled) - it.canceled = false - } - - val listener2 get() = listener<External> { - println("listener 2") - println(it.canceled) - it.canceled = true - } - - fun listener3() = listener<External>(-250) { - println("listener 3") - println(it.canceled) - } -} - -class External { - var canceled = false -} diff --git a/src/test/kotlin/Test.kt b/src/test/kotlin/Test.kt new file mode 100644 index 0000000..e7fb4cc --- /dev/null +++ b/src/test/kotlin/Test.kt @@ -0,0 +1,151 @@ +import me.bush.illnamethislater.Event +import me.bush.illnamethislater.EventBus +import me.bush.illnamethislater.listener +import org.apache.logging.log4j.Level +import org.apache.logging.log4j.LogManager +import org.apache.logging.log4j.core.config.Configurator +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.api.TestInstance.Lifecycle +import kotlin.random.Random + +/** + * I don't know how to do these.... + * + * @author bush + * @since 1.0.0 + */ +@TestInstance(Lifecycle.PER_CLASS) +class Test { + lateinit var eventBus: EventBus + private val logger = LogManager.getLogger() + + @BeforeAll + fun setup() { + Configurator.setRootLevel(Level.ALL) + + logger.info("Initializing") + eventBus = EventBus() + + logger.info("Logging empty debug info") + eventBus.debugInfo() + + logger.info("Subscribing") + eventBus.subscribe(this) + + logger.info("Testing Events") + } + + // Tests autoboxing and internal data structure against primitives. + @Test + fun primitiveListenerTest() { + val random = Random.nextInt() + eventBus.post(random) + Assertions.assertEquals(random, primitiveTestValue) + } + + var primitiveTestValue = 0 + + val primitiveListener = listener<Int> { + primitiveTestValue = it + } + + // Tests unsubscribing of "free" listeners which don't belong to a subscriber. todo allow keys to be resubscribed and test top level listeners + @Test + fun freeListenerTest() { + // Register "free" listener, and keep the returned key + val key = eventBus.register(listener<String> { + freeListenerTestValue = it + }) + val valueOne = "i love bush's eventbus <3" + val valueTwo = "sdklasdjsakdsadlksadlksdl" + // Will change the value + eventBus.post(valueOne) + Assertions.assertEquals(valueOne, freeListenerTestValue) + // Remove the listener + eventBus.unsubscribe(key) + // No effect + eventBus.post(valueTwo) + // Value will not change + Assertions.assertEquals(valueOne, freeListenerTestValue) + } + + var freeListenerTestValue: String? = null + + // Tests priority and receiveCancelled functionality. + @Test + fun myEventListenerTest() { + val event = MyEvent() + eventBus.post(event) + Assertions.assertEquals(event.lastListener, "myEventListener3") + } + + // First to be called; highest priority. + val myEventListener0 = listener<MyEvent>(priority = 10) { + Assertions.assertEquals(it.lastListener, "") + it.lastListener = "myEventListener0" + it.cancel() + } + + // Will not be called; second-highest priority, no receiveCancelled. + val myEventListener1 + get() = listener<MyEvent>(priority = 0) { + Assertions.assertTrue(false) + } + + // Second to be called; has receiveCancelled and can un-cancel the event. + fun myEventListener2() = listener<MyEvent>(priority = Int.MIN_VALUE + 100, receiveCancelled = true) { + Assertions.assertEquals(it.lastListener, "myEventListener0") + it.lastListener = "myEventListener2" + it.cancelled = false + } + + // Last to be called; does not have receiveCancelled, but the last listener un-cancelled the event. + fun myEventListener3() = listener<MyEvent>(priority = Int.MIN_VALUE) { + Assertions.assertEquals(it.lastListener, "myEventListener2") + it.lastListener = "myEventListener3" + } + + // Tests external event cancel state functionality. + @Test + fun externalEventListenerTest() { + + } + + // Tests parallel invocation functionality. todo how will this work with cancellability + @Test + fun parallelListenerTest() { + + } + + // Tests reflection against singleton object classes. + @Test + fun objectSubscriberTest() { + + } + + // todo what else? +} + +// todo test changing cancellability +class MyEvent : Event() { + override val cancellable = true + + var lastListener = "" +} + +class ExternalEvent0 { + var canceled = false +} + +class ExternalEvent1 { + var cancelled = false +} + +// Should give us a warning about duplicates +class ExternalEvent2 { + var canceled = false + var cancelled = false +} |