13 Commits

Author SHA1 Message Date
ec73be9046 Split EventBusTest into DispatchTest and CancelTest, add Javadoc 2020-11-26 08:14:11 +01:00
659bd7888f Simplify cancellation test, fix a typo 2020-11-25 08:35:51 +01:00
8aefb43823 Add Test for Cancellation 2020-11-23 23:42:17 +01:00
9d1707de5b Add event consumption section to README 2020-10-11 11:31:51 +02:00
1d2102d729 Add event cancellation mechanism to EventBus 2020-10-02 17:50:11 +02:00
dbb816c6cb Add double checked synchronization to EventBus instance initializer 2020-09-24 17:41:08 +02:00
603a838640 Add Missing Javadoc to EventBusException (#1)
Removed annoying Javadoc error
Reviewed-on: https://git.kske.dev/zdm/event-bus/pulls/1
Reviewed-by: kske <kai@kske.dev>
2020-09-23 20:55:16 +02:00
b6b73d335a Add logging to EventBus using the Platform Logging API 2020-09-20 15:28:13 +02:00
8cf51441ad Add priority section to README 2020-09-20 14:35:50 +02:00
001c0eea7e Fix Maven plugin versions for sources and Javadoc, fix <ul> in Javadoc 2020-09-20 14:05:35 +02:00
ba06b49368 Add subtype inclusion for event handlers 2020-09-20 12:20:29 +02:00
7a3debe444 Support parameter-less event handlers
- Add eventType value to Event
- Move semantic event handler checks to EventHandler
- Use Objects#requireNonNull(T) on public API method parameters
- Update README with a parameter-less event handlers section
2020-09-08 20:13:42 +02:00
5f88ad6095 Add a module descriptor for the entire library 2020-09-08 17:06:45 +02:00
11 changed files with 420 additions and 74 deletions

View File

@ -12,6 +12,7 @@ In addition, a singleton instance of the event bus is provided by the `EventBus#
To listen to events, register event handling methods using the `Event` annotation.
For this to work, the method must have a return type of `void` and declare a single parameter of the desired event type.
Alternatively, a parameter-less event handler can be declared as shown [below](#parameter-less-event-handlers).
Additionally, the class containing the method must implement the `EventListener` interface.
## A Simple Example
@ -47,7 +48,72 @@ public class SimpleEventListener implements EventListener {
}
```
In this case, an event bus is created and used locally. In a more sophisticated example the class would acquire an external event bus that is used by multiple classes.
In this case, an event bus is created and used locally.
In a more sophisticated example the class would acquire an external event bus that is used by multiple classes.
## Event handlers for subtypes
On certain occasions its practical for an event handler to accept both events of the specified type, as well as subclasses of that event.
To include subtypes for an event handler, use the `includeSubtypes` parameter as follows:
```java
@Event(includeSubtypes = true)
```
## Event handler execution order
Sometimes when using multiple handlers for one event, it might be useful to know in which order they will be executed.
Event Bus provides a mechanism to ensure the correct propagation of events: the `priority`.
Priority can be set on the `@Event` annotation like that:
```java
@Event(priority=100)
```
The default priority for events is `100`.
**Important:**
Events are dispatched top-down, meaning the event handler with the highest priority will be executed first.
If no priority is set or multiple handlers have the same priority, the order of execution is undefined.
## Parameter-less event handlers
In some cases an event handler is not interested in the dispatched event instance.
To avoid declaring a useless parameter just to specify the event type of the handler, there is an alternative:
```java
@Event(eventType = SimpleEvent.class)
private void onSimpleEvent() {
System.out.println("SimpleEvent received!");
}
```
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
@ -66,7 +132,7 @@ To include it inside your project, just add the Maven repository and the depende
<dependency>
<groupId>dev.kske</groupId>
<artifactId>event-bus</artifactId>
<version>0.0.2</version>
<version>0.1.0</version>
</dependency>
</dependencies>
```

17
pom.xml
View File

@ -5,7 +5,7 @@
<groupId>dev.kske</groupId>
<artifactId>event-bus</artifactId>
<version>0.0.2</version>
<version>0.1.0</version>
<name>Event Bus</name>
<description>An event handling framework for Java utilizing annotations.</description>
@ -45,12 +45,25 @@
</properties>
<build>
<!-- Disable resource folders -->
<resources />
<testResources />
<plugins>
<!-- Support Java 9 modules -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
</plugin>
<!-- Attach sources and Javadoc to JAR -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>3.2.1</version>
<executions>
<execution>
<id>attach-sources</id>
@ -63,6 +76,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>3.2.0</version>
<executions>
<execution>
<id>attach-javadocs</id>
@ -72,6 +86,7 @@
</execution>
</executions>
</plugin>
</plugins>
</build>

View File

@ -10,7 +10,12 @@ import java.lang.annotation.*;
* comply with the following specifications:
* <ul>
* <li>Declared inside a class that implements {@link EventListener}</li>
* <li>One parameter of a type that implements {@link IEvent}</li>
* <li>Specifying an event type by either
* <ul>
* <li>Declaring one parameter of a type that implements {@link IEvent}</li>
* <li>Defining the class of the event using the {@link Event#eventType()} value</li>
* </ul>
* </li>
* <li>Return type of {@code void}</li>
* </ul>
*
@ -28,7 +33,35 @@ public @interface Event {
* <p>
* The execution order of handlers with the same priority is undefined.
*
* @return the priority of the event handler
* @since 0.0.1
*/
int priority() default 100;
/**
* Defines whether instances of subtypes of the event type are dispatched to the event handler.
*
* @return whether the event handler includes subtypes
* @since 0.0.4
*/
boolean includeSubtypes() default false;
/**
* Defines the event type the handler listens to. If this value is set, the handler is not
* allowed to declare parameters.
* <p>
* This is useful when the event handler does not utilize the event instance.
*
* @return the event type accepted by the handler
* @since 0.0.3
*/
Class<? extends IEvent> eventType() default USE_PARAMETER.class;
/**
* Signifies that the event type the handler listens to is determined by the type of its only
* parameter.
*
* @since 0.0.3
*/
static final class USE_PARAMETER implements IEvent {}
}

View File

@ -1,5 +1,7 @@
package dev.kske.eventbus;
import java.lang.System.Logger;
import java.lang.System.Logger.Level;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
@ -17,7 +19,19 @@ import java.util.concurrent.ConcurrentHashMap;
*/
public final class EventBus {
private static EventBus singletonInstance;
/**
* 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());
/**
* Produces a singleton instance of the event bus. It is lazily initialized on the first call.
@ -26,14 +40,22 @@ public final class EventBus {
* @since 0.0.2
*/
public static EventBus getInstance() {
if (singletonInstance == null)
singletonInstance = new EventBus();
return singletonInstance;
EventBus instance = singletonInstance;
if (instance == null)
synchronized (EventBus.class) {
if ((instance = singletonInstance) == null) {
logger.log(Level.DEBUG, "Initializing singleton event bus instance");
instance = singletonInstance = new EventBus();
}
}
return instance;
}
private final Map<Class<? extends IEvent>, Collection<EventHandler>> bindings
private final Map<Class<? extends IEvent>, TreeSet<EventHandler>> bindings
= new ConcurrentHashMap<>();
private final Set<EventListener> registeredListeners = ConcurrentHashMap.newKeySet();
private final ThreadLocal<DispatchState> dispatchState
= ThreadLocal.withInitial(DispatchState::new);
/**
* Dispatches an event to all event handlers registered for it in descending order of their
@ -43,7 +65,26 @@ public final class EventBus {
* @since 0.0.1
*/
public void dispatch(IEvent event) {
getHandlersFor(event.getClass()).forEach(handler -> handler.execute(event));
Objects.requireNonNull(event);
logger.log(Level.INFO, "Dispatching event {0}", 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);
}
/**
@ -54,8 +95,34 @@ public final class EventBus {
* @since 0.0.1
*/
private List<EventHandler> getHandlersFor(Class<? extends IEvent> eventClass) {
return bindings.containsKey(eventClass) ? new ArrayList<>(bindings.get(eventClass))
: new ArrayList<>();
// Get handlers defined for the event class
Set<EventHandler> handlers
= bindings.containsKey(eventClass) ? bindings.get(eventClass)
: new TreeSet<>();
// Get subtype handlers
for (var binding : bindings.entrySet())
if (binding.getKey().isAssignableFrom(eventClass))
for (var handler : binding.getValue())
if (handler.includeSubtypes())
handlers.add(handler);
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!");
}
/**
@ -63,13 +130,16 @@ public final class EventBus {
*
* @param listener the listener to register
* @throws EventBusException if the listener is already registered or a declared event handler
* does not comply to the specification
* does not comply with the specification
* @since 0.0.1
* @see Event
*/
public void registerListener(EventListener listener) throws EventBusException {
Objects.requireNonNull(listener);
if (registeredListeners.contains(listener))
throw new EventBusException(listener + " already registered!");
logger.log(Level.INFO, "Registering event listener {0}", listener.getClass().getName());
boolean handlerBound = false;
registeredListeners.add(listener);
for (var method : listener.getClass().getDeclaredMethods()) {
@ -79,24 +149,22 @@ public final class EventBus {
if (annotation == null)
continue;
// Check for correct method signature and return type
if (method.getParameterCount() != 1)
throw new EventBusException(method + " does not have an argument count of 1!");
if (!method.getReturnType().equals(void.class))
throw new EventBusException(method + " does not have a return type of void!");
var param = method.getParameterTypes()[0];
if (!IEvent.class.isAssignableFrom(param))
throw new EventBusException(param + " is not of type IEvent!");
@SuppressWarnings("unchecked")
var realParam = (Class<? extends IEvent>) param;
if (!bindings.containsKey(realParam))
bindings.put(realParam, new TreeSet<>());
bindings.get(realParam).add(new EventHandler(listener, method, annotation));
// Initialize and bind the handler
var handler = new EventHandler(listener, method, annotation);
if (!bindings.containsKey(handler.getEventType()))
bindings.put(handler.getEventType(), new TreeSet<>());
logger.log(Level.DEBUG, "Binding event handler {0}", handler);
bindings.get(handler.getEventType())
.add(handler);
handlerBound = true;
}
if(!handlerBound)
logger.log(
Level.WARNING,
"No event handlers bound for event listener {0}",
listener.getClass().getName()
);
}
/**
@ -106,11 +174,18 @@ public final class EventBus {
* @since 0.0.1
*/
public void removeListener(EventListener listener) {
Objects.requireNonNull(listener);
logger.log(Level.INFO, "Removing event listener {0}", listener.getClass().getName());
for (var binding : bindings.values()) {
var it = binding.iterator();
while (it.hasNext())
if (it.next().getListener() == listener)
while (it.hasNext()) {
var handler = it.next();
if (handler.getListener() == listener) {
logger.log(Level.DEBUG, "Unbinding event handler {0}", handler);
it.remove();
}
}
}
registeredListeners.remove(listener);
}
@ -121,6 +196,7 @@ public final class EventBus {
* @since 0.0.1
*/
public void clearListeners() {
logger.log(Level.INFO, "Clearing event listeners");
bindings.clear();
registeredListeners.clear();
}

View File

@ -1,8 +1,9 @@
package dev.kske.eventbus;
/**
* This runtime exception is thrown when an event bus error occurs. This can either occur while
* registering event listeners with invalid handlers, or when an event handler throws an exception.
* This runtime exception is thrown when an event bus error occurs. This can
* either occur while registering event listeners with invalid handlers, or when
* an event handler throws an exception.
*
* @author Kai S. K. Engelbart
* @since 0.0.1
@ -11,10 +12,21 @@ public class EventBusException extends RuntimeException {
private static final long serialVersionUID = 1L;
/**
* Creates a new event bus exception.
*
* @param message the message to display
* @param cause the cause of this exception
*/
public EventBusException(String message, Throwable cause) {
super(message, cause);
}
/**
* Creates a new event bus exception.
*
* @param message the message to display
*/
public EventBusException(String message) {
super(message);
}

View File

@ -2,6 +2,8 @@ package dev.kske.eventbus;
import java.lang.reflect.*;
import dev.kske.eventbus.Event.USE_PARAMETER;
/**
* Internal representation of an event handling method.
*
@ -14,6 +16,7 @@ final class EventHandler implements Comparable<EventHandler> {
private final EventListener listener;
private final Method method;
private final Event annotation;
private final Class<? extends IEvent> eventType;
/**
* Constructs an event handler.
@ -21,12 +24,40 @@ final class EventHandler implements Comparable<EventHandler> {
* @param listener the listener containing the handler
* @param method the handler method
* @param annotation the event annotation
* @throws EventBusException if the method or the annotation do not comply with the
* specification
* @since 0.0.1
*/
EventHandler(EventListener listener, Method method, Event annotation) {
@SuppressWarnings("unchecked")
EventHandler(EventListener listener, Method method, Event annotation) throws EventBusException {
this.listener = listener;
this.method = method;
this.annotation = annotation;
// Check for correct method signature and return type
if (method.getParameterCount() == 0 && annotation.eventType().equals(USE_PARAMETER.class))
throw new EventBusException(method + " does not define an event type!");
if (method.getParameterCount() == 1 && !annotation.eventType().equals(USE_PARAMETER.class))
throw new EventBusException(method + " defines an ambiguous event type!");
if (method.getParameterCount() > 1)
throw new EventBusException(method + " defines more than one parameter!");
if (!method.getReturnType().equals(void.class))
throw new EventBusException(method + " does not have a return type of void!");
// Determine the event type
Class<? extends IEvent> eventType = annotation.eventType();
if (eventType.equals(USE_PARAMETER.class)) {
var param = method.getParameterTypes()[0];
if (!IEvent.class.isAssignableFrom(param))
throw new EventBusException(param + " is not of type IEvent!");
eventType = (Class<? extends IEvent>) param;
}
this.eventType = eventType;
// Allow access if the method is non-public
method.setAccessible(true);
}
@ -46,6 +77,11 @@ final class EventHandler implements Comparable<EventHandler> {
return priority == 0 ? hashCode() - other.hashCode() : priority;
}
@Override
public String toString() {
return String.format("EventHandler[method=%s, annotation=%s]", method, annotation);
}
/**
* Executes the event handler.
*
@ -55,7 +91,10 @@ final class EventHandler implements Comparable<EventHandler> {
*/
void execute(IEvent event) throws EventBusException {
try {
method.invoke(listener, event);
if (annotation.eventType().equals(USE_PARAMETER.class))
method.invoke(listener, event);
else
method.invoke(listener);
} catch (
IllegalAccessException
| IllegalArgumentException
@ -82,4 +121,16 @@ final class EventHandler implements Comparable<EventHandler> {
* @since 0.0.1
*/
int getPriority() { return annotation.priority(); }
/**
* @return whether this handler includes subtypes
* @since 0.0.4
*/
boolean includeSubtypes() { return annotation.includeSubtypes(); }
/**
* @return the event type this handler listens to
* @since 0.0.3
*/
Class<? extends IEvent> getEventType() { return eventType; }
}

View File

@ -0,0 +1,12 @@
/**
* Contains the public API and implementation of the event bus library.
*
* @author Kai S. K. Engelbart
* @since 0.0.3
* @see dev.kske.eventbus.Event
* @see dev.kske.eventbus.EventBus
*/
module dev.kske.eventbus {
exports dev.kske.eventbus;
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -1,38 +0,0 @@
package dev.kske.eventbus;
import static org.junit.jupiter.api.Assertions.assertEquals;
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;
@BeforeEach
public void registerListener() {
EventBus.getInstance().registerListener(this);
}
@Test
void testDispatch() {
EventBus.getInstance().dispatch(new SimpleEvent());
}
@Event(priority = 50)
private void onSimpleEventSecond(SimpleEvent event) {
++hits;
assertEquals(2, hits);
}
@Event(priority = 150)
private void onSimpleEventFirst(SimpleEvent event) {
++hits;
assertEquals(1, hits);
}
}

View File

@ -0,0 +1,9 @@
package dev.kske.eventbus;
/**
* Subclass of {@link SimpleEvent} for testing purposes.
*
* @author Kai S. K. Engelbart
* @since 0.0.4
*/
public class SimpleEventSub extends SimpleEvent {}