14 Commits

Author SHA1 Message Date
0e5f31b63e Merge branch 'develop' into f/cancel-event
Conflicts:
	src/test/java/dev/kske/eventbus/EventBusTest.java
2020-11-26 08:16:01 +01:00
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
cd2598d5d3 Add Section About Static Methods in README (#2)
Add paragraph about static methods in README

Co-authored-by: kske <kai@kske.dev>
Reviewed-on: https://git.kske.dev/zdm/event-bus/pulls/2
Reviewed-by: kske <kai@kske.dev>
2020-09-26 09:59:08 +02:00
f6e5c90a44 Add static event handler test 2020-09-25 16:22:27 +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
10 changed files with 313 additions and 53 deletions

View File

@ -51,6 +51,41 @@ public class SimpleEventListener implements EventListener {
In this case, an event bus is created and used locally. 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 a more sophisticated example the class would acquire an external event bus that is used by multiple classes.
Note that creating static event handlers like this
```java
@Event
private static void onSimpleEvent(SimpleEvent event) ...
```
is technically possible, however you would still have to create an instance of the event listener to register it at an event bus.
## 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 ## Parameter-less event handlers
In some cases an event handler is not interested in the dispatched event instance. In some cases an event handler is not interested in the dispatched event instance.
@ -65,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. 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 ## Installation
Event Bus is currently hosted at [kske.dev](https://kske.dev). Event Bus is currently hosted at [kske.dev](https://kske.dev).
@ -82,7 +141,7 @@ To include it inside your project, just add the Maven repository and the depende
<dependency> <dependency>
<groupId>dev.kske</groupId> <groupId>dev.kske</groupId>
<artifactId>event-bus</artifactId> <artifactId>event-bus</artifactId>
<version>0.0.3</version> <version>0.1.0</version>
</dependency> </dependency>
</dependencies> </dependencies>
``` ```

View File

@ -5,7 +5,7 @@
<groupId>dev.kske</groupId> <groupId>dev.kske</groupId>
<artifactId>event-bus</artifactId> <artifactId>event-bus</artifactId>
<version>0.0.3</version> <version>0.1.0</version>
<name>Event Bus</name> <name>Event Bus</name>
<description>An event handling framework for Java utilizing annotations.</description> <description>An event handling framework for Java utilizing annotations.</description>
@ -63,6 +63,7 @@
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId> <artifactId>maven-source-plugin</artifactId>
<version>3.2.1</version>
<executions> <executions>
<execution> <execution>
<id>attach-sources</id> <id>attach-sources</id>
@ -75,6 +76,7 @@
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId> <artifactId>maven-javadoc-plugin</artifactId>
<version>3.2.0</version>
<executions> <executions>
<execution> <execution>
<id>attach-javadocs</id> <id>attach-javadocs</id>

View File

@ -10,11 +10,12 @@ import java.lang.annotation.*;
* comply with the following specifications: * comply with the following specifications:
* <ul> * <ul>
* <li>Declared inside a class that implements {@link EventListener}</li> * <li>Declared inside a class that implements {@link EventListener}</li>
* <li>Specifying an event type by either</li> * <li>Specifying an event type by either
* <ul> * <ul>
* <li>Declaring one parameter of a type that implements {@link IEvent}</li> * <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> * <li>Defining the class of the event using the {@link Event#eventType()} value</li>
* </ul> * </ul>
* </li>
* <li>Return type of {@code void}</li> * <li>Return type of {@code void}</li>
* </ul> * </ul>
* *
@ -32,16 +33,26 @@ public @interface Event {
* <p> * <p>
* The execution order of handlers with the same priority is undefined. * The execution order of handlers with the same priority is undefined.
* *
* @return the priority of the event handler
* @since 0.0.1 * @since 0.0.1
*/ */
int priority() default 100; 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 * Defines the event type the handler listens to. If this value is set, the handler is not
* allowed to declare parameters. * allowed to declare parameters.
* <p> * <p>
* This is useful when the event handler does not utilize the event instance. * 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 * @since 0.0.3
*/ */
Class<? extends IEvent> eventType() default USE_PARAMETER.class; Class<? extends IEvent> eventType() default USE_PARAMETER.class;

View File

@ -1,5 +1,7 @@
package dev.kske.eventbus; package dev.kske.eventbus;
import java.lang.System.Logger;
import java.lang.System.Logger.Level;
import java.util.*; import java.util.*;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
@ -17,7 +19,19 @@ import java.util.concurrent.ConcurrentHashMap;
*/ */
public final class EventBus { 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. * 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 * @since 0.0.2
*/ */
public static EventBus getInstance() { public static EventBus getInstance() {
if (singletonInstance == null) EventBus instance = singletonInstance;
singletonInstance = new EventBus(); if (instance == null)
return singletonInstance; 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<>(); = new ConcurrentHashMap<>();
private final Set<EventListener> registeredListeners = ConcurrentHashMap.newKeySet(); 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 * Dispatches an event to all event handlers registered for it in descending order of their
@ -44,7 +66,25 @@ public final class EventBus {
*/ */
public void dispatch(IEvent event) { public void dispatch(IEvent event) {
Objects.requireNonNull(event); Objects.requireNonNull(event);
getHandlersFor(event.getClass()).forEach(handler -> handler.execute(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);
} }
/** /**
@ -55,8 +95,34 @@ public final class EventBus {
* @since 0.0.1 * @since 0.0.1
*/ */
private List<EventHandler> getHandlersFor(Class<? extends IEvent> eventClass) { 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!");
} }
/** /**
@ -72,6 +138,8 @@ public final class EventBus {
Objects.requireNonNull(listener); Objects.requireNonNull(listener);
if (registeredListeners.contains(listener)) if (registeredListeners.contains(listener))
throw new EventBusException(listener + " already registered!"); throw new EventBusException(listener + " already registered!");
logger.log(Level.INFO, "Registering event listener {0}", listener.getClass().getName());
boolean handlerBound = false;
registeredListeners.add(listener); registeredListeners.add(listener);
for (var method : listener.getClass().getDeclaredMethods()) { for (var method : listener.getClass().getDeclaredMethods()) {
@ -85,9 +153,18 @@ public final class EventBus {
var handler = new EventHandler(listener, method, annotation); var handler = new EventHandler(listener, method, annotation);
if (!bindings.containsKey(handler.getEventType())) if (!bindings.containsKey(handler.getEventType()))
bindings.put(handler.getEventType(), new TreeSet<>()); bindings.put(handler.getEventType(), new TreeSet<>());
logger.log(Level.DEBUG, "Binding event handler {0}", handler);
bindings.get(handler.getEventType()) bindings.get(handler.getEventType())
.add(handler); .add(handler);
handlerBound = true;
} }
if(!handlerBound)
logger.log(
Level.WARNING,
"No event handlers bound for event listener {0}",
listener.getClass().getName()
);
} }
/** /**
@ -98,12 +175,18 @@ public final class EventBus {
*/ */
public void removeListener(EventListener listener) { public void removeListener(EventListener listener) {
Objects.requireNonNull(listener); Objects.requireNonNull(listener);
logger.log(Level.INFO, "Removing event listener {0}", listener.getClass().getName());
for (var binding : bindings.values()) { for (var binding : bindings.values()) {
var it = binding.iterator(); var it = binding.iterator();
while (it.hasNext()) while (it.hasNext()) {
if (it.next().getListener() == listener) var handler = it.next();
if (handler.getListener() == listener) {
logger.log(Level.DEBUG, "Unbinding event handler {0}", handler);
it.remove(); it.remove();
} }
}
}
registeredListeners.remove(listener); registeredListeners.remove(listener);
} }
@ -113,6 +196,7 @@ public final class EventBus {
* @since 0.0.1 * @since 0.0.1
*/ */
public void clearListeners() { public void clearListeners() {
logger.log(Level.INFO, "Clearing event listeners");
bindings.clear(); bindings.clear();
registeredListeners.clear(); registeredListeners.clear();
} }

View File

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

View File

@ -77,6 +77,11 @@ final class EventHandler implements Comparable<EventHandler> {
return priority == 0 ? hashCode() - other.hashCode() : priority; 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. * Executes the event handler.
* *
@ -117,6 +122,12 @@ final class EventHandler implements Comparable<EventHandler> {
*/ */
int getPriority() { return annotation.priority(); } 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 * @return the event type this handler listens to
* @since 0.0.3 * @since 0.0.3

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(eventType = SimpleEvent.class, priority = 150)
private void onSimpleEventFirst() {
++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 {}