17 Commits

Author SHA1 Message Date
51f10c4144 Merge pull request 'Listener-Level Properties' (#13) from f/listener-level-properties into develop
Reviewed-on: https://git.kske.dev/kske/event-bus/pulls/13
Reviewed-by: delvh <leon@kske.dev>
2021-03-17 07:56:22 +01:00
f74b953db8 Replace hardcoded priority value by constant 2021-03-16 10:17:24 +01:00
52719d22d4 Merge pull request 'Transparently Propagate Event Handler Errors' (#14) from b/error-passthrough into develop
Reviewed-on: https://git.kske.dev/kske/event-bus/pulls/14
Reviewed-by: delvh <leon@kske.dev>
Reviewed-by: DieGurke <maxi@kske.dev>
2021-03-16 08:17:41 +01:00
122106bf39 Transparently propagate event handler errors
When an exception occurs during the execution of an event handler, it is
caught, wrapped inside an exception event and dispatched on the event
bus.

This applies to any throwable, but is not very useful for errors, as
these are not normally caught. Assertion errors in particular, which are
used in unit tests, should not be caught, as this would cause the test
runner to miss a failed test.

Therefore, errors are now transparently passed through to the caller of
the dispatch method.
2021-03-15 08:29:15 +01:00
7357198d45 Warn about useless handler property redefinitions
When polymorphism or a priority is defined at listener-level and then
identically defined for a specific handler, a warning is issued.

This does not affect redefinitions of default values.
2021-03-14 14:38:43 +01:00
32dfe64c0f Add listener-level properties section to README 2021-03-14 11:44:56 +01:00
2ec0a82a96 Respect listener-level properties 2021-03-14 11:18:11 +01:00
6c74af608c Allow @Polymorphic and @Priority on types, add value to @Polymorphic
This is the first step for listener level handler properties.

To allow a handler inside a polymorphic listener to be non-polymorphic,
the @Polymorphic annotation now has a boolean value that defaults to
true. In that case, it can be explicitly set to false to override the
listener-level default.
2021-02-22 19:12:06 +01:00
d9ddc0e1a9 Merge pull request 'Add ExceptionEvent' (#12) from f/exception-event into develop
Reviewed-on: https://git.kske.dev/kske/event-bus/pulls/12
Reviewed-by: delvh <leon@kske.dev>
Reviewed-by: DieGurke <maxi@kske.dev>
2021-02-21 14:04:26 +01:00
7c3cd017de Add system events section to README 2021-02-21 13:50:12 +01:00
6a2cad4ae5 Add ExceptionEvent
An exception event wraps an event that caused an exception inside of an
event handler while being dispatched and is then dispatched to dedicated
handlers.
2021-02-21 10:36:06 +01:00
0f9b64be48 Merge pull request 'Add DeadEvent' (#9) from f/dead-event into develop
Reviewed-on: https://git.kske.dev/kske/event-bus/pulls/9
Reviewed-by: delvh <leon@kske.dev>
Reviewed-by: DieGurke <maxi@kske.dev>
2021-02-21 09:16:32 +01:00
b2fe3a9d6c Log unhandled dead events 2021-02-20 22:10:48 +01:00
9379e6bb94 Merge pull request 'Additional Warnings in Event Bus Proc' (#8) from f/additional-warnings into develop
Reviewed-on: https://git.kske.dev/kske/event-bus/pulls/8
Reviewed-by: delvh <leon@kske.dev>
2021-02-20 21:46:08 +01:00
0036dc4829 Add DeadEvent
A dead events wraps an event that was dispatched but not delivered to
any handler. The dead event is than dispatched to dedicated handlers.
2021-02-19 16:05:11 +01:00
8a30493c52 Warn about unused event handler return values
If an event handler has a non-void return type, a warning is issued as
the event bus cannot use the returned value.

In rare cases this might be justified as the event handler could be
invoked directly.
2021-02-19 11:34:58 +01:00
b56f08e441 Warn about unnecessarily polymorphic event handlers
When Event Bus Proc detects a handler for a final type that uses the
@Polymorphic annotation, it issues a warning.
2021-02-19 11:30:09 +01:00
11 changed files with 394 additions and 49 deletions

View File

@ -97,6 +97,13 @@ private void onSimpleEvent() {
Make sure that you **do not** both declare a parameter and specify the event type in the annotation, as this would be ambiguous.
## Listener-Level Properties
When defining a dedicated event listener that, for example, performs pre- or post-processing, all event handlers will probably have the same non-standard priority.
Instead of defining that priority for each handler, it can be defined at the listener level by annotating the listener itself.
The same applies to polymorphism.
## Event Consumption
In some cases it might be useful to stop the propagation of an event.
@ -123,6 +130,38 @@ This applies to all event handlers that would have been executed after the one c
Avoid cancelling events while using multiple event handlers with the same priority.
As event handlers are ordered by priority, it is not defined which of them will be executed after the event has been consumed.
## System Events
To accommodate for special circumstances in an event distribution, system events have been introduced.
At the moment, there are two system events, which are explained in this section.
### Detecting Unhandled Events
When an event is dispatched but not delivered to any handler, a dead event is dispatched that wraps the original event.
You can declare a dead event handler to respond to this situation:
```java
private void onDeadEvent(DeadEvent deadEvent) { ... }
```
### Detecting Exceptions Thrown by Event Handlers
When an event handler throws an exception, an exception event is dispatched that wraps the original event.
A exception handler is declared as follows:
```java
private void onExceptionEvent(ExceptionEvent ExceptionEvent) { ... }
```
Both system events reference the event bus that caused them and a warning is logged if they are unhandled.
### What About Endless Recursion Caused By Dead Events and Exception Events?
As one might imagine, an unhandled dead event would theoretically lead to an endless recursion.
The same applies when an exception event handler throws an exception.
To avoid this, system events never cause system events and instead just issue a warning to the logger.
## Installation
Event Bus is available in Maven Central.

View File

@ -0,0 +1,37 @@
package dev.kske.eventbus.core;
/**
* Wraps an event that was dispatched but for which no handler has been bound.
* <p>
* Handling dead events is useful as it can identify a poorly configured event distribution.
*
* @author Kai S. K. Engelbart
* @since 1.1.0
*/
public final class DeadEvent {
private final EventBus eventBus;
private final Object event;
DeadEvent(EventBus eventBus, Object event) {
this.eventBus = eventBus;
this.event = event;
}
@Override
public String toString() {
return String.format("DeadEvent[eventBus=%s, event=%s]", eventBus, event);
}
/**
* @return the event bus that dispatched this event
* @since 1.1.0
*/
public EventBus getEventBus() { return eventBus; }
/**
* @return the event that could not be delivered
* @since 1.1.0
*/
public Object getEvent() { return event; }
}

View File

@ -2,6 +2,7 @@ package dev.kske.eventbus.core;
import java.lang.System.Logger;
import java.lang.System.Logger.Level;
import java.lang.reflect.InvocationTargetException;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
@ -29,6 +30,14 @@ public final class EventBus {
boolean isDispatching, isCancelled;
}
/**
* The priority assigned to every event handler without an explicitly defined priority.
*
* @since 1.1.0
* @see Priority
*/
public static final int DEFAULT_PRIORITY = 100;
private static volatile EventBus singletonInstance;
private static final Logger logger = System.getLogger(EventBus.class.getName());
@ -63,9 +72,10 @@ public final class EventBus {
* priority.
*
* @param event the event to dispatch
* @throws EventBusException if an event handler isn't accessible or has an invalid signature
* @since 0.0.1
*/
public void dispatch(Object event) {
public void dispatch(Object event) throws EventBusException {
Objects.requireNonNull(event);
logger.log(Level.INFO, "Dispatching event {0}", event);
@ -73,14 +83,40 @@ public final class EventBus {
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);
}
Iterator<EventHandler> handlers = getHandlersFor(event.getClass());
if (handlers.hasNext()) {
while (handlers.hasNext())
if (state.isCancelled) {
logger.log(Level.INFO, "Cancelled dispatching event {0}", event);
state.isCancelled = false;
break;
} else {
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)
// Transparently pass error to the caller
throw (Error) e.getCause();
else
// Dispatch exception event
dispatch(new ExceptionEvent(this, event, e.getCause()));
}
}
} else if (event instanceof DeadEvent || event instanceof ExceptionEvent) {
// Warn about the dead event not being handled
logger.log(Level.WARNING, "{0} not handled", event);
} else {
// Dispatch dead event
dispatch(new DeadEvent(this, event));
}
// Reset dispatch state
state.isDispatching = false;
@ -89,25 +125,26 @@ public final class EventBus {
}
/**
* Searches for the event handlers bound to an event class.
* Searches for the event handlers bound to an event class. This includes polymorphic handlers
* that are bound to a supertype of the event class.
*
* @param eventClass the event class to use for the search
* @return all event handlers registered for the event class
* @return an iterator over the applicable handlers in descending order of priority
* @since 0.0.1
*/
private List<EventHandler> getHandlersFor(Class<?> eventClass) {
private Iterator<EventHandler> getHandlersFor(Class<?> eventClass) {
// Get handlers defined for the event class
Set<EventHandler> handlers = bindings.getOrDefault(eventClass, new TreeSet<>());
TreeSet<EventHandler> handlers = bindings.getOrDefault(eventClass, new TreeSet<>());
// Get subtype handlers
// Get polymorphic handlers
for (var binding : bindings.entrySet())
if (binding.getKey().isAssignableFrom(eventClass))
for (var handler : binding.getValue())
if (handler.isPolymorphic())
handlers.add(handler);
return new ArrayList<>(handlers);
return handlers.iterator();
}
/**
@ -140,6 +177,16 @@ public final class EventBus {
logger.log(Level.INFO, "Registering event listener {0}", listener.getClass().getName());
boolean handlerBound = false;
// Predefined handler polymorphism
boolean polymorphic = false;
if (listener.getClass().isAnnotationPresent(Polymorphic.class))
polymorphic = listener.getClass().getAnnotation(Polymorphic.class).value();
// Predefined handler priority
int priority = DEFAULT_PRIORITY;
if (listener.getClass().isAnnotationPresent(Priority.class))
priority = listener.getClass().getAnnotation(Priority.class).value();
registeredListeners.add(listener);
for (var method : listener.getClass().getDeclaredMethods()) {
Event annotation = method.getAnnotation(Event.class);
@ -149,7 +196,7 @@ public final class EventBus {
continue;
// Initialize and bind the handler
var handler = new EventHandler(listener, method, annotation);
var handler = new EventHandler(listener, method, annotation, polymorphic, priority);
bindings.putIfAbsent(handler.getEventType(), new TreeSet<>());
logger.log(Level.DEBUG, "Binding event handler {0}", handler);
bindings.get(handler.getEventType())

View File

@ -13,14 +13,6 @@ import dev.kske.eventbus.core.Event.USE_PARAMETER;
*/
final class EventHandler implements Comparable<EventHandler> {
/**
* The priority assigned to every event handler without an explicitly defined priority.
*
* @since 1.0.0
* @see Priority
*/
public static final int DEFAULT_PRIORITY = 100;
private final Object listener;
private final Method method;
private final Class<?> eventType;
@ -31,14 +23,17 @@ final class EventHandler implements Comparable<EventHandler> {
/**
* Constructs an event handler.
*
* @param listener the listener containing the handler
* @param method the handler method
* @param annotation the event annotation
* @param listener the listener containing the handler
* @param method the handler method
* @param annotation the event annotation
* @param defPolymorphism the predefined polymorphism (default or listener-level)
* @param defPriority the predefined priority (default or listener-level)
* @throws EventBusException if the method or the annotation do not comply with the
* specification
* @since 0.0.1
*/
EventHandler(Object listener, Method method, Event annotation) throws EventBusException {
EventHandler(Object listener, Method method, Event annotation, boolean defPolymorphism,
int defPriority) throws EventBusException {
this.listener = listener;
this.method = method;
useParameter = annotation.value() == USE_PARAMETER.class;
@ -55,10 +50,12 @@ final class EventHandler implements Comparable<EventHandler> {
// Determine handler properties
eventType = useParameter ? method.getParameterTypes()[0] : annotation.value();
polymorphic = method.isAnnotationPresent(Polymorphic.class);
polymorphic = method.isAnnotationPresent(Polymorphic.class)
? method.getAnnotation(Polymorphic.class).value()
: defPolymorphism;
priority = method.isAnnotationPresent(Priority.class)
? method.getAnnotation(Priority.class).value()
: DEFAULT_PRIORITY;
: defPriority;
// Allow access if the method is non-public
method.setAccessible(true);
@ -68,7 +65,7 @@ final class EventHandler implements Comparable<EventHandler> {
* Compares this to another event handler based on priority. In case of equal priority a
* non-zero value based on hash codes is returned.
* <p>
* This is used to retrieve event handlers in order of descending priority from a tree set.
* This is used to retrieve event handlers in descending order of priority from a tree set.
*
* @since 0.0.1
*/
@ -91,17 +88,22 @@ final class EventHandler implements Comparable<EventHandler> {
* Executes the event handler.
*
* @param event the event used as the method parameter
* @throws EventBusException if the handler throws an exception
* @throws EventBusException if the event handler isn't accessible or has an invalid
* signature
* @throws InvocationTargetException if the handler throws an exception
* @throws EventBusException if the handler has the wrong signature or is inaccessible
* @since 0.0.1
*/
void execute(Object event) throws EventBusException {
void execute(Object event) throws EventBusException, InvocationTargetException {
try {
if (useParameter)
method.invoke(listener, event);
else
method.invoke(listener);
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
throw new EventBusException("Failed to invoke event handler!", e);
} catch (IllegalArgumentException e) {
throw new EventBusException("Event handler rejected target / argument!", e);
} catch (IllegalAccessException e) {
throw new EventBusException("Event handler is not accessible!", e);
}
}

View File

@ -0,0 +1,47 @@
package dev.kske.eventbus.core;
/**
* Wraps an event that was dispatched but caused an exception in one of its handlers.
* <p>
* Handling exception events is useful as it allows the creation of a centralized exception handling
* mechanism for unexpected exceptions.
*
* @author Kai S. K. Engelbart
* @since 1.1.0
*/
public final class ExceptionEvent {
private final EventBus eventBus;
private final Object event;
private final Throwable cause;
ExceptionEvent(EventBus eventBus, Object event, Throwable cause) {
this.eventBus = eventBus;
this.event = event;
this.cause = cause;
}
@Override
public String toString() {
return String.format("ExceptionEvent[eventBus=%s, event=%s, cause=%s]", eventBus, event,
cause);
}
/**
* @return the event bus that dispatched this event
* @since 1.1.0
*/
public EventBus getEventBus() { return eventBus; }
/**
* @return the event that could not be handled because of an exception
* @since 1.1.0
*/
public Object getEvent() { return event; }
/**
* @return the exception that was thrown while handling the event
* @since 1.1.0
*/
public Throwable getCause() { return cause; }
}

View File

@ -1,6 +1,6 @@
package dev.kske.eventbus.core;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.*;
@ -8,6 +8,9 @@ import java.lang.annotation.*;
/**
* Allows an event handler to receive events that are subtypes of the declared event type.
* <p>
* When used on a type, the value applies to all event handlers declared within that type that don't
* define a value on their own.
* <p>
* This is useful when defining an event handler for an interface or an abstract class.
*
* @author Kai S. K. Engelbart
@ -16,5 +19,12 @@ import java.lang.annotation.*;
*/
@Documented
@Retention(RUNTIME)
@Target(METHOD)
public @interface Polymorphic {}
@Target({ METHOD, TYPE })
public @interface Polymorphic {
/**
* @return whether the event handler is polymorphic
* @since 1.1.0
*/
boolean value() default true;
}

View File

@ -1,6 +1,6 @@
package dev.kske.eventbus.core;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.*;
@ -9,6 +9,9 @@ import java.lang.annotation.*;
* Defines the priority of an event handler. Handlers are executed in descending order of their
* priority.
* <p>
* When used on a type, the value applies to all event handlers declared within that type that don't
* define a value on their own.
* <p>
* Handlers without this annotation have the default priority of 100.
* <p>
* The execution order of handlers with the same priority is undefined.
@ -19,7 +22,7 @@ import java.lang.annotation.*;
*/
@Documented
@Retention(RUNTIME)
@Target(METHOD)
@Target({ METHOD, TYPE })
public @interface Priority {
/**

View File

@ -0,0 +1,49 @@
package dev.kske.eventbus.core;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;
/**
* Tests the dispatching of a dead event if an event could not be delivered.
*
* @author Kai S. K. Engelbart
* @since 1.1.0
*/
class DeadTest {
EventBus bus = new EventBus();
String event = "This event has no handler";
boolean deadEventHandled;
/**
* Tests dead event delivery.
*
* @since 1.1.0
*/
@Test
void testDeadEvent() {
bus.registerListener(this);
bus.dispatch(event);
assertTrue(deadEventHandled);
bus.removeListener(this);
}
/**
* Tests how the event bus reacts to an unhandled dead event. This should not lead to an
* exception or an endless recursion and should be logged instead.
*
* @since 1.1.0
*/
@Test
void testUnhandledDeadEvent() {
bus.dispatch(event);
}
@Event
void onDeadEvent(DeadEvent deadEvent) {
assertEquals(bus, deadEvent.getEventBus());
assertEquals(event, deadEvent.getEvent());
deadEventHandled = true;
}
}

View File

@ -10,6 +10,8 @@ import org.junit.jupiter.api.*;
* @author Kai S. K. Engelbart
* @since 0.0.1
*/
@Polymorphic
@Priority(150)
class DispatchTest {
EventBus bus;
@ -27,8 +29,8 @@ class DispatchTest {
}
/**
* Tests {@link EventBus#dispatch(Object)} with multiple handler priorities, a subtype handler
* and a static handler.
* Tests {@link EventBus#dispatch(Object)} with multiple handler priorities, a polymorphic
* handler and a static handler.
*
* @since 0.0.1
*/
@ -40,20 +42,21 @@ class DispatchTest {
@Event(SimpleEvent.class)
@Priority(200)
@Polymorphic
void onSimpleEventFirst() {
++hits;
assertTrue(hits == 1 || hits == 2);
}
@Event(SimpleEvent.class)
@Priority(150)
@Polymorphic(false)
static void onSimpleEventSecond() {
++hits;
assertEquals(3, hits);
}
@Event
@Polymorphic(false)
@Priority(100)
void onSimpleEventThird(SimpleEvent event) {
++hits;
assertEquals(4, hits);

View File

@ -0,0 +1,62 @@
package dev.kske.eventbus.core;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;
/**
* Tests the dispatching of an exception event if an event handler threw an exception.
*
* @author Kai S. K. Engelbart
* @since 1.1.0
*/
class ExceptionTest {
EventBus bus = new EventBus();
String event = "This event will cause an exception";
RuntimeException exception = new RuntimeException("I failed");
boolean exceptionEventHandled;
/**
* Tests exception event delivery.
*
* @since 1.1.0
*/
@Test
void testExceptionEvent() {
bus.registerListener(this);
bus.registerListener(new ExceptionListener());
bus.dispatch(event);
assertTrue(exceptionEventHandled);
bus.clearListeners();
}
/**
* Tests how the event bus reacts to an unhandled exception event. This should not lead to an
* exception or an endless recursion and should be logged instead.
*
* @since 1.1.0
*/
@Test
void testUnhandledExceptionEvent() {
bus.registerListener(this);
bus.dispatch(event);
bus.removeListener(this);
}
@Event(String.class)
void onString() {
throw exception;
}
class ExceptionListener {
@Event
void onExceptionEvent(ExceptionEvent exceptionEvent) {
assertEquals(bus, exceptionEvent.getEventBus());
assertEquals(event, exceptionEvent.getEvent());
assertEquals(exception, exceptionEvent.getCause());
exceptionEventHandled = true;
}
}
}

View File

@ -63,6 +63,10 @@ public class EventProcessor extends AbstractProcessor {
else
pass = true;
// Warn the user about unused return values
if (useParameter && eventHandler.getReturnType().getKind() != TypeKind.VOID)
warning(eventHandler, "Unused return value");
// Abort checking if the handler signature is incorrect
if (!pass)
continue;
@ -80,14 +84,56 @@ public class EventProcessor extends AbstractProcessor {
}
}
// Check for handlers for abstract types that aren't polymorphic
// Get the listener containing this handler
TypeElement listener = (TypeElement) eventHandler.getEnclosingElement();
// Default properties
boolean defPolymorphic = false;
int defPriority = 100;
// Listener-level polymorphism
Polymorphic listenerPolymorphic = listener.getAnnotation(Polymorphic.class);
boolean hasListenerPolymorphic = listenerPolymorphic != null;
// Listener-level priority
Priority listenerPriority = listener.getAnnotation(Priority.class);
boolean hasListenerPriority = listenerPriority != null;
// Effective polymorphism
boolean polymorphic =
hasListenerPolymorphic ? listenerPolymorphic.value() : defPolymorphic;
boolean hasHandlerPolymorphic = eventHandler.getAnnotation(Polymorphic.class) != null;
if (hasHandlerPolymorphic)
polymorphic = eventHandler.getAnnotation(Polymorphic.class).value();
// Effective priority
int priority = hasListenerPriority ? listenerPriority.value() : defPriority;
boolean hasHandlerPriority = eventHandler.getAnnotation(Priority.class) != null;
if (hasHandlerPriority)
priority = eventHandler.getAnnotation(Priority.class).value();
// Detect useless polymorphism redefinition
if (hasListenerPolymorphic && hasHandlerPolymorphic
&& listenerPolymorphic.value() == polymorphic)
warning(eventHandler, "@Polymorphism is already defined at listener level");
// Detect useless priority redefinition
if (hasListenerPriority && hasHandlerPriority && listenerPriority.value() == priority)
warning(eventHandler, "@Priority is already defined at the listener level");
// Detect missing or useless @Polymorphic
Element eventElement = ((DeclaredType) eventType).asElement();
if (eventHandler.getAnnotation(Polymorphic.class) == null
&& (eventElement.getKind() == ElementKind.INTERFACE
|| eventElement.getModifiers().contains(Modifier.ABSTRACT))) {
// Check for handlers for abstract types that aren't polymorphic
if (!polymorphic && (eventElement.getKind() == ElementKind.INTERFACE
|| eventElement.getModifiers().contains(Modifier.ABSTRACT)))
warning(eventHandler,
"Parameter should be instantiable or handler should use @Polymorphic");
}
// Check for handlers for final types that are polymorphic
else if (polymorphic && eventElement.getModifiers().contains(Modifier.FINAL))
warning(eventHandler,
"@Polymorphic should be removed as parameter cannot be subclassed");
}
}