diff options
author | therealbush <therealbush@users.noreply.github.com> | 2022-04-02 00:17:29 -1000 |
---|---|---|
committer | therealbush <therealbush@users.noreply.github.com> | 2022-04-02 00:17:29 -1000 |
commit | b196c7c2fe06d84e862648a8bf25058f72c6c0b1 (patch) | |
tree | 8ff36c4f86705eb955e6d38759a5fcc014f7e75a | |
parent | 24c23176f66e3474d5daa7a30242f08f28a07edf (diff) | |
download | eventbus-kotlin-b196c7c2fe06d84e862648a8bf25058f72c6c0b1.tar.gz eventbus-kotlin-b196c7c2fe06d84e862648a8bf25058f72c6c0b1.tar.bz2 eventbus-kotlin-b196c7c2fe06d84e862648a8bf25058f72c6c0b1.zip |
Nearly done
-rw-r--r-- | src/main/kotlin/me/bush/illnamethislater/CancelledState.kt | 8 | ||||
-rw-r--r-- | src/main/kotlin/me/bush/illnamethislater/Event.kt | 2 | ||||
-rw-r--r-- | src/main/kotlin/me/bush/illnamethislater/EventBus.kt | 85 | ||||
-rw-r--r-- | src/main/kotlin/me/bush/illnamethislater/Listener.kt | 40 | ||||
-rw-r--r-- | src/main/kotlin/me/bush/illnamethislater/ListenerGroup.kt | 12 | ||||
-rw-r--r-- | src/test/java/TestJava.java | 80 | ||||
-rw-r--r-- | src/test/kotlin/TestKotlin.kt (renamed from src/test/kotlin/Test.kt) | 50 |
7 files changed, 215 insertions, 62 deletions
diff --git a/src/main/kotlin/me/bush/illnamethislater/CancelledState.kt b/src/main/kotlin/me/bush/illnamethislater/CancelledState.kt index 80022ba..d6ff39d 100644 --- a/src/main/kotlin/me/bush/illnamethislater/CancelledState.kt +++ b/src/main/kotlin/me/bush/illnamethislater/CancelledState.kt @@ -19,12 +19,14 @@ internal fun interface CancelledState { /** * Returns whether [event] is cancelled or not. [event] should only ever be of the type - * that was passed to [CancelledState.of], or this will cause an error. + * that was passed to [CancelledState.of], **or this will crash.** */ fun isCancelled(event: Any): Boolean companion object { - private val UNSAFE = Unsafe::class.declaredMembers.single { it.name == "theUnsafe" }.handleCall() as Unsafe + private val UNSAFE = runCatching { + Unsafe::class.declaredMembers.single { it.name == "theUnsafe" }.handleCall() as Unsafe + }.getOrNull() // soy jvm private val CANCELLED_NAMES = arrayOf("canceled", "cancelled") private val NOT_CANCELLABLE = CancelledState { false } private val OFFSETS = hashMapOf<KClass<*>, Long>() @@ -40,7 +42,7 @@ internal fun interface CancelledState { // Find a field named "cancelled" or "canceled" that is a boolean, and has a backing field. type.allMembers.filter { it.name in CANCELLED_NAMES && it.returnType == typeOf<Boolean>() } .filterIsInstance<KMutableProperty<*>>().filter { it.javaField != null }.toList().let { - if (it.isEmpty()) return NOT_CANCELLABLE + if (it.isEmpty() || UNSAFE == null) return NOT_CANCELLABLE if (it.size != 1) config.logger.warn("Multiple possible cancel fields found for event type $type") it[0].javaField!!.let { field -> if (Modifier.isStatic(field.modifiers)) OFFSETS[type] = UNSAFE.staticFieldOffset(field) diff --git a/src/main/kotlin/me/bush/illnamethislater/Event.kt b/src/main/kotlin/me/bush/illnamethislater/Event.kt index 476a34a..2881c0e 100644 --- a/src/main/kotlin/me/bush/illnamethislater/Event.kt +++ b/src/main/kotlin/me/bush/illnamethislater/Event.kt @@ -23,7 +23,7 @@ abstract class Event { /** * Determines if this event can be [cancelled]. This does not have to return a constant value. */ - abstract val cancellable: Boolean + protected abstract val cancellable: Boolean /** * Sets [cancelled] to true. diff --git a/src/main/kotlin/me/bush/illnamethislater/EventBus.kt b/src/main/kotlin/me/bush/illnamethislater/EventBus.kt index 6b9b0bf..5b278e3 100644 --- a/src/main/kotlin/me/bush/illnamethislater/EventBus.kt +++ b/src/main/kotlin/me/bush/illnamethislater/EventBus.kt @@ -3,31 +3,32 @@ package me.bush.illnamethislater import kotlin.reflect.KClass /** - * A simple event dispatcher - * - * [Information and examples](https://github.com/therealbush/eventbus-kotlin#tododothething) + * [A simple event dispatcher.](https://github.com/therealbush/eventbus-kotlin#tododothething) * * @author bush * @since 1.0.0 */ class EventBus(private val config: Config = Config()) { private val listeners = hashMapOf<KClass<*>, ListenerGroup>() - private val subscribers = mutableSetOf<Any>() + private val subscribers = hashMapOf<Any, List<Listener>>() /** - * doc + * Searches [subscriber] for members that return [Listener] and registers them. + * + * This will not find top level listeners, use [register] instead. + * + * Returns `false` if [subscriber] was already subscribed, `true` otherwise. * * [Information and examples](https://github.com/therealbush/eventbus-kotlin#tododothething) */ fun subscribe(subscriber: Any): Boolean { return if (subscriber in subscribers) false else runCatching { - subscriber::class.listeners.forEach { member -> - register(member.handleCall(subscriber).also { - it.subscriber = subscriber - }) - } - subscribers.add(subscriber) + // Register every listener into a group, but also + // keep a separate list just for this subscriber. + subscribers[subscriber] = subscriber::class.listeners.map { member -> + register(member.handleCall(subscriber).also { it.subscriber = subscriber }) + }.toList() true }.getOrElse { config.logger.error("Unable to register listeners for subscriber $subscriber", it) @@ -36,40 +37,64 @@ class EventBus(private val config: Config = Config()) { } /** - * doc + * Unregisters all listeners belonging to [subscriber]. + * + * This will not remove top level listeners, use [unregister] instead. + * + * Returns `true` if [subscriber] was subscribed, `false` otherwise. * * [Information and examples](https://github.com/therealbush/eventbus-kotlin#tododothething) */ fun unsubscribe(subscriber: Any): Boolean { - return subscribers.remove(subscriber).also { contains -> - if (contains) listeners.entries.removeIf { - it.value.unsubscribe(subscriber) - it.value.sequential.isEmpty() && it.value.parallel.isEmpty() - } + val contained = subscriber in subscribers + // Unregister every listener for this subscriber, + // and return null so the map entry is removed. + subscribers.computeIfPresent(subscriber) { _, listeners -> + listeners.forEach { unregister(it) } + null } + return contained } /** - * Registers a listener (which may not belong to any subscriber) to this [EventBus]. If no object - * is given, a key will be returned which can be used in [unsubscribe] to remove the listener. + * Registers a [Listener] to this [EventBus]. * * [Information and examples](https://github.com/therealbush/eventbus-kotlin#tododothething) */ - fun register(listener: Listener) = listener.also { - listeners.computeIfAbsent(it.type) { type -> ListenerGroup(type, config) }.register(it) + fun register(listener: Listener): Listener { + listeners.computeIfAbsent(listener.type) { + ListenerGroup(it, config) + }.register(listener) + return listener } /** - * doc + * Unregisters a [Listener] from this [EventBus]. * * [Information and examples](https://github.com/therealbush/eventbus-kotlin#tododothething) */ - fun unregister(listener: Listener) = listener.also { - listeners[it.type]?.unregister(it) + fun unregister(listener: Listener): Boolean { + return listeners[listener.type]?.let { + val contained = it.unregister(listener) + if (it.parallel.isEmpty() && it.sequential.isEmpty()) { + listeners.remove(listener.type) + } + contained + } ?: false } /** - * Posts an event. doc + * Posts an [event] to every listener that accepts its type. + * + * Events are **not** queued: only listeners subscribed currently will be called. + * + * If [event] is a subclass of [Event], or has a field-backed mutable boolean property + * named "cancelled" or "canceled" and [Config.thirdPartyCompatibility] is `true`, + * it can be cancelled by a listener, and only future listeners with [Listener.receiveCancelled] + * will receive it. + * + * Sequential listeners are called in the order of [Listener.priority], and parallel + * listeners are called before or after, depending on the value of [Config.parallelFirst]. * * [Information and examples](https://github.com/therealbush/eventbus-kotlin#tododothething) */ @@ -79,13 +104,15 @@ class EventBus(private val config: Config = Config()) { * Logs the subscriber count, total listener count, and listener count for every event type with at * least one subscriber to [Config.logger]. Per-event counts are sorted from greatest to least listeners. * + * **This may cause a [ConcurrentModificationException] if [register] or [subscribe] is called in parallel.** + * * [Information and examples](https://github.com/therealbush/eventbus-kotlin#tododothething) * ``` * Subscribers: 5 - * Listeners: 8 sequential, 4 parallel - * BushIsSoCool: 4, 2 - * OtherEvent: 3, 1 - * String: 1, 1 + * Listeners: 8 sequential, 21 parallel + * BushIsSoCool: 4, 9 + * OtherEvent: 1, 10 + * String: 3, 0 */ fun debugInfo() { config.logger.info("Subscribers: ${subscribers.size}") diff --git a/src/main/kotlin/me/bush/illnamethislater/Listener.kt b/src/main/kotlin/me/bush/illnamethislater/Listener.kt index 7105baf..b2bb2ce 100644 --- a/src/main/kotlin/me/bush/illnamethislater/Listener.kt +++ b/src/main/kotlin/me/bush/illnamethislater/Listener.kt @@ -1,5 +1,6 @@ package me.bush.illnamethislater +import java.util.function.Consumer import kotlin.reflect.KClass /** @@ -26,16 +27,15 @@ class Listener @PublishedApi internal constructor( } /** - * Creates a listener that can be held in a variable, returned from - * a function or getter, or directly registered to an Eventbus. + * Creates a listener that can be held in a variable or returned from a function + * or getter belonging to an object to be subscribed with [EventBus.subscribe], + * or directly registered to an [EventBus] with [EventBus.register]. * * [Information and examples](https://github.com/therealbush/eventbus-kotlin#tododothething) * - * @param T The type of event to listen for. Inheritance has no effect here. - * If [T] is a base class, subclass events will not be received. - * @param priority The priority of this listener. This can be any integer. - * Listeners with a higher [priority] will be invoked first. - * @param parallel If a listener should be invoked in parallel with other [parallel] listeners, or sequentially. todo finish parallel + * @param T The **exact** (no inheritance) type of event to listen for. + * @param priority The priority of this listener, high to low. + * @param parallel If a listener should be invoked in parallel with other parallel listeners, or sequentially. * @param receiveCancelled If a listener should receive events that have been cancelled by previous listeners. * @param listener The body of the listener that will be invoked. */ @@ -45,3 +45,29 @@ inline fun <reified T : Any> listener( receiveCancelled: Boolean = false, noinline listener: (T) -> Unit ) = Listener(listener, T::class, priority, parallel, receiveCancelled) + +/** + * **This function is intended for use in Java code.** + * + * Creates a listener that can be held in a variable or returned from a function + * or getter belonging to an object to be subscribed with [EventBus.subscribe], + * or directly registered to an [EventBus] with [EventBus.register]. + * + * [Information and examples](https://github.com/therealbush/eventbus-kotlin#tododothething) + * + * @param type The **exact** (no inheritance) type of event to listen for. + * @param priority The priority of this listener, high to low. + * @param parallel If a listener should be invoked in parallel with other parallel listeners, or sequentially. + * @param receiveCancelled If a listener should receive events that have been cancelled by previous listeners. + * @param listener The body of the listener that will be invoked. + */ +@JvmOverloads +fun <T : Any> listener( + type: Class<T>, + priority: Int = 0, + parallel: Boolean = false, + receiveCancelled: Boolean = false, + // This might introduce some overhead, but its worth + // not manually having to return "Kotlin.UNIT" from every Java listener. + listener: Consumer<T> +) = Listener({ event: T -> listener.accept(event) }, type.kotlin, priority, parallel, receiveCancelled) diff --git a/src/main/kotlin/me/bush/illnamethislater/ListenerGroup.kt b/src/main/kotlin/me/bush/illnamethislater/ListenerGroup.kt index 7b2a039..fdcc7bc 100644 --- a/src/main/kotlin/me/bush/illnamethislater/ListenerGroup.kt +++ b/src/main/kotlin/me/bush/illnamethislater/ListenerGroup.kt @@ -32,20 +32,12 @@ internal class ListenerGroup( /** * Removes [listener] from this [ListenerGroup]. */ - fun unregister(listener: Listener) { - if (listener.parallel) parallel.remove(listener) + fun unregister(listener: Listener): Boolean { + return if (listener.parallel) parallel.remove(listener) else sequential.remove(listener) } /** - * Removes every listener whose subscriber is [subscriber]. - */ - fun unsubscribe(subscriber: Any) { - parallel.removeIf { it.subscriber == subscriber } - sequential.removeIf { it.subscriber == subscriber } - } - - /** * Posts an event to every listener. Returns true of the event was cancelled. */ fun post(event: Any): Boolean { diff --git a/src/test/java/TestJava.java b/src/test/java/TestJava.java new file mode 100644 index 0000000..c6c77a8 --- /dev/null +++ b/src/test/java/TestJava.java @@ -0,0 +1,80 @@ +import me.bush.illnamethislater.Listener; +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.Logger; +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.TestInstance; +import org.junit.jupiter.api.TestInstance.Lifecycle; +import me.bush.illnamethislater.EventBus; +import org.apache.logging.log4j.LogManager; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; + +import static me.bush.illnamethislater.ListenerKt.listener; + +/** + * I was getting noclassdeffound when trying to load this Java + * class in the other test and I don't care enough to fix it. + * + * @author bush + * @since 1.0.0 + */ +@TestInstance(Lifecycle.PER_CLASS) +public class TestJava { + private static boolean thisShouldChange; + private boolean thisShouldChangeToo; + private EventBus eventBus; + private final Logger logger = LogManager.getLogger(); + + @BeforeAll + public void setup() { + Configurator.setRootLevel(Level.ALL); + + // Test that init works + logger.info("Initializing"); + eventBus = new EventBus(); + + // Ensure no npe + logger.info("Logging empty debug info"); + eventBus.debugInfo(); + + // Test that basic subscribing reflection works + logger.info("Subscribing"); + eventBus.subscribe(this); + + logger.info("Testing Events"); + } + + @AfterAll + public void unsubscribe() { + logger.info("Unsubscribing"); + eventBus.unsubscribe(this); + eventBus.debugInfo(); + } + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + @Test + public void javaSubscriberTest() { + eventBus.subscribe(this); + eventBus.post(new MyEvent()); + Assertions.assertTrue(TestJava.thisShouldChange); + Assertions.assertTrue(this.thisShouldChangeToo); + // TODO: 4/2/2022 fix calling from java + } + + public Listener listener = listener(MyEvent.class, 200, event -> { + Assertions.assertEquals(event.getSomeString(), "donda"); + event.setSomeString("donda 2"); + this.thisShouldChangeToo = true; + }); + + public static Listener someStaticListener() { + return listener(MyEvent.class, 100, event -> { + Assertions.assertEquals(event.getSomeString(), "donda 2"); + thisShouldChange = true; + }); + } +} diff --git a/src/test/kotlin/Test.kt b/src/test/kotlin/TestKotlin.kt index 2bdb445..2ae6313 100644 --- a/src/test/kotlin/Test.kt +++ b/src/test/kotlin/TestKotlin.kt @@ -4,6 +4,7 @@ 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.AfterAll import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.Test @@ -44,6 +45,13 @@ class Test { logger.info("Testing Events") } + @AfterAll + fun unsubscribe() { + logger.info("Unsubscribing") + eventBus.unsubscribe(this) + eventBus.debugInfo() + } + //////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -138,13 +146,26 @@ class Test { // Tests external event cancel state functionality. @Test fun externalEventListenerTest() { - + var unchanged = "this will not change" + // Cancels the event + eventBus.register(listener<ExternalEvent>(200) { + it.canceled = true + }) + // This shouldn't be called + eventBus.register(listener<ExternalEvent>(100) { + unchanged = "changed" + }) + eventBus.post(ExternalEvent()) + Assertions.assertEquals(unchanged, "this will not change") + // Tests that duplicates are detected, and that both + // "canceled" and "cancelled" are detected as valid fields + eventBus.register(listener<ExternalDuplicates> {}) } //////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////// - // Tests parallel invocation functionality. todo how will this work with cancellability + // Tests parallel invocation functionality. @Test fun parallelListenerTest() { @@ -156,34 +177,39 @@ class Test { // Tests reflection against singleton object classes. @Test fun objectSubscriberTest() { - + eventBus.subscribe(ObjectSubscriber) + eventBus.post(Unit) + Assertions.assertTrue(ObjectSubscriber.willChange) } //////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - // todo what else? + // todo test thread safety // todo ensure boolean functions return proper value (subscribe, unsubscribe, etc) } -// todo test changing cancellability +object ObjectSubscriber { + var willChange = false + + fun listener() = listener<Unit> { + willChange = true + } +} + class MyEvent : Event() { override val cancellable = true var lastListener = "" + var someString = "donda" } -class ExternalEvent0 { +class ExternalEvent { var canceled = false } -class ExternalEvent1 { - var cancelled = false -} - // Should give us a warning about duplicates -class ExternalEvent2 { +class ExternalDuplicates { var canceled = false var cancelled = false } |