aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/main/kotlin/me/bush/illnamethislater/CancelState.kt46
-rw-r--r--src/main/kotlin/me/bush/illnamethislater/CancelledState.kt54
-rw-r--r--src/main/kotlin/me/bush/illnamethislater/Config.kt29
-rw-r--r--src/main/kotlin/me/bush/illnamethislater/Event.kt21
-rw-r--r--src/main/kotlin/me/bush/illnamethislater/EventBus.kt85
-rw-r--r--src/main/kotlin/me/bush/illnamethislater/Listener.kt16
-rw-r--r--src/main/kotlin/me/bush/illnamethislater/ListenerGroup.kt27
-rw-r--r--src/main/kotlin/me/bush/illnamethislater/ReflectUtil.kt16
-rw-r--r--src/test/kotlin/Test.kt38
9 files changed, 217 insertions, 115 deletions
diff --git a/src/main/kotlin/me/bush/illnamethislater/CancelState.kt b/src/main/kotlin/me/bush/illnamethislater/CancelState.kt
deleted file mode 100644
index 270d88f..0000000
--- a/src/main/kotlin/me/bush/illnamethislater/CancelState.kt
+++ /dev/null
@@ -1,46 +0,0 @@
-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/CancelledState.kt b/src/main/kotlin/me/bush/illnamethislater/CancelledState.kt
new file mode 100644
index 0000000..ed2263a
--- /dev/null
+++ b/src/main/kotlin/me/bush/illnamethislater/CancelledState.kt
@@ -0,0 +1,54 @@
+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
+
+/**
+ * A simple SAM interface for determining if an event (or any class) is cancellable.
+ *
+ * @author bush
+ * @since 1.0.0
+ */
+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.
+ */
+ fun isCancelled(event: Any): Boolean
+
+ // Maybe move this to eventbus or util? todo
+ // Make CancelledState a class? todo
+ companion object {
+ private val UNSAFE = Unsafe::class.declaredMembers.single { it.name == "theUnsafe" }.handleCall() as Unsafe
+ private val CANCELLED_NAMES = arrayOf("canceled", "cancelled")
+ private val NOT_CANCELLABLE = CancelledState { false }
+ private val OFFSETS = hashMapOf<KClass<*>, Long>()
+
+ /**
+ * Creates a [CancelledState] object for events of class [type].
+ */
+ // TODO: 3/31/2022 static/singleton fields
+ fun of(type: KClass<*>, config: Config): CancelledState {
+ // Default impl for our event class
+ if (type.isSubclassOf(Event::class)) return CancelledState { (it as Event).cancelled }
+ // If compat is disabled
+ if (!config.thirdPartyCompatibility) return NOT_CANCELLABLE
+ // Find a field named "cancelled" or "canceled"
+ 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) config.logger.warn("Multiple possible cancel fields found for event type $type")
+ OFFSETS[type] = UNSAFE.objectFieldOffset(it[0].javaField)
+ // This is not using reflection, and it is the same speed as direct access.
+ // If you are familiar with C, this is essentially the same idea as pointers.
+ return CancelledState { event -> UNSAFE.getBoolean(event, OFFSETS[type]!!) }
+ }
+ }
+ }
+}
diff --git a/src/main/kotlin/me/bush/illnamethislater/Config.kt b/src/main/kotlin/me/bush/illnamethislater/Config.kt
new file mode 100644
index 0000000..eb9ccf5
--- /dev/null
+++ b/src/main/kotlin/me/bush/illnamethislater/Config.kt
@@ -0,0 +1,29 @@
+package me.bush.illnamethislater
+
+import org.apache.logging.log4j.LogManager
+import org.apache.logging.log4j.Logger
+
+
+/**
+ * A class containing configuration options for an [EventBus].
+ *
+ * [Information and examples](https://github.com/therealbush/eventbus-kotlin#tododothething)
+ *
+ * @author bush
+ * @since 1.0.0
+ */
+data class Config(
+
+ /**
+ * The logger this [EventBus] will use to log errors, or [EventBus.debugInfo]
+ */
+ val logger: Logger = LogManager.getLogger("Eventbus"),
+
+ /**
+ * Whether this [EventBus] should try to find a "cancelled" field in events being listened for that
+ * are not a subclass of [Event]. This is experimental, and should be set to `false` if problems arise.
+ *
+ * [Information and examples](https://github.com/therealbush/eventbus-kotlin#tododothething)
+ */
+ val thirdPartyCompatibility: Boolean = true
+)
diff --git a/src/main/kotlin/me/bush/illnamethislater/Event.kt b/src/main/kotlin/me/bush/illnamethislater/Event.kt
index dedd4df..476a34a 100644
--- a/src/main/kotlin/me/bush/illnamethislater/Event.kt
+++ b/src/main/kotlin/me/bush/illnamethislater/Event.kt
@@ -1,25 +1,34 @@
package me.bush.illnamethislater
/**
- * A base class for events that can be cancelled. More information can be found
- * [here](https://github.com/therealbush/eventbus-kotlin)
+ * A base class for events that can be cancelled.
*
- * 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 previous listener with [Listener.receiveCancelled] sets [cancelled] back to `false`
+ * [Information and examples](https://github.com/therealbush/eventbus-kotlin#ththingtodo)
*
* @author bush
* @since 1.0.0
*/
abstract class Event {
+
+ /**
+ * Whether this event is cancelled or not. If it is, only future listeners with
+ * [Listener.receiveCancelled] will receive it. However, it can be set back to
+ * `false`, and listeners will be able to receive it again.
+ */
var cancelled = false
set(value) {
if (cancellable) field = value
}
+ /**
+ * Determines if this event can be [cancelled]. This does not have to return a constant value.
+ */
abstract val cancellable: Boolean
+ /**
+ * Sets [cancelled] to true.
+ */
fun cancel() {
- cancelled = false
+ cancelled = true
}
}
diff --git a/src/main/kotlin/me/bush/illnamethislater/EventBus.kt b/src/main/kotlin/me/bush/illnamethislater/EventBus.kt
index 13e6b48..90ec395 100644
--- a/src/main/kotlin/me/bush/illnamethislater/EventBus.kt
+++ b/src/main/kotlin/me/bush/illnamethislater/EventBus.kt
@@ -1,7 +1,5 @@
package me.bush.illnamethislater
-import org.apache.logging.log4j.LogManager
-import org.apache.logging.log4j.Logger
import kotlin.reflect.KClass
// TODO: 3/30/2022 Refactor some stuff
@@ -12,25 +10,29 @@ import kotlin.reflect.KClass
* @author bush
* @since 1.0.0
*/
-class EventBus( // todo encapsulation??
- internal val logger: Logger = LogManager.getLogger("EVENTBUS"),
- internal val externalSupport: Boolean = true
+class EventBus(
+ private val config: Config = Config()
) {
private val listeners = hashMapOf<KClass<*>, ListenerGroup>()
private val subscribers = mutableSetOf<Any>()
/**
* doc
+ *
+ * [Information and examples](https://github.com/therealbush/eventbus-kotlin#tododothething)
*/
- infix fun subscribe(subscriber: Any): Boolean {
+ fun subscribe(subscriber: Any): Boolean {
return if (subscriber in subscribers) false
else runCatching {
- subscriber::class.listeners.forEach {
- register(it.handleCall(subscriber), subscriber)
+ subscriber::class.listeners.forEach { member ->
+ register(member.handleCall(subscriber).also {
+ it.subscriber = subscriber
+ })
}
+ subscribers += subscriber
true
}.getOrElse {
- logger.error("Unable to register listeners for subscriber $subscriber", it)
+ config.logger.error("Unable to register listeners for subscriber $subscriber", it)
false
}
}
@@ -39,60 +41,61 @@ class EventBus( // todo encapsulation??
* 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.
*
- * The
+ * [Information and examples](https://github.com/therealbush/eventbus-kotlin#tododothething)
*/
- fun register(listener: Listener, subscriber: Any = Any()): Any {
- listener.subscriber = subscriber
- listeners.computeIfAbsent(listener.type) { ListenerGroup(CancelledState.cancelStateOf(listener.type, this)) }
- .addListener(listener)
- subscribers += subscriber
- return subscriber
+ fun register(listener: Listener): Listener {
+ listeners.computeIfAbsent(listener.type) {
+ ListenerGroup(it, config)
+ }.add(listener)
+ return listener
}
/**
* doc
*
+ * [Information and examples](https://github.com/therealbush/eventbus-kotlin#tododothething)
*/
- infix fun unsubscribe(subscriber: Any) = subscribers.remove(subscriber).apply {
- if (this) listeners.entries.removeIf {
- it.value.removeFrom(subscriber)
- it.value.isEmpty
+ fun unsubscribe(subscriber: Any): Boolean {
+ return subscribers.remove(subscriber).also { contains ->
+ if (contains) listeners.entries.removeIf {
+ it.value.removeFrom(subscriber)
+ it.value.sequential.isEmpty() && it.value.parallel.isEmpty()
+ }
}
}
/**
+ * doc
+ *
+ * [Information and examples](https://github.com/therealbush/eventbus-kotlin#tododothething)
+ */
+ fun unregister(listener: Listener) = listeners[listener.type]?.remove(listener) ?: false
+
+ /**
* Posts an event. doc
+ *
+ * [Information and examples](https://github.com/therealbush/eventbus-kotlin#tododothething)
*/
- 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)
- }
- }
- }
+ fun post(event: Any) = listeners[event::class]?.post(event) ?: false
/**
- * Logs the subscriber count, total listener count, and listener count
- * for every event type with at least one subscriber to [logger].
- * Per-event counts are sorted from greatest to least listeners.
+ * 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.
+ *
+ * [Information and examples](https://github.com/therealbush/eventbus-kotlin#tododothething)
* ```
* Subscribers: 5
* Listeners: 8 sequential, 4 parallel
- * SomeInnerClass: 4, 2
+ * BushIsSoCool: 4, 2
* OtherEvent: 3, 1
* String: 1, 1
*/
- // do lol
-
fun debugInfo() {
- logger.info(StringBuilder().apply {
- append("\nSubscribers: ${subscribers.size}")
- append("\nListeners: ${listeners.values.sumOf { it.sequential.size }} sequential, ${listeners.values.sumOf { it.parallel.size }} parallel")
- listeners.entries.sortedByDescending { it.value.sequential.size + it.value.parallel.size }.forEach {
- append("${it.key.simpleName}: ${it.value.sequential.size}, ${it.value.parallel.size}")
- }
- }.toString())
+ config.logger.info("Subscribers: ${subscribers.size}")
+ val sequential = listeners.values.sumOf { it.sequential.size }
+ val parallel = listeners.values.sumOf { it.parallel.size }
+ config.logger.info("Listeners: $sequential sequential, $parallel parallel")
+ listeners.values.sortedByDescending { it.sequential.size + it.parallel.size }.forEach { it.debugInfo() }
}
}
diff --git a/src/main/kotlin/me/bush/illnamethislater/Listener.kt b/src/main/kotlin/me/bush/illnamethislater/Listener.kt
index f7a2a81..2d96d54 100644
--- a/src/main/kotlin/me/bush/illnamethislater/Listener.kt
+++ b/src/main/kotlin/me/bush/illnamethislater/Listener.kt
@@ -6,6 +6,8 @@ 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 you would have to specify the type explicitly. (ew!)
*
+ * [Information and examples](https://github.com/therealbush/eventbus-kotlin#tododothething)
+ *
* @author bush
* @since 1.0.0
*/
@@ -24,10 +26,18 @@ class Listener @PublishedApi internal constructor(
}
/**
- * doc later
+ * Creates a listener that can be held in a variable, returned from
+ * a function or getter, or directly registered to an Eventbus.
*
- * @author bush
- * @since 1.0.0
+ * [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 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.
*/
inline fun <reified T : Any> listener(
priority: Int = 0,
diff --git a/src/main/kotlin/me/bush/illnamethislater/ListenerGroup.kt b/src/main/kotlin/me/bush/illnamethislater/ListenerGroup.kt
index cef2a0c..56b5421 100644
--- a/src/main/kotlin/me/bush/illnamethislater/ListenerGroup.kt
+++ b/src/main/kotlin/me/bush/illnamethislater/ListenerGroup.kt
@@ -1,24 +1,39 @@
package me.bush.illnamethislater
import java.util.concurrent.CopyOnWriteArrayList
+import kotlin.reflect.KClass
/**
* @author bush
* @since 1.0.0
*/
-internal class ListenerGroup(val cancelState: CancelledState) {
- val parallel = CopyOnWriteArrayList<Listener>()
+internal class ListenerGroup(
+ private val type: KClass<*>,
+ private val config: Config
+) {
+ val cancelledState = CancelledState.of(type, config)
val sequential = CopyOnWriteArrayList<Listener>()
+ val parallel = CopyOnWriteArrayList<Listener>()
- val isEmpty get() = parallel.isEmpty() && sequential.isEmpty()
+ fun add(listener: Listener) {
+ with(if (listener.parallel) parallel else sequential) {
+ add(listener)
+ sortBy { it.priority }
+ }
+ }
- fun addListener(listener: Listener) {
- if (listener.parallel) parallel += listener
- else sequential += listener
+ fun remove(listener: Listener) = with(if (listener.parallel) parallel else sequential) {
+ remove(listener)
}
fun removeFrom(subscriber: Any) {
parallel.removeIf(Listener::subscriber::equals)
sequential.removeIf(Listener::subscriber::equals)
}
+
+ fun post(event: Any): Boolean {
+ return false
+ }
+
+ fun debugInfo() = config.logger.info("${type.simpleName}: ${sequential.size}, ${parallel.size}")
}
diff --git a/src/main/kotlin/me/bush/illnamethislater/ReflectUtil.kt b/src/main/kotlin/me/bush/illnamethislater/ReflectUtil.kt
index d03fe58..ba42f76 100644
--- a/src/main/kotlin/me/bush/illnamethislater/ReflectUtil.kt
+++ b/src/main/kotlin/me/bush/illnamethislater/ReflectUtil.kt
@@ -11,12 +11,11 @@ import kotlin.reflect.jvm.javaField
import kotlin.reflect.jvm.javaGetter
import kotlin.reflect.typeOf
+// by bush, unchanged 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.
- *
- * @author bush
- * @since 1.0.0
*/
internal val <T : Any> KClass<T>.allMembers
get() = (declaredMembers + allSuperclasses.flatMap { it.declaredMembers }).asSequence()
@@ -24,9 +23,6 @@ internal val <T : Any> KClass<T>.allMembers
/**
* 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
@@ -38,19 +34,13 @@ internal fun <R> KCallable<R>.handleCall(receiver: Any? = null): R {
* 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
+ * Finds all members of return type [Listener]. (properties and methods)
*/
@Suppress("UNCHECKED_CAST") // This cannot fail
internal inline val KClass<*>.listeners
diff --git a/src/test/kotlin/Test.kt b/src/test/kotlin/Test.kt
index e7fb4cc..c166cb2 100644
--- a/src/test/kotlin/Test.kt
+++ b/src/test/kotlin/Test.kt
@@ -22,22 +22,40 @@ class Test {
lateinit var eventBus: EventBus
private val logger = LogManager.getLogger()
+ ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
@BeforeAll
fun setup() {
Configurator.setRootLevel(Level.ALL)
+ // Test that init works
logger.info("Initializing")
eventBus = 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")
}
+ ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+ // Tests debug info format
+ @Test
+ fun debugInfoTest() {
+ eventBus.debugInfo()
+ }
+
+ ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
// Tests autoboxing and internal data structure against primitives.
@Test
fun primitiveListenerTest() {
@@ -52,6 +70,9 @@ class Test {
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() {
@@ -74,6 +95,9 @@ class Test {
var freeListenerTestValue: String? = null
+ ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
// Tests priority and receiveCancelled functionality.
@Test
fun myEventListenerTest() {
@@ -108,25 +132,39 @@ class Test {
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 ensure boolean functions return proper value (subscribe, unsubscribe, etc)
}
// todo test changing cancellability