14 Commits
0.0.4 ... 0.1.0

Author SHA1 Message Date
748cb8b71a Merge pull request 'Add Event Cancellation' (#3) from f/cancel-event into develop
Reviewed-on: https://git.kske.dev/zdm/event-bus/pulls/3
2020-11-26 08:57:46 +01:00
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
9 changed files with 267 additions and 61 deletions

View File

@ -51,6 +51,15 @@ 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.
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.
@ -60,6 +69,23 @@ To include subtypes for an event handler, use the `includeSubtypes` parameter as
@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.
@ -74,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).
@ -91,7 +141,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.4</version>
<version>0.1.0</version>
</dependency>
</dependencies>
```

View File

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

View File

@ -10,11 +10,12 @@ import java.lang.annotation.*;
* comply with the following specifications:
* <ul>
* <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>
* <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>
*
@ -32,6 +33,7 @@ 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;
@ -39,6 +41,7 @@ public @interface Event {
/**
* 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;
@ -49,6 +52,7 @@ public @interface Event {
* <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;

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>, 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
@ -44,7 +66,25 @@ public final class EventBus {
*/
public void dispatch(IEvent 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);
}
/**
@ -71,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.
*
@ -84,6 +138,8 @@ public final class EventBus {
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()) {
@ -97,9 +153,18 @@ public final class EventBus {
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()
);
}
/**
@ -110,11 +175,17 @@ public final class EventBus {
*/
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);
}
@ -125,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

@ -77,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.
*

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,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 {
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 void onSimpleEventSecond() {
++hits;
assertEquals(3, hits);
}
@Event(priority = 50)
private void onSimpleEventThird(SimpleEvent event) {
++hits;
assertEquals(4, hits);
}
}