From 88ba515cbf4dda083d52648f6cfdbf3bd3376098 Mon Sep 17 00:00:00 2001 From: kske Date: Wed, 2 Sep 2020 16:15:31 +0200 Subject: [PATCH] Add all relevant classes and event bus logic --- src/main/java/dev/kske/eventbus/Event.java | 34 +++++ src/main/java/dev/kske/eventbus/EventBus.java | 120 ++++++++++++++++++ .../dev/kske/eventbus/EventBusException.java | 21 +++ .../java/dev/kske/eventbus/EventHandler.java | 82 ++++++++++++ .../java/dev/kske/eventbus/EventListener.java | 12 ++ src/main/java/dev/kske/eventbus/IEvent.java | 12 ++ .../java/dev/kske/eventbus/package-info.java | 9 ++ 7 files changed, 290 insertions(+) create mode 100644 src/main/java/dev/kske/eventbus/Event.java create mode 100644 src/main/java/dev/kske/eventbus/EventBus.java create mode 100644 src/main/java/dev/kske/eventbus/EventBusException.java create mode 100644 src/main/java/dev/kske/eventbus/EventHandler.java create mode 100644 src/main/java/dev/kske/eventbus/EventListener.java create mode 100644 src/main/java/dev/kske/eventbus/IEvent.java create mode 100644 src/main/java/dev/kske/eventbus/package-info.java diff --git a/src/main/java/dev/kske/eventbus/Event.java b/src/main/java/dev/kske/eventbus/Event.java new file mode 100644 index 0000000..b67fdc0 --- /dev/null +++ b/src/main/java/dev/kske/eventbus/Event.java @@ -0,0 +1,34 @@ +package dev.kske.eventbus; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.*; + +/** + * Indicates that a method is an event handler. To be successfully used as such, the method has to + * comply with the following specifications: + * + * + * @author Kai S. K. Engelbart + * @since 0.0.1 + */ +@Documented +@Retention(RUNTIME) +@Target(METHOD) +public @interface Event { + + /** + * Defines the priority of the event handler. Handlers are executed in descending order of their + * priority. + *

+ * The execution order of handlers with the same priority is undefined. + * + * @since 0.0.1 + */ + int priority() default 100; +} diff --git a/src/main/java/dev/kske/eventbus/EventBus.java b/src/main/java/dev/kske/eventbus/EventBus.java new file mode 100644 index 0000000..c44cb8d --- /dev/null +++ b/src/main/java/dev/kske/eventbus/EventBus.java @@ -0,0 +1,120 @@ +package dev.kske.eventbus; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Event listeners can be registered at an event bus to be notified when an event is dispatched. + *

+ * This is a thread-safe implementation. + * + * @author Kai S. K. Engelbart + * @since 0.0.1 + * @see Event + */ +public final class EventBus { + + private final Map, Collection> bindings + = new ConcurrentHashMap<>(); + private final Set registeredListeners = ConcurrentHashMap.newKeySet(); + + /** + * Dispatches an event to all event handlers registered for it in descending order of their + * priority. + * + * @param event the event to dispatch + * @since 0.0.1 + */ + public void dispatch(IEvent event) { + getHandlersFor(event.getClass()).forEach(handler -> handler.execute(event)); + } + + /** + * Searches for the event handlers bound to an event class. + * + * @param eventClass the event class to use for the search + * @return all event handlers registered for the event class + * @since 0.0.1 + */ + private List getHandlersFor(Class eventClass) { + return bindings.containsKey(eventClass) ? new ArrayList<>(bindings.get(eventClass)) + : new ArrayList<>(); + } + + /** + * Registers an event listener at this event bus. + * + * @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 + * @since 0.0.1 + * @see Event + */ + public void registerListener(EventListener listener) throws EventBusException { + if (registeredListeners.contains(listener)) + throw new EventBusException(listener + " already registered!"); + + registeredListeners.add(listener); + for (var method : listener.getClass().getDeclaredMethods()) { + Event annotation = method.getAnnotation(Event.class); + + // Skip methods without annotations + 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) param; + if (!bindings.containsKey(realParam)) + bindings.put(realParam, new HashSet<>()); + + bindings.get(realParam).add(new EventHandler(listener, method, annotation)); + } + } + + /** + * Removes a specific listener from this event bus. + * + * @param listener the listener to remove + * @since 0.0.1 + */ + public void removeListener(EventListener listener) { + for (var binding : bindings.values()) { + var it = binding.iterator(); + while (it.hasNext()) + if (it.next().getListener() == listener) + it.remove(); + } + registeredListeners.remove(listener); + } + + /** + * Removes all event listeners from this event bus. + * + * @since 0.0.1 + */ + public void clearListeners() { + bindings.clear(); + registeredListeners.clear(); + } + + /** + * Provides an unmodifiable view of the event listeners registered at this event bus. + * + * @return all registered event listeners + * @since 0.0.1 + */ + public Set getRegisteredListeners() { + return Collections.unmodifiableSet(registeredListeners); + } +} diff --git a/src/main/java/dev/kske/eventbus/EventBusException.java b/src/main/java/dev/kske/eventbus/EventBusException.java new file mode 100644 index 0000000..1c46c80 --- /dev/null +++ b/src/main/java/dev/kske/eventbus/EventBusException.java @@ -0,0 +1,21 @@ +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. + * + * @author Kai S. K. Engelbart + * @since 0.0.1 + */ +public class EventBusException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + public EventBusException(String message, Throwable cause) { + super(message, cause); + } + + public EventBusException(String message) { + super(message); + } +} diff --git a/src/main/java/dev/kske/eventbus/EventHandler.java b/src/main/java/dev/kske/eventbus/EventHandler.java new file mode 100644 index 0000000..7619d72 --- /dev/null +++ b/src/main/java/dev/kske/eventbus/EventHandler.java @@ -0,0 +1,82 @@ +package dev.kske.eventbus; + +import java.lang.reflect.*; + +/** + * Internal representation of an event handling method. + * + * @author Kai S. K. Engelbart + * @since 0.0.1 + * @see EventBus + */ +final class EventHandler implements Comparable { + + private final EventListener listener; + private final Method method; + private final Event annotation; + + /** + * Constructs an event handler. + * + * @param listener the listener containing the handler + * @param method the handler method + * @param annotation the event annotation + * @since 0.0.1 + */ + EventHandler(EventListener listener, Method method, Event annotation) { + this.listener = listener; + this.method = method; + this.annotation = annotation; + } + + /** + * Compares this to another event handler based on {@link Event#priority()}. In case of equal + * priority a non-zero value based on hash codes is returned. + * + * @since 0.0.1 + */ + @Override + public int compareTo(EventHandler other) { + int priority = annotation.priority() - other.annotation.priority(); + if (priority == 0) + priority = listener.hashCode() - other.listener.hashCode(); + return priority == 0 ? hashCode() - other.hashCode() : priority; + } + + /** + * Executes the event handler. + * + * @param event the event used as the method parameter + * @throws EventBusException if the handler throws an exception + * @since 0.0.1 + */ + void execute(IEvent event) throws EventBusException { + try { + method.invoke(listener, event); + } catch ( + IllegalAccessException + | IllegalArgumentException + | InvocationTargetException e + ) { + throw new EventBusException("Failed to invoke event handler!", e); + } + } + + /** + * @return the listener containing this handler + * @since 0.0.1 + */ + EventListener getListener() { return listener; } + + /** + * @return the event annotation + * @since 0.0.1 + */ + Event getAnnotation() { return annotation; } + + /** + * @return the priority of the event annotation + * @since 0.0.1 + */ + int getPriority() { return annotation.priority(); } +} diff --git a/src/main/java/dev/kske/eventbus/EventListener.java b/src/main/java/dev/kske/eventbus/EventListener.java new file mode 100644 index 0000000..bc48ec7 --- /dev/null +++ b/src/main/java/dev/kske/eventbus/EventListener.java @@ -0,0 +1,12 @@ +package dev.kske.eventbus; + +/** + * Marker interface for event listeners. Event listeners can contain event handling methods to which + * events can be dispatched. + * + * @author Kai S. K. Engelbart + * @since 0.0.1 + * @see Event + * @see EventBus + */ +public interface EventListener {} diff --git a/src/main/java/dev/kske/eventbus/IEvent.java b/src/main/java/dev/kske/eventbus/IEvent.java new file mode 100644 index 0000000..fe9e843 --- /dev/null +++ b/src/main/java/dev/kske/eventbus/IEvent.java @@ -0,0 +1,12 @@ +package dev.kske.eventbus; + +/** + * Marker interface for event objects. Event objects can be used as event handler parameters and + * thus can be dispatched to the event bus. + * + * @author Kai S. K. Engelbart + * @since 0.0.1 + * @see Event + * @see EventBus + */ +public interface IEvent {} diff --git a/src/main/java/dev/kske/eventbus/package-info.java b/src/main/java/dev/kske/eventbus/package-info.java new file mode 100644 index 0000000..99c8ccf --- /dev/null +++ b/src/main/java/dev/kske/eventbus/package-info.java @@ -0,0 +1,9 @@ +/** + * Contains the public API and implementation of the event bus library. + * + * @author Kai S. K. Engelbart + * @since 0.0.1 + * @see dev.kske.eventbus.Event + * @see dev.kske.eventbus.EventBus + */ +package dev.kske.eventbus;