3 Commits
0.0.2 ... 0.0.4

Author SHA1 Message Date
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
8 changed files with 183 additions and 34 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,31 @@ 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)
```
## 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.
## Installation
@ -66,7 +91,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.0.4</version>
</dependency>
</dependencies>
```

15
pom.xml
View File

@ -5,7 +5,7 @@
<groupId>dev.kske</groupId>
<artifactId>event-bus</artifactId>
<version>0.0.2</version>
<version>0.0.4</version>
<name>Event Bus</name>
<description>An event handling framework for Java utilizing annotations.</description>
@ -45,9 +45,21 @@
</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>
@ -72,6 +84,7 @@
</execution>
</executions>
</plugin>
</plugins>
</build>

View File

@ -10,7 +10,11 @@ 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</li>
* <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>Return type of {@code void}</li>
* </ul>
*
@ -31,4 +35,29 @@ public @interface Event {
* @since 0.0.1
*/
int priority() default 100;
/**
* Defines whether instances of subtypes of the event type are dispatched to the event handler.
*
* @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.
*
* @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

@ -31,7 +31,7 @@ public final class EventBus {
return singletonInstance;
}
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();
@ -43,6 +43,7 @@ public final class EventBus {
* @since 0.0.1
*/
public void dispatch(IEvent event) {
Objects.requireNonNull(event);
getHandlersFor(event.getClass()).forEach(handler -> handler.execute(event));
}
@ -54,8 +55,20 @@ 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);
}
/**
@ -63,11 +76,12 @@ 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!");
@ -79,23 +93,12 @@ 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<>());
bindings.get(handler.getEventType())
.add(handler);
}
}
@ -106,6 +109,7 @@ public final class EventBus {
* @since 0.0.1
*/
public void removeListener(EventListener listener) {
Objects.requireNonNull(listener);
for (var binding : bindings.values()) {
var it = binding.iterator();
while (it.hasNext())

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);
}
@ -55,7 +86,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 +116,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

@ -1,6 +1,6 @@
package dev.kske.eventbus;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.*;
@ -21,18 +21,29 @@ class EventBusTest implements EventListener {
@Test
void testDispatch() {
EventBus.getInstance().dispatch(new SimpleEventSub());
EventBus.getInstance().dispatch(new SimpleEvent());
}
@Event(priority = 50)
private void onSimpleEventSecond(SimpleEvent event) {
@Event(
eventType = SimpleEvent.class,
includeSubtypes = true,
priority = 200
)
private void onSimpleEventFirst() {
++hits;
assertEquals(2, hits);
assertTrue(hits == 1 || hits == 2);
}
@Event(priority = 150)
private void onSimpleEventFirst(SimpleEvent event) {
@Event(eventType = SimpleEvent.class, priority = 150)
private void onSimpleEventSecond() {
++hits;
assertEquals(1, hits);
assertEquals(3, hits);
}
@Event(priority = 50)
private void onSimpleEventThird(SimpleEvent event) {
++hits;
assertEquals(4, 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 {}