diff --git a/README.md b/README.md index a4a7576..d926edb 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,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). 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. * 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 3a2fd42..0000000 --- a/src/test/java/dev/kske/eventbus/EventBusTest.java +++ /dev/null @@ -1,49 +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 { - - static int hits; - - @BeforeEach - public void registerListener() { - EventBus.getInstance().registerListener(this); - } - - @Test - void testDispatch() { - EventBus.getInstance().dispatch(new SimpleEventSub()); - EventBus.getInstance().dispatch(new SimpleEvent()); - } - - @Event( - eventType = SimpleEvent.class, - includeSubtypes = true, - priority = 200 - ) - private void onSimpleEventFirst() { - ++hits; - assertTrue(hits == 1 || hits == 2); - } - - @Event(eventType = SimpleEvent.class, priority = 150) - private static void onSimpleEventSecond() { - ++hits; - assertEquals(3, hits); - } - - @Event(priority = 50) - private void onSimpleEventThird(SimpleEvent event) { - ++hits; - assertEquals(4, hits); - } -}