diff --git a/event-bus-core/src/main/java/dev/kske/eventbus/core/EventBus.java b/event-bus-core/src/main/java/dev/kske/eventbus/core/EventBus.java index 45c9ef8..27bb4fb 100644 --- a/event-bus-core/src/main/java/dev/kske/eventbus/core/EventBus.java +++ b/event-bus-core/src/main/java/dev/kske/eventbus/core/EventBus.java @@ -27,7 +27,21 @@ public final class EventBus { */ private static final class DispatchState { - boolean isDispatching, isCancelled; + /** + * Indicates that the last event handler invoked has called {@link EventBus#cancel}. In that + * case, the event is not dispatched further. + * + * @since 0.1.0 + */ + boolean isCancelled; + + /** + * Is incremented when {@link EventBus#dispatch(Object)} is invoked and decremented when it + * finishes. This allows keeping track of nested dispatches. + * + * @since 1.2.0 + */ + int nestingCount; } /** @@ -79,9 +93,11 @@ public final class EventBus { Objects.requireNonNull(event); logger.log(Level.INFO, "Dispatching event {0}", event); - // Set dispatch state + // Look up dispatch state var state = dispatchState.get(); - state.isDispatching = true; + + // Increment nesting count (becomes > 1 during nested dispatches) + ++state.nestingCount; Iterator handlers = getHandlersFor(event.getClass()); if (handlers.hasNext()) { @@ -94,14 +110,14 @@ public final class EventBus { try { handlers.next().execute(event); } catch (InvocationTargetException e) { - if (event instanceof DeadEvent || event instanceof ExceptionEvent) - - // Warn about system event not being handled - logger.log(Level.WARNING, event + " not handled due to exception", e); - else if (e.getCause() instanceof Error) + if (e.getCause() instanceof Error) // Transparently pass error to the caller throw (Error) e.getCause(); + else if (event instanceof DeadEvent || event instanceof ExceptionEvent) + + // Warn about system event not being handled + logger.log(Level.WARNING, event + " not handled due to exception", e); else // Dispatch exception event @@ -118,8 +134,8 @@ public final class EventBus { dispatch(new DeadEvent(this, event)); } - // Reset dispatch state - state.isDispatching = false; + // Decrement nesting count (becomes 0 when all dispatches on the thread are finished) + --state.nestingCount; logger.log(Level.DEBUG, "Finished dispatching event {0}", event); } @@ -155,7 +171,7 @@ public final class EventBus { */ public void cancel() { var state = dispatchState.get(); - if (state.isDispatching && !state.isCancelled) + if (state.nestingCount > 0 && !state.isCancelled) state.isCancelled = true; else throw new EventBusException("Calling thread not an active dispatching thread!"); diff --git a/event-bus-core/src/test/java/dev/kske/eventbus/core/NestedTest.java b/event-bus-core/src/test/java/dev/kske/eventbus/core/NestedTest.java new file mode 100644 index 0000000..bb0670e --- /dev/null +++ b/event-bus-core/src/test/java/dev/kske/eventbus/core/NestedTest.java @@ -0,0 +1,73 @@ +package dev.kske.eventbus.core; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.*; + +/** + * Tests nested event dispatches. + * + * @author Kai S. K. Engelbart + * @since 1.2.0 + */ +class NestedTest { + + EventBus bus; + boolean nestedHit; + + /** + * Constructs an event bus and registers this test instance as an event listener. + * + * @since 1.2.0 + */ + @BeforeEach + void registerListener() { + bus = new EventBus(); + bus.registerListener(this); + } + + /** + * Dispatches a simple event, which should in turn cause a string to be dispatched as a nested + * event. If the corresponding handler sets {@link #nestedHit} to {@code true}, the test is + * successful. + * + * @since 1.2.0 + */ + @Test + void testNestedDispatch() { + bus.dispatch(new SimpleEvent()); + assertTrue(nestedHit); + } + + /** + * Dispatches a string as a nested event and cancels the current dispatch afterwards. + * + * @since 1.2.0 + */ + @Event(SimpleEvent.class) + void onSimpleEvent() { + bus.dispatch("Nested event"); + bus.cancel(); + } + + /** + * Sets {@link #nestedHit} to {@code true} indicating that nested dispatches work. + * + * @since 1.2.0 + */ + @Event(String.class) + void onString() { + nestedHit = true; + } + + /** + * Fails the test if an exception is caused during the dispatch. + * + * @param e the event containing the exception + * @since 1.2.0 + */ + @Event + void onException(ExceptionEvent e) { + fail("Exception during dispatch", e.getCause()); + } +}