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