diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/main/kotlin/me/bush/illnamethislater/CancelState.kt | 46 | ||||
-rw-r--r-- | src/main/kotlin/me/bush/illnamethislater/CancelledState.kt | 54 | ||||
-rw-r--r-- | src/main/kotlin/me/bush/illnamethislater/Config.kt | 29 | ||||
-rw-r--r-- | src/main/kotlin/me/bush/illnamethislater/Event.kt | 21 | ||||
-rw-r--r-- | src/main/kotlin/me/bush/illnamethislater/EventBus.kt | 85 | ||||
-rw-r--r-- | src/main/kotlin/me/bush/illnamethislater/Listener.kt | 16 | ||||
-rw-r--r-- | src/main/kotlin/me/bush/illnamethislater/ListenerGroup.kt | 27 | ||||
-rw-r--r-- | src/main/kotlin/me/bush/illnamethislater/ReflectUtil.kt | 16 | ||||
-rw-r--r-- | src/test/kotlin/Test.kt | 38 |
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 |