aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authortherealbush <therealbush@users.noreply.github.com>2022-04-02 00:17:29 -1000
committertherealbush <therealbush@users.noreply.github.com>2022-04-02 00:17:29 -1000
commitb196c7c2fe06d84e862648a8bf25058f72c6c0b1 (patch)
tree8ff36c4f86705eb955e6d38759a5fcc014f7e75a
parent24c23176f66e3474d5daa7a30242f08f28a07edf (diff)
downloadeventbus-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.kt8
-rw-r--r--src/main/kotlin/me/bush/illnamethislater/Event.kt2
-rw-r--r--src/main/kotlin/me/bush/illnamethislater/EventBus.kt85
-rw-r--r--src/main/kotlin/me/bush/illnamethislater/Listener.kt40
-rw-r--r--src/main/kotlin/me/bush/illnamethislater/ListenerGroup.kt12
-rw-r--r--src/test/java/TestJava.java80
-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
}