From 1d2102d729d6c3bfed15018653c8b85f98d9ccff Mon Sep 17 00:00:00 2001 From: kske Date: Fri, 2 Oct 2020 17:50:11 +0200 Subject: [PATCH 1/5] Add event cancellation mechanism to EventBus --- src/main/java/dev/kske/eventbus/EventBus.java | 45 ++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/src/main/java/dev/kske/eventbus/EventBus.java b/src/main/java/dev/kske/eventbus/EventBus.java index 98114fc..628e0b9 100644 --- a/src/main/java/dev/kske/eventbus/EventBus.java +++ b/src/main/java/dev/kske/eventbus/EventBus.java @@ -19,6 +19,16 @@ import java.util.concurrent.ConcurrentHashMap; */ public final class EventBus { + /** + * Holds the state of the dispatching process on one thread. + * + * @since 0.1.0 + */ + private static final class DispatchState { + + boolean isDispatching, isCancelled; + } + private static volatile EventBus singletonInstance; private static final Logger logger = System.getLogger(EventBus.class.getName()); @@ -44,6 +54,8 @@ public final class EventBus { private final Map, TreeSet> bindings = new ConcurrentHashMap<>(); private final Set registeredListeners = ConcurrentHashMap.newKeySet(); + private final ThreadLocal dispatchState + = ThreadLocal.withInitial(DispatchState::new); /** * Dispatches an event to all event handlers registered for it in descending order of their @@ -55,7 +67,24 @@ public final class EventBus { public void dispatch(IEvent event) { Objects.requireNonNull(event); logger.log(Level.INFO, "Dispatching event {0}", event); - getHandlersFor(event.getClass()).forEach(handler -> handler.execute(event)); + + // Set dispatch state + var state = dispatchState.get(); + state.isDispatching = true; + + for (var handler : getHandlersFor(event.getClass())) + if (state.isCancelled) { + logger.log(Level.INFO, "Cancelled dispatching event {0}", event); + state.isCancelled = false; + break; + } else { + handler.execute(event); + } + + // Reset dispatch state + state.isDispatching = false; + + logger.log(Level.DEBUG, "Finished dispatching event {0}", event); } /** @@ -82,6 +111,20 @@ public final class EventBus { return new ArrayList<>(handlers); } + /** + * Cancels an event that is currently dispatched from inside an event handler. + * + * @throws EventBusException if the calling thread is not an active dispatching thread + * @since 0.1.0 + */ + public void cancel() { + var state = dispatchState.get(); + if (state.isDispatching && !state.isCancelled) + state.isCancelled = true; + else + throw new EventBusException("Calling thread not an active dispatching thread!"); + } + /** * Registers an event listener at this event bus. * From 9d1707de5b31a677a9421663f70bb2915f11ac4f Mon Sep 17 00:00:00 2001 From: delvh Date: Sun, 11 Oct 2020 11:25:18 +0200 Subject: [PATCH 2/5] Add event consumption section to README --- README.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/README.md b/README.md index 07e8240..6b449bb 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,30 @@ private void onSimpleEvent() { Make sure that you **do not** declare both a parameter and the `eventType` value of the annotation, as this would be ambiguous. +## Event consumption + +There are cases when it would be useful to stop event propagation after a certain condition has been fulfilled. +Event Bus provides a mechanism to consume events: + +```java +@Event(eventType = SimpleEvent.class, priority=1000) +private void onSimpleEvent() { + EventBus.getInstance().cancel(); +} + +@Event(eventType = SimpleEvent.class, priority=900) +private void onSimpleEvent2() { + System.out.println("Will not be printed!"); +} +``` + +In this example, the second method will not be executed as the event will no longer be forwarded. +Any event handler with a lower priority than the one canceling it will not get executed. + +**Important:** +Please avoid cancelling events when (multiple) event handlers have the same priority as the one cancelling it: +It is undefined whether those will be executed or not. + ## Installation Event Bus is currently hosted at [kske.dev](https://kske.dev). From 8aefb438234783deff82b04fd698c9c4a486629d Mon Sep 17 00:00:00 2001 From: delvh Date: Mon, 23 Nov 2020 23:42:17 +0100 Subject: [PATCH 3/5] Add Test for Cancellation --- .../java/dev/kske/eventbus/EventBusTest.java | 29 +++++++++++++++---- .../dev/kske/eventbus/SimpleCancelEvent.java | 9 ++++++ 2 files changed, 32 insertions(+), 6 deletions(-) create mode 100644 src/test/java/dev/kske/eventbus/SimpleCancelEvent.java diff --git a/src/test/java/dev/kske/eventbus/EventBusTest.java b/src/test/java/dev/kske/eventbus/EventBusTest.java index c754bb9..1eff726 100644 --- a/src/test/java/dev/kske/eventbus/EventBusTest.java +++ b/src/test/java/dev/kske/eventbus/EventBusTest.java @@ -12,7 +12,8 @@ import org.junit.jupiter.api.*; */ class EventBusTest implements EventListener { - int hits; + int hits; + static int canceledHits; @BeforeEach public void registerListener() { @@ -25,11 +26,15 @@ class EventBusTest implements EventListener { EventBus.getInstance().dispatch(new SimpleEvent()); } - @Event( - eventType = SimpleEvent.class, - includeSubtypes = true, - priority = 200 - ) + @Test + void testCancellation() { + var test2 = new EventBusTest(); + test2.registerListener(); + EventBus.getInstance().dispatch(new SimpleCancelEvent()); + assertTrue(canceledHits == 1); + } + + @Event(eventType = SimpleEvent.class, includeSubtypes = true, priority = 200) private void onSimpleEventFirst() { ++hits; assertTrue(hits == 1 || hits == 2); @@ -46,4 +51,16 @@ class EventBusTest implements EventListener { ++hits; assertEquals(4, hits); } + + @Event(eventType = SimpleCancelEvent.class, priority = 500) + private void onSimpleCancelFirst() { + ++canceledHits; + assertTrue(canceledHits == 1); + EventBus.getInstance().cancel(); + } + + @Event(eventType = SimpleCancelEvent.class, priority = 200) + private void onSimpleCancelSecond() { + fail(); + } } diff --git a/src/test/java/dev/kske/eventbus/SimpleCancelEvent.java b/src/test/java/dev/kske/eventbus/SimpleCancelEvent.java new file mode 100644 index 0000000..63eed2b --- /dev/null +++ b/src/test/java/dev/kske/eventbus/SimpleCancelEvent.java @@ -0,0 +1,9 @@ +package dev.kske.eventbus; + +/** + * A simple event for testing purposes that will get cancelled during propagation. + * + * @author Leon Hofmeister + * @since 0.1 + */ +public class SimpleCancelEvent implements IEvent {} From 659bd7888f992fb9f942e45d091e8a33373bf576 Mon Sep 17 00:00:00 2001 From: kske Date: Wed, 25 Nov 2020 08:35:51 +0100 Subject: [PATCH 4/5] Simplify cancellation test, fix a typo --- src/test/java/dev/kske/eventbus/EventBusTest.java | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/test/java/dev/kske/eventbus/EventBusTest.java b/src/test/java/dev/kske/eventbus/EventBusTest.java index 1eff726..bbf8317 100644 --- a/src/test/java/dev/kske/eventbus/EventBusTest.java +++ b/src/test/java/dev/kske/eventbus/EventBusTest.java @@ -12,8 +12,7 @@ import org.junit.jupiter.api.*; */ class EventBusTest implements EventListener { - int hits; - static int canceledHits; + int hits, cancelledHits; @BeforeEach public void registerListener() { @@ -28,10 +27,8 @@ class EventBusTest implements EventListener { @Test void testCancellation() { - var test2 = new EventBusTest(); - test2.registerListener(); EventBus.getInstance().dispatch(new SimpleCancelEvent()); - assertTrue(canceledHits == 1); + assertTrue(cancelledHits == 1); } @Event(eventType = SimpleEvent.class, includeSubtypes = true, priority = 200) @@ -54,8 +51,8 @@ class EventBusTest implements EventListener { @Event(eventType = SimpleCancelEvent.class, priority = 500) private void onSimpleCancelFirst() { - ++canceledHits; - assertTrue(canceledHits == 1); + ++cancelledHits; + assertTrue(cancelledHits == 1); EventBus.getInstance().cancel(); } From ec73be90462461f0e73763afa4d33c6b2d974a75 Mon Sep 17 00:00:00 2001 From: kske Date: Thu, 26 Nov 2020 08:14:11 +0100 Subject: [PATCH 5/5] Split EventBusTest into DispatchTest and CancelTest, add Javadoc --- .../java/dev/kske/eventbus/CancelTest.java | 52 +++++++++++++++ .../java/dev/kske/eventbus/DispatchTest.java | 58 +++++++++++++++++ .../java/dev/kske/eventbus/EventBusTest.java | 63 ------------------- .../dev/kske/eventbus/SimpleCancelEvent.java | 9 --- 4 files changed, 110 insertions(+), 72 deletions(-) create mode 100644 src/test/java/dev/kske/eventbus/CancelTest.java create mode 100644 src/test/java/dev/kske/eventbus/DispatchTest.java delete mode 100644 src/test/java/dev/kske/eventbus/EventBusTest.java delete mode 100644 src/test/java/dev/kske/eventbus/SimpleCancelEvent.java diff --git a/src/test/java/dev/kske/eventbus/CancelTest.java b/src/test/java/dev/kske/eventbus/CancelTest.java new file mode 100644 index 0000000..8584f48 --- /dev/null +++ b/src/test/java/dev/kske/eventbus/CancelTest.java @@ -0,0 +1,52 @@ +package dev.kske.eventbus; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.*; + +/** + * Tests the event cancellation mechanism of the event bus. + * + * @author Kai S. K. Engelbart + * @author Leon Hofmeister + * @since 0.1.0 + */ +class CancelTest implements EventListener { + + EventBus bus; + int hits; + + /** + * Constructs an event bus and registers this test instance as an event listener. + * + * @since 0.1.0 + */ + @BeforeEach + void registerListener() { + bus = new EventBus(); + bus.registerListener(this); + } + + /** + * Tests {@link EventBus#cancel()} with two event handlers, of which the first cancels the + * event. + * + * @since 0.1.0 + */ + @Test + void testCancellation() { + bus.dispatch(new SimpleEvent()); + assertEquals(1, hits); + } + + @Event(eventType = SimpleEvent.class, priority = 100) + void onSimpleFirst() { + ++hits; + bus.cancel(); + } + + @Event(eventType = SimpleEvent.class, priority = 50) + void onSimpleSecond() { + ++hits; + } +} diff --git a/src/test/java/dev/kske/eventbus/DispatchTest.java b/src/test/java/dev/kske/eventbus/DispatchTest.java new file mode 100644 index 0000000..83c99e7 --- /dev/null +++ b/src/test/java/dev/kske/eventbus/DispatchTest.java @@ -0,0 +1,58 @@ +package dev.kske.eventbus; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.*; + +/** + * Tests the dispatching mechanism of the event bus. + * + * @author Kai S. K. Engelbart + * @since 0.0.1 + */ +class DispatchTest implements EventListener { + + EventBus bus; + static int hits; + + /** + * Constructs an event bus and registers this test instance as an event listener. + * + * @since 0.0.1 + */ + @BeforeEach + void registerListener() { + bus = new EventBus(); + bus.registerListener(this); + } + + /** + * Tests {@link EventBus#dispatch(IEvent)} with multiple handler priorities, a subtype handler + * and a static handler. + * + * @since 0.0.1 + */ + @Test + void testDispatch() { + bus.dispatch(new SimpleEventSub()); + bus.dispatch(new SimpleEvent()); + } + + @Event(eventType = SimpleEvent.class, includeSubtypes = true, priority = 200) + void onSimpleEventFirst() { + ++hits; + assertTrue(hits == 1 || hits == 2); + } + + @Event(eventType = SimpleEvent.class, priority = 150) + static void onSimpleEventSecond() { + ++hits; + assertEquals(3, hits); + } + + @Event(priority = 100) + void onSimpleEventThird(SimpleEvent event) { + ++hits; + assertEquals(4, hits); + } +} diff --git a/src/test/java/dev/kske/eventbus/EventBusTest.java b/src/test/java/dev/kske/eventbus/EventBusTest.java deleted file mode 100644 index bbf8317..0000000 --- a/src/test/java/dev/kske/eventbus/EventBusTest.java +++ /dev/null @@ -1,63 +0,0 @@ -package dev.kske.eventbus; - -import static org.junit.jupiter.api.Assertions.*; - -import org.junit.jupiter.api.*; - -/** - * Tests the of the event bus library. - * - * @author Kai S. K. Engelbart - * @since 0.0.1 - */ -class EventBusTest implements EventListener { - - int hits, cancelledHits; - - @BeforeEach - public void registerListener() { - EventBus.getInstance().registerListener(this); - } - - @Test - void testDispatch() { - EventBus.getInstance().dispatch(new SimpleEventSub()); - EventBus.getInstance().dispatch(new SimpleEvent()); - } - - @Test - void testCancellation() { - EventBus.getInstance().dispatch(new SimpleCancelEvent()); - assertTrue(cancelledHits == 1); - } - - @Event(eventType = SimpleEvent.class, includeSubtypes = true, priority = 200) - private void onSimpleEventFirst() { - ++hits; - assertTrue(hits == 1 || hits == 2); - } - - @Event(eventType = SimpleEvent.class, priority = 150) - private void onSimpleEventSecond() { - ++hits; - assertEquals(3, hits); - } - - @Event(priority = 50) - private void onSimpleEventThird(SimpleEvent event) { - ++hits; - assertEquals(4, hits); - } - - @Event(eventType = SimpleCancelEvent.class, priority = 500) - private void onSimpleCancelFirst() { - ++cancelledHits; - assertTrue(cancelledHits == 1); - EventBus.getInstance().cancel(); - } - - @Event(eventType = SimpleCancelEvent.class, priority = 200) - private void onSimpleCancelSecond() { - fail(); - } -} diff --git a/src/test/java/dev/kske/eventbus/SimpleCancelEvent.java b/src/test/java/dev/kske/eventbus/SimpleCancelEvent.java deleted file mode 100644 index 63eed2b..0000000 --- a/src/test/java/dev/kske/eventbus/SimpleCancelEvent.java +++ /dev/null @@ -1,9 +0,0 @@ -package dev.kske.eventbus; - -/** - * A simple event for testing purposes that will get cancelled during propagation. - * - * @author Leon Hofmeister - * @since 0.1 - */ -public class SimpleCancelEvent implements IEvent {}