Add Callback Event Handling #26

Merged
kske merged 3 commits from f/callback-handler into develop 2021-11-05 08:49:07 +01:00
7 changed files with 280 additions and 87 deletions
Showing only changes of commit ee688929fd - Show all commits

View File

@ -22,7 +22,7 @@ import java.lang.annotation.*;
public @interface Event { public @interface Event {
/** /**
* Defines the event type the handler listens to. If this value is set, the handler is not * Defines the event type the handler listens for. 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.

View File

@ -5,6 +5,9 @@ import java.lang.System.Logger.Level;
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.InvocationTargetException;
import java.util.*; import java.util.*;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
import dev.kske.eventbus.core.handler.*;
/** /**
* Event listeners can be registered at an event bus to be notified when an event is dispatched. * Event listeners can be registered at an event bus to be notified when an event is dispatched.
@ -56,6 +59,19 @@ public final class EventBus {
private static final Logger logger = System.getLogger(EventBus.class.getName()); private static final Logger logger = System.getLogger(EventBus.class.getName());
/**
* Compares event handlers based on priority, but uses hash codes for equal priorities.
*
* @implNote As the priority comparator by itself is not consistent with equals (two handlers
* with the same priority are not necessarily equal, but would have a comparison
* result of 0), the hash code is used for the fallback comparison. This way,
* consistency with equals is restored.
* @since 1.2.0
*/
private static final Comparator<EventHandler> byPriority =
Comparator.comparingInt(EventHandler::getPriority).reversed()
.thenComparingInt(EventHandler::hashCode);
/** /**
* Returns the default event bus, which is a statically initialized singleton instance. * Returns the default event bus, which is a statically initialized singleton instance.
* *
@ -154,18 +170,19 @@ public final class EventBus {
* Searches for the event handlers bound to an event class. This includes polymorphic handlers * Searches for the event handlers bound to an event class. This includes polymorphic handlers
* that are bound to a supertype of the event class. * that are bound to a supertype of the event class.
* *
* @param eventClass the event class to use for the search * @param eventType the event type to use for the search
* @return a navigable set containing the applicable handlers in descending order of priority * @return a navigable set containing the applicable handlers in descending order of priority
* @since 1.2.0 * @since 1.2.0
*/ */
private NavigableSet<EventHandler> getHandlersFor(Class<?> eventClass) { private NavigableSet<EventHandler> getHandlersFor(Class<?> eventType) {
// Get handlers defined for the event class // Get handlers defined for the event class
TreeSet<EventHandler> handlers = bindings.getOrDefault(eventClass, new TreeSet<>()); TreeSet<EventHandler> handlers =
bindings.getOrDefault(eventType, new TreeSet<>(byPriority));
// Get polymorphic handlers // Get polymorphic handlers
for (var binding : bindings.entrySet()) for (var binding : bindings.entrySet())
if (binding.getKey().isAssignableFrom(eventClass)) if (binding.getKey().isAssignableFrom(eventType))
for (var handler : binding.getValue()) for (var handler : binding.getValue())
if (handler.isPolymorphic()) if (handler.isPolymorphic())
handlers.add(handler); handlers.add(handler);
@ -223,11 +240,8 @@ public final class EventBus {
continue; continue;
// Initialize and bind the handler // Initialize and bind the handler
var handler = new EventHandler(listener, method, annotation, polymorphic, priority); bindHandler(
bindings.putIfAbsent(handler.getEventType(), new TreeSet<>()); new ReflectiveEventHandler(listener, method, annotation, polymorphic, priority));
logger.log(Level.DEBUG, "Binding event handler {0}", handler);
bindings.get(handler.getEventType())
.add(handler);
handlerBound = true; handlerBound = true;
} }
@ -238,6 +252,85 @@ public final class EventBus {
listener.getClass().getName()); listener.getClass().getName());
} }
/**
* Registers a callback listener, which is a consumer that is invoked when an event occurs. The
* listener is not polymorphic and has the {@link #DEFAULT_PRIORITY}.
*
* @param <E> the event type the listener listens for
* @param eventType the event type the listener listens for
* @param callback the callback that is invoked when an event occurs
* @since 1.2.0
* @see #registerListener(Class, Consumer, boolean, int)
*/
public <E> void registerListener(Class<E> eventType, Consumer<E> callback) {
registerListener(eventType, callback, false, DEFAULT_PRIORITY);
}
/**
* Registers a callback listener, which is a consumer that is invoked when an event occurs. The
* listener has the {@link #DEFAULT_PRIORITY}.
*
* @param <E> the event type the listener listens for
* @param eventType the event type the listener listens for
* @param callback the callback that is invoked when an event occurs
* @param polymorphic whether the listener is also invoked for subtypes of the event type
* @since 1.2.0
* @see #registerListener(Class, Consumer, boolean, int)
*/
public <E> void registerListener(Class<E> eventType, Consumer<E> callback,
kske marked this conversation as resolved Outdated
Outdated
Review

Consumer<E> eventListener?

`Consumer<E> eventListener`?
boolean polymorphic) {
registerListener(eventType, callback, polymorphic, DEFAULT_PRIORITY);
}
/**
* Registers a callback listener, which is a consumer that is invoked when an event occurs. The
* listener is not polymorphic.
*
* @param <E> the event type the listener listens for
* @param eventType the event type the listener listens for
* @param callback the callback that is invoked when an event occurs
* @param priority the priority to assign to the listener
* @since 1.2.0
* @see #registerListener(Class, Consumer, boolean, int)
*/
public <E> void registerListener(Class<E> eventType, Consumer<E> callback, int priority) {
kske marked this conversation as resolved Outdated
Outdated
Review

eventListener?

eventListener?
registerListener(eventType, callback, false, priority);
}
/**
* Registers a callback listener, which is a consumer that is invoked when an event occurs.
*
* @param <E> the event type the listener listens for
* @param eventType the event type the listener listens for
* @param callback the callback that is invoked when an event occurs
* @param polymorphic whether the listener is also invoked for subtypes of the event type
* @param priority the priority to assign to the listener
* @since 1.2.0
*/
public <E> void registerListener(Class<E> eventType, Consumer<E> callback, boolean polymorphic,
kske marked this conversation as resolved Outdated
Outdated
Review

eventListener?

eventListener?
int priority) {
Objects.requireNonNull(callback);
if (registeredListeners.contains(callback))
throw new EventBusException(callback + " already registered!");
logger.log(Level.INFO, "Registering callback event listener {0}",
callback.getClass().getName());
registeredListeners.add(callback);
bindHandler(new CallbackEventHandler(eventType, callback, polymorphic, priority));
}
/**
* Inserts a new handler into the {@link #bindings} map.
*
* @param handler the handler to bind
* @since 1.2.0
*/
private void bindHandler(EventHandler handler) {
bindings.putIfAbsent(handler.getEventType(), new TreeSet<>(byPriority));
logger.log(Level.DEBUG, "Binding event handler {0}", handler);
bindings.get(handler.getEventType()).add(handler);
}
/** /**
* Removes a specific listener from this event bus. * Removes a specific listener from this event bus.
* *

View File

@ -0,0 +1,73 @@
package dev.kske.eventbus.core.handler;
import java.lang.reflect.InvocationTargetException;
import java.util.function.Consumer;
/**
* An event handler wrapping a callback method.
*
* @author Kai S. K. Engelbart
* @since 1.2.0
*/
public class CallbackEventHandler implements EventHandler {
kske marked this conversation as resolved Outdated
Outdated
Review

final?

final?
private final Class<?> eventType;
private final Consumer<Object> callback;
delvh marked this conversation as resolved
Review

What about Consumer ?

What about Consumer<?> ?
Review

This wouldn't work, as `callback.accept(event)' would complain about an incompatible argument type.

This wouldn't work, as `callback.accept(event)' would complain about an incompatible argument type.
private final boolean polymorphic;
private final int priority;
/**
* Constructs a callback event handler.
*
* @param <E> the event type of the handler
* @param eventType the event type of the handler
kske marked this conversation as resolved
Review

😆

:laugh:
* @param callback the callback method to execute when the handler is invoked
* @param polymorphic whether the handler is polymorphic
* @param priority the priority of the handler
* @since 1.2.0
*/
@SuppressWarnings("unchecked")
public <E> CallbackEventHandler(Class<E> eventType, Consumer<E> callback, boolean polymorphic,
int priority) {
this.eventType = eventType;
this.callback = (Consumer<Object>) callback;
kske marked this conversation as resolved
Review

😆

:laugh:
this.polymorphic = polymorphic;
this.priority = priority;
}
@Override
public void execute(Object event) throws InvocationTargetException {
try {
callback.accept(event);
} catch (RuntimeException e) {
throw new InvocationTargetException(e, "Callback event handler failed!");
}
}
@Override
public String toString() {
return String.format(
"CallbackEventHandler[eventType=%s, polymorphic=%b, priority=%d]",
eventType, polymorphic, priority);
}
@Override
public Consumer<?> getListener() {
return callback;
}
@Override
public Class<?> getEventType() {
return eventType;
}
@Override
public int getPriority() {
return priority;
}
@Override
public boolean isPolymorphic() {
return polymorphic;
}
}

View File

@ -0,0 +1,53 @@
package dev.kske.eventbus.core.handler;
import java.lang.reflect.InvocationTargetException;
import dev.kske.eventbus.core.*;
/**
* Internal representation of an event handling method.
*
* @author Kai S. K. Engelbart
* @since 1.2.0
* @see EventBus
*/
public interface EventHandler {
/**
* Executes the event handler.
*
* @param event the event used as the method parameter
* @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 1.2.0
*/
void execute(Object event) throws EventBusException, InvocationTargetException;
/**
* @return the listener containing this handler
* @since 1.2.0
*/
Object getListener();
/**
* @return the event type this handler listens for
* @since 1.2.0
*/
Class<?> getEventType();
/**
* @return the priority of this handler
* @since 1.2.0
* @see Priority
*/
int getPriority();
/**
* @return whether this handler is polymorphic
kske marked this conversation as resolved Outdated
Outdated
Review

... accepts subtypes of the event type as well

... accepts subtypes of the event type as well
* @since 1.2.0
* @see Polymorphic
*/
boolean isPolymorphic();
}

View File

@ -1,17 +1,18 @@
package dev.kske.eventbus.core; package dev.kske.eventbus.core.handler;
import java.lang.reflect.*; import java.lang.reflect.*;
import dev.kske.eventbus.core.*;
import dev.kske.eventbus.core.Event.USE_PARAMETER; import dev.kske.eventbus.core.Event.USE_PARAMETER;
/** /**
* Internal representation of an event handling method. * An event handler wrapping a method annotated with {@link Event} and executing it using
* reflection.
* *
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
* @since 0.0.1 * @since 1.2.0
* @see EventBus
*/ */
final class EventHandler implements Comparable<EventHandler> { public final class ReflectiveEventHandler implements EventHandler {
private final Object listener; private final Object listener;
private final Method method; private final Method method;
@ -21,7 +22,7 @@ final class EventHandler implements Comparable<EventHandler> {
private final int priority; private final int priority;
/** /**
* Constructs an event handler. * Constructs a reflective event handler.
* *
* @param listener the listener containing the handler * @param listener the listener containing the handler
* @param method the handler method * @param method the handler method
@ -30,10 +31,10 @@ final class EventHandler implements Comparable<EventHandler> {
* @param defPriority the predefined priority (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 * @throws EventBusException if the method or the annotation do not comply with the
* specification * specification
* @since 0.0.1 * @since 1.2.0
*/ */
EventHandler(Object listener, Method method, Event annotation, boolean defPolymorphism, public ReflectiveEventHandler(Object listener, Method method, Event annotation,
int defPriority) throws EventBusException { boolean defPolymorphism, int defPriority) throws EventBusException {
this.listener = listener; this.listener = listener;
this.method = method; this.method = method;
useParameter = annotation.value() == USE_PARAMETER.class; useParameter = annotation.value() == USE_PARAMETER.class;
@ -61,45 +62,13 @@ final class EventHandler implements Comparable<EventHandler> {
method.setAccessible(true); method.setAccessible(true);
} }
/**
* 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 descending order of priority from a tree set.
*
* @since 0.0.1
*/
@Override @Override
public int compareTo(EventHandler other) { public void execute(Object event) throws EventBusException, InvocationTargetException {
int priority = other.priority - this.priority;
if (priority == 0)
priority = listener.hashCode() - other.listener.hashCode();
return priority == 0 ? hashCode() - other.hashCode() : priority;
}
@Override
public String toString() {
return String.format(
"EventHandler[method=%s, eventType=%s, useParameter=%b, polymorphic=%b, priority=%d]",
method, eventType, useParameter, polymorphic, priority);
}
/**
* Executes the event handler.
*
* @param event the event used as the method parameter
* @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, InvocationTargetException {
try { try {
if (useParameter) if (useParameter)
method.invoke(listener, event); method.invoke(getListener(), event);
else else
method.invoke(listener); method.invoke(getListener());
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
throw new EventBusException("Event handler rejected target / argument!", e); throw new EventBusException("Event handler rejected target / argument!", e);
} catch (IllegalAccessException e) { } catch (IllegalAccessException e) {
@ -107,29 +76,30 @@ final class EventHandler implements Comparable<EventHandler> {
} }
} }
/** @Override
* @return the listener containing this handler public String toString() {
* @since 0.0.1 return String.format(
*/ "ReflectiveEventHandler[eventType=%s, polymorphic=%b, priority=%d, method=%s, useParameter=%b]",
Object getListener() { return listener; } eventType, polymorphic, priority, method, useParameter);
}
/**
* @return the event type this handler listens for @Override
* @since 0.0.3 public Object getListener() {
*/ return listener;
Class<?> getEventType() { return eventType; } }
/** @Override
* @return the priority of this handler public Class<?> getEventType() {
* @since 0.0.1 return eventType;
* @see Priority }
*/
int getPriority() { return priority; } @Override
public int getPriority() {
/** return priority;
* @return whether this handler is polymorphic }
* @since 1.0.0
* @see Polymorphic @Override
*/ public boolean isPolymorphic() {
boolean isPolymorphic() { return polymorphic; } return polymorphic;
}
} }

View File

@ -0,0 +1,8 @@
/**
* Contains the internal representation of event handling methods.
*
* @author Kai S. K. Engelbart
* @since 1.2.0
* @see dev.kske.eventbus.core.handler.EventHandler
*/
package dev.kske.eventbus.core.handler;

View File

@ -26,6 +26,10 @@ class DispatchTest {
void registerListener() { void registerListener() {
bus = new EventBus(); bus = new EventBus();
bus.registerListener(this); bus.registerListener(this);
bus.registerListener(SimpleEvent.class, e -> {
++hits;
assertEquals(4, hits);
});
} }
/** /**
@ -52,9 +56,9 @@ class DispatchTest {
assertEquals( assertEquals(
"Event handler execution order for class dev.kske.eventbus.core.SimpleEvent (3 handler(s)):\n" "Event handler execution order for class dev.kske.eventbus.core.SimpleEvent (3 handler(s)):\n"
+ "==========================================================================================\n" + "==========================================================================================\n"
+ "EventHandler[method=void dev.kske.eventbus.core.DispatchTest.onSimpleEventFirst(), eventType=class dev.kske.eventbus.core.SimpleEvent, useParameter=false, polymorphic=true, priority=200]\n" + "ReflectiveEventHandler[eventType=class dev.kske.eventbus.core.SimpleEvent, polymorphic=true, priority=200, method=void dev.kske.eventbus.core.DispatchTest.onSimpleEventFirst(), useParameter=false]\n"
+ "EventHandler[method=static void dev.kske.eventbus.core.DispatchTest.onSimpleEventSecond(), eventType=class dev.kske.eventbus.core.SimpleEvent, useParameter=false, polymorphic=false, priority=150]\n" + "ReflectiveEventHandler[eventType=class dev.kske.eventbus.core.SimpleEvent, polymorphic=false, priority=150, method=static void dev.kske.eventbus.core.DispatchTest.onSimpleEventSecond(), useParameter=false]\n"
+ "EventHandler[method=void dev.kske.eventbus.core.DispatchTest.onSimpleEventThird(dev.kske.eventbus.core.SimpleEvent), eventType=class dev.kske.eventbus.core.SimpleEvent, useParameter=true, polymorphic=false, priority=100]\n" + "CallbackEventHandler[eventType=class dev.kske.eventbus.core.SimpleEvent, polymorphic=false, priority=100]\n"
+ "==========================================================================================", + "==========================================================================================",
executionOrder); executionOrder);
} }
@ -72,12 +76,4 @@ class DispatchTest {
++hits; ++hits;
assertEquals(3, hits); assertEquals(3, hits);
} }
@Event
@Polymorphic(false)
@Priority(100)
void onSimpleEventThird(SimpleEvent event) {
++hits;
assertEquals(4, hits);
}
} }