Add parent project, convert existing project to Maven module
This commit is contained in:
67
event-bus-core/src/main/java/dev/kske/eventbus/Event.java
Normal file
67
event-bus-core/src/main/java/dev/kske/eventbus/Event.java
Normal file
@ -0,0 +1,67 @@
|
||||
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>Specifying an event type by either
|
||||
* <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>
|
||||
* <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.
|
||||
*
|
||||
* @return the priority of the event handler
|
||||
* @since 0.0.1
|
||||
*/
|
||||
int priority() default 100;
|
||||
|
||||
/**
|
||||
* Defines whether instances of subtypes of the event type are dispatched to the event handler.
|
||||
*
|
||||
* @return whether the event handler includes subtypes
|
||||
* @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.
|
||||
*
|
||||
* @return the event type accepted by the handler
|
||||
* @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 {}
|
||||
}
|
213
event-bus-core/src/main/java/dev/kske/eventbus/EventBus.java
Normal file
213
event-bus-core/src/main/java/dev/kske/eventbus/EventBus.java
Normal file
@ -0,0 +1,213 @@
|
||||
package dev.kske.eventbus;
|
||||
|
||||
import java.lang.System.Logger;
|
||||
import java.lang.System.Logger.Level;
|
||||
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>
|
||||
* A singleton instance of this class can be lazily created and acquired using the
|
||||
* {@link EventBus#getInstance()} method.
|
||||
* <p>
|
||||
* This is a thread-safe implementation.
|
||||
*
|
||||
* @author Kai S. K. Engelbart
|
||||
* @since 0.0.1
|
||||
* @see Event
|
||||
*/
|
||||
public final class EventBus {
|
||||
|
||||
/**
|
||||
* Holds the state of the dispatching process on one thread.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
private static final class DispatchState {
|
||||
|
||||
boolean isDispatching, isCancelled;
|
||||
}
|
||||
|
||||
private static volatile EventBus singletonInstance;
|
||||
|
||||
private static final Logger logger = System.getLogger(EventBus.class.getName());
|
||||
|
||||
/**
|
||||
* Produces a singleton instance of the event bus. It is lazily initialized on the first call.
|
||||
*
|
||||
* @return a singleton instance of the event bus.
|
||||
* @since 0.0.2
|
||||
*/
|
||||
public static EventBus getInstance() {
|
||||
EventBus instance = singletonInstance;
|
||||
if (instance == null)
|
||||
synchronized (EventBus.class) {
|
||||
if ((instance = singletonInstance) == null) {
|
||||
logger.log(Level.DEBUG, "Initializing singleton event bus instance");
|
||||
instance = singletonInstance = new EventBus();
|
||||
}
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
private final Map<Class<? extends IEvent>, TreeSet<EventHandler>> bindings
|
||||
= new ConcurrentHashMap<>();
|
||||
private final Set<EventListener> registeredListeners = ConcurrentHashMap.newKeySet();
|
||||
private final ThreadLocal<DispatchState> dispatchState
|
||||
= ThreadLocal.withInitial(DispatchState::new);
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
Objects.requireNonNull(event);
|
||||
logger.log(Level.INFO, "Dispatching event {0}", event);
|
||||
|
||||
// Set dispatch state
|
||||
var state = dispatchState.get();
|
||||
state.isDispatching = true;
|
||||
|
||||
for (var handler : getHandlersFor(event.getClass()))
|
||||
if (state.isCancelled) {
|
||||
logger.log(Level.INFO, "Cancelled dispatching event {0}", event);
|
||||
state.isCancelled = false;
|
||||
break;
|
||||
} else {
|
||||
handler.execute(event);
|
||||
}
|
||||
|
||||
// Reset dispatch state
|
||||
state.isDispatching = false;
|
||||
|
||||
logger.log(Level.DEBUG, "Finished dispatching event {0}", 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) {
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels an event that is currently dispatched from inside an event handler.
|
||||
*
|
||||
* @throws EventBusException if the calling thread is not an active dispatching thread
|
||||
* @since 0.1.0
|
||||
*/
|
||||
public void cancel() {
|
||||
var state = dispatchState.get();
|
||||
if (state.isDispatching && !state.isCancelled)
|
||||
state.isCancelled = true;
|
||||
else
|
||||
throw new EventBusException("Calling thread not an active dispatching thread!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 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!");
|
||||
logger.log(Level.INFO, "Registering event listener {0}", listener.getClass().getName());
|
||||
boolean handlerBound = false;
|
||||
|
||||
registeredListeners.add(listener);
|
||||
for (var method : listener.getClass().getDeclaredMethods()) {
|
||||
Event annotation = method.getAnnotation(Event.class);
|
||||
|
||||
// Skip methods without annotations
|
||||
if (annotation == null)
|
||||
continue;
|
||||
|
||||
// Initialize and bind the handler
|
||||
var handler = new EventHandler(listener, method, annotation);
|
||||
if (!bindings.containsKey(handler.getEventType()))
|
||||
bindings.put(handler.getEventType(), new TreeSet<>());
|
||||
logger.log(Level.DEBUG, "Binding event handler {0}", handler);
|
||||
bindings.get(handler.getEventType())
|
||||
.add(handler);
|
||||
handlerBound = true;
|
||||
}
|
||||
|
||||
if(!handlerBound)
|
||||
logger.log(
|
||||
Level.WARNING,
|
||||
"No event handlers bound for event listener {0}",
|
||||
listener.getClass().getName()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a specific listener from this event bus.
|
||||
*
|
||||
* @param listener the listener to remove
|
||||
* @since 0.0.1
|
||||
*/
|
||||
public void removeListener(EventListener listener) {
|
||||
Objects.requireNonNull(listener);
|
||||
logger.log(Level.INFO, "Removing event listener {0}", listener.getClass().getName());
|
||||
|
||||
for (var binding : bindings.values()) {
|
||||
var it = binding.iterator();
|
||||
while (it.hasNext()) {
|
||||
var handler = it.next();
|
||||
if (handler.getListener() == listener) {
|
||||
logger.log(Level.DEBUG, "Unbinding event handler {0}", handler);
|
||||
it.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
registeredListeners.remove(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all event listeners from this event bus.
|
||||
*
|
||||
* @since 0.0.1
|
||||
*/
|
||||
public void clearListeners() {
|
||||
logger.log(Level.INFO, "Clearing event listeners");
|
||||
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);
|
||||
}
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
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;
|
||||
|
||||
/**
|
||||
* Creates a new event bus exception.
|
||||
*
|
||||
* @param message the message to display
|
||||
* @param cause the cause of this exception
|
||||
*/
|
||||
public EventBusException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new event bus exception.
|
||||
*
|
||||
* @param message the message to display
|
||||
*/
|
||||
public EventBusException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
136
event-bus-core/src/main/java/dev/kske/eventbus/EventHandler.java
Normal file
136
event-bus-core/src/main/java/dev/kske/eventbus/EventHandler.java
Normal file
@ -0,0 +1,136 @@
|
||||
package dev.kske.eventbus;
|
||||
|
||||
import java.lang.reflect.*;
|
||||
|
||||
import dev.kske.eventbus.Event.USE_PARAMETER;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
private final Class<? extends IEvent> eventType;
|
||||
|
||||
/**
|
||||
* Constructs an event handler.
|
||||
*
|
||||
* @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
|
||||
*/
|
||||
@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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* <p>
|
||||
* This is used to retrieve event handlers in the correct order from a tree set.
|
||||
*
|
||||
* @since 0.0.1
|
||||
*/
|
||||
@Override
|
||||
public int compareTo(EventHandler other) {
|
||||
int priority = other.annotation.priority() - annotation.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, annotation=%s]", method, annotation);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
if (annotation.eventType().equals(USE_PARAMETER.class))
|
||||
method.invoke(listener, event);
|
||||
else
|
||||
method.invoke(listener);
|
||||
} 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(); }
|
||||
|
||||
/**
|
||||
* @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; }
|
||||
}
|
@ -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 {}
|
12
event-bus-core/src/main/java/dev/kske/eventbus/IEvent.java
Normal file
12
event-bus-core/src/main/java/dev/kske/eventbus/IEvent.java
Normal 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 {}
|
@ -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;
|
12
event-bus-core/src/main/java/module-info.java
Normal file
12
event-bus-core/src/main/java/module-info.java
Normal 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;
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
package dev.kske.eventbus;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
import org.junit.jupiter.api.*;
|
||||
|
||||
/**
|
||||
* Tests the event cancellation mechanism of the event bus.
|
||||
*
|
||||
* @author Kai S. K. Engelbart
|
||||
* @author Leon Hofmeister
|
||||
* @since 0.1.0
|
||||
*/
|
||||
class CancelTest implements EventListener {
|
||||
|
||||
EventBus bus;
|
||||
int hits;
|
||||
|
||||
/**
|
||||
* Constructs an event bus and registers this test instance as an event listener.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
@BeforeEach
|
||||
void registerListener() {
|
||||
bus = new EventBus();
|
||||
bus.registerListener(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests {@link EventBus#cancel()} with two event handlers, of which the first cancels the
|
||||
* event.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
@Test
|
||||
void testCancellation() {
|
||||
bus.dispatch(new SimpleEvent());
|
||||
assertEquals(1, hits);
|
||||
}
|
||||
|
||||
@Event(eventType = SimpleEvent.class, priority = 100)
|
||||
void onSimpleFirst() {
|
||||
++hits;
|
||||
bus.cancel();
|
||||
}
|
||||
|
||||
@Event(eventType = SimpleEvent.class, priority = 50)
|
||||
void onSimpleSecond() {
|
||||
++hits;
|
||||
}
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
package dev.kske.eventbus;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import org.junit.jupiter.api.*;
|
||||
|
||||
/**
|
||||
* Tests the dispatching mechanism of the event bus.
|
||||
*
|
||||
* @author Kai S. K. Engelbart
|
||||
* @since 0.0.1
|
||||
*/
|
||||
class DispatchTest implements EventListener {
|
||||
|
||||
EventBus bus;
|
||||
static int hits;
|
||||
|
||||
/**
|
||||
* Constructs an event bus and registers this test instance as an event listener.
|
||||
*
|
||||
* @since 0.0.1
|
||||
*/
|
||||
@BeforeEach
|
||||
void registerListener() {
|
||||
bus = new EventBus();
|
||||
bus.registerListener(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests {@link EventBus#dispatch(IEvent)} with multiple handler priorities, a subtype handler
|
||||
* and a static handler.
|
||||
*
|
||||
* @since 0.0.1
|
||||
*/
|
||||
@Test
|
||||
void testDispatch() {
|
||||
bus.dispatch(new SimpleEventSub());
|
||||
bus.dispatch(new SimpleEvent());
|
||||
}
|
||||
|
||||
@Event(eventType = SimpleEvent.class, includeSubtypes = true, priority = 200)
|
||||
void onSimpleEventFirst() {
|
||||
++hits;
|
||||
assertTrue(hits == 1 || hits == 2);
|
||||
}
|
||||
|
||||
@Event(eventType = SimpleEvent.class, priority = 150)
|
||||
static void onSimpleEventSecond() {
|
||||
++hits;
|
||||
assertEquals(3, hits);
|
||||
}
|
||||
|
||||
@Event(priority = 100)
|
||||
void onSimpleEventThird(SimpleEvent event) {
|
||||
++hits;
|
||||
assertEquals(4, hits);
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
package dev.kske.eventbus;
|
||||
|
||||
/**
|
||||
* A simple event for testing purposes.
|
||||
*
|
||||
* @author Kai S. K. Engelbart
|
||||
* @since 0.0.1
|
||||
*/
|
||||
public class SimpleEvent implements IEvent {}
|
@ -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 {}
|
Reference in New Issue
Block a user