Add all relevant classes and event bus logic

This commit is contained in:
Kai S. K. Engelbart 2020-09-02 16:15:31 +02:00
parent 9fdf2a822b
commit 88ba515cbf
Signed by: kske
GPG Key ID: 8BEB13EC5DF7EF13
7 changed files with 290 additions and 0 deletions

View File

@ -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:
* <ul>
* <li>Declared inside a class that implements {@link EventListener}</li>
* <li>One parameter of a type that implements {@link IEvent}</li>
* <li>Return type of {@code void}</li>
* </ul>
*
* @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.
* <p>
* The execution order of handlers with the same priority is undefined.
*
* @since 0.0.1
*/
int priority() default 100;
}

View File

@ -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.
* <p>
* This is a thread-safe implementation.
*
* @author Kai S. K. Engelbart
* @since 0.0.1
* @see Event
*/
public final class EventBus {
private final Map<Class<? extends IEvent>, Collection<EventHandler>> bindings
= new ConcurrentHashMap<>();
private final Set<EventListener> 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<EventHandler> getHandlersFor(Class<? extends IEvent> 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<? extends IEvent>) 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<EventListener> getRegisteredListeners() {
return Collections.unmodifiableSet(registeredListeners);
}
}

View File

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

View File

@ -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<EventHandler> {
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(); }
}

View File

@ -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 {}

View File

@ -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 {}

View File

@ -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;