Compare commits
	
		
			20 Commits
		
	
	
		
			1.1.0
			...
			5ddef71c26
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 5ddef71c26   | |||
| 85b2da391a   | |||
| 46a358da97 | |||
| 6bf9e1097a | |||
| e67b64678b   | |||
| c614beb063 | |||
| d3abb0aca3 | |||
| ee688929fd | |||
| 897d794b86   | |||
| 40d48cb959   | |||
| b760c58298 | |||
| 872b395374 | |||
| 82c66c45ec | |||
| 866a547114   | |||
| 33ebf0302b | |||
| b915a5c490   | |||
| 205a183db7 | |||
| 74447dea59 | |||
| 6eebd3c121 | |||
| b758f4cef1 | 
| @@ -45,9 +45,6 @@ public class SimpleEventListener { | ||||
| } | ||||
| ``` | ||||
|  | ||||
| In this case, an event bus is created and used locally. | ||||
| In a more sophisticated example the class would acquire an external event bus that is used by multiple classes. | ||||
|  | ||||
| Note that creating static event handlers like this | ||||
|  | ||||
| ```java | ||||
|   | ||||
| @@ -16,7 +16,7 @@ | ||||
| 		<dependency> | ||||
| 			<groupId>org.junit.jupiter</groupId> | ||||
| 			<artifactId>junit-jupiter-api</artifactId> | ||||
| 			<version>5.6.2</version> | ||||
| 			<version>5.8.1</version> | ||||
| 			<scope>test</scope> | ||||
| 		</dependency> | ||||
| 	</dependencies> | ||||
| @@ -26,5 +26,14 @@ | ||||
| 		<!-- Disable resource folder --> | ||||
| 		<resources /> | ||||
|  | ||||
| 		<!-- Run unit tests --> | ||||
| 		<plugins> | ||||
| 			<plugin> | ||||
| 				<groupId>org.apache.maven.plugins</groupId> | ||||
| 				<artifactId>maven-surefire-plugin</artifactId> | ||||
| 				<version>3.0.0-M5</version> | ||||
| 			</plugin> | ||||
| 		</plugins> | ||||
|  | ||||
| 	</build> | ||||
| </project> | ||||
| @@ -22,7 +22,7 @@ import java.lang.annotation.*; | ||||
| 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. | ||||
| 	 * <p> | ||||
| 	 * This is useful when the event handler does not utilize the event instance. | ||||
|   | ||||
| @@ -5,6 +5,9 @@ import java.lang.System.Logger.Level; | ||||
| import java.lang.reflect.InvocationTargetException; | ||||
| import java.util.*; | ||||
| 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. | ||||
| @@ -27,7 +30,21 @@ public final class EventBus { | ||||
| 	 */ | ||||
| 	private static final class DispatchState { | ||||
|  | ||||
| 		boolean isDispatching, isCancelled; | ||||
| 		/** | ||||
| 		 * Indicates that the last event handler invoked has called {@link EventBus#cancel}. In that | ||||
| 		 * case, the event is not dispatched further. | ||||
| 		 * | ||||
| 		 * @since 0.1.0 | ||||
| 		 */ | ||||
| 		boolean isCancelled; | ||||
|  | ||||
| 		/** | ||||
| 		 * Is incremented when {@link EventBus#dispatch(Object)} is invoked and decremented when it | ||||
| 		 * finishes. This allows keeping track of nested dispatches. | ||||
| 		 * | ||||
| 		 * @since 1.2.0 | ||||
| 		 */ | ||||
| 		int nestingCount; | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| @@ -38,33 +55,55 @@ public final class EventBus { | ||||
| 	 */ | ||||
| 	public static final int DEFAULT_PRIORITY = 100; | ||||
|  | ||||
| 	private static volatile EventBus singletonInstance; | ||||
| 	private static final EventBus singletonInstance = new EventBus(); | ||||
|  | ||||
| 	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. | ||||
| 	 * Compares event handlers based on priority, but uses hash codes for equal priorities. | ||||
| 	 * | ||||
| 	 * @return a singleton instance of the event bus. | ||||
| 	 * @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. | ||||
| 	 * | ||||
| 	 * @return the default 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; | ||||
| 		return singletonInstance; | ||||
| 	} | ||||
|  | ||||
| 	private final Map<Class<?>, TreeSet<EventHandler>>	bindings			= | ||||
| 		new ConcurrentHashMap<>(); | ||||
| 	private final Set<Object>							registeredListeners	= | ||||
| 		ConcurrentHashMap.newKeySet(); | ||||
| 	private final ThreadLocal<DispatchState>			dispatchState		= | ||||
| 	/** | ||||
| 	 * Event handler bindings (target class to handlers registered for that class), does not contain | ||||
| 	 * other (polymorphic) handlers. | ||||
| 	 * | ||||
| 	 * @since 0.0.1 | ||||
| 	 */ | ||||
| 	private final Map<Class<?>, TreeSet<EventHandler>> bindings = new ConcurrentHashMap<>(); | ||||
|  | ||||
| 	/** | ||||
| 	 * Stores all registered event listeners (which declare event handlers) and prevents them from | ||||
| 	 * being garbage collected. | ||||
| 	 * | ||||
| 	 * @since 0.0.1 | ||||
| 	 */ | ||||
| 	private final Set<Object> registeredListeners = ConcurrentHashMap.newKeySet(); | ||||
|  | ||||
| 	/** | ||||
| 	 * The current event dispatching state, local to each thread. | ||||
| 	 * | ||||
| 	 * @since 0.1.0 | ||||
| 	 */ | ||||
| 	private final ThreadLocal<DispatchState> dispatchState = | ||||
| 		ThreadLocal.withInitial(DispatchState::new); | ||||
|  | ||||
| 	/** | ||||
| @@ -72,18 +111,21 @@ public final class EventBus { | ||||
| 	 * priority. | ||||
| 	 * | ||||
| 	 * @param event the event to dispatch | ||||
| 	 * @throws EventBusException if an event handler isn't accessible or has an invalid signature | ||||
| 	 * @throws EventBusException    if an event handler isn't accessible or has an invalid signature | ||||
| 	 * @throws NullPointerException if the specified event is {@code null} | ||||
| 	 * @since 0.0.1 | ||||
| 	 */ | ||||
| 	public void dispatch(Object event) throws EventBusException { | ||||
| 		Objects.requireNonNull(event); | ||||
| 		logger.log(Level.INFO, "Dispatching event {0}", event); | ||||
|  | ||||
| 		// Set dispatch state | ||||
| 		// Look up dispatch state | ||||
| 		var state = dispatchState.get(); | ||||
| 		state.isDispatching = true; | ||||
|  | ||||
| 		Iterator<EventHandler> handlers = getHandlersFor(event.getClass()); | ||||
| 		// Increment nesting count (becomes > 1 during nested dispatches) | ||||
| 		++state.nestingCount; | ||||
|  | ||||
| 		Iterator<EventHandler> handlers = getHandlersFor(event.getClass()).iterator(); | ||||
| 		if (handlers.hasNext()) { | ||||
| 			while (handlers.hasNext()) | ||||
| 				if (state.isCancelled) { | ||||
| @@ -94,14 +136,14 @@ public final class EventBus { | ||||
| 					try { | ||||
| 						handlers.next().execute(event); | ||||
| 					} catch (InvocationTargetException e) { | ||||
| 						if (event instanceof DeadEvent || event instanceof ExceptionEvent) | ||||
|  | ||||
| 							// Warn about system event not being handled | ||||
| 							logger.log(Level.WARNING, event + " not handled due to exception", e); | ||||
| 						else if (e.getCause() instanceof Error) | ||||
| 						if (e.getCause() instanceof Error) | ||||
|  | ||||
| 							// Transparently pass error to the caller | ||||
| 							throw (Error) e.getCause(); | ||||
| 						else if (event instanceof DeadEvent || event instanceof ExceptionEvent) | ||||
|  | ||||
| 							// Warn about system event not being handled | ||||
| 							logger.log(Level.WARNING, event + " not handled due to exception", e); | ||||
| 						else | ||||
|  | ||||
| 							// Dispatch exception event | ||||
| @@ -118,8 +160,8 @@ public final class EventBus { | ||||
| 			dispatch(new DeadEvent(this, event)); | ||||
| 		} | ||||
|  | ||||
| 		// Reset dispatch state | ||||
| 		state.isDispatching = false; | ||||
| 		// Decrement nesting count (becomes 0 when all dispatches on the thread are finished) | ||||
| 		--state.nestingCount; | ||||
|  | ||||
| 		logger.log(Level.DEBUG, "Finished dispatching event {0}", event); | ||||
| 	} | ||||
| @@ -128,23 +170,24 @@ public final class EventBus { | ||||
| 	 * Searches for the event handlers bound to an event class. This includes polymorphic handlers | ||||
| 	 * that are bound to a supertype of the event class. | ||||
| 	 * | ||||
| 	 * @param eventClass the event class to use for the search | ||||
| 	 * @return an iterator over the applicable handlers in descending order of priority | ||||
| 	 * @since 0.0.1 | ||||
| 	 * @param eventType the event type to use for the search | ||||
| 	 * @return a navigable set containing the applicable handlers in descending order of priority | ||||
| 	 * @since 1.2.0 | ||||
| 	 */ | ||||
| 	private Iterator<EventHandler> getHandlersFor(Class<?> eventClass) { | ||||
| 	private NavigableSet<EventHandler> getHandlersFor(Class<?> eventType) { | ||||
|  | ||||
| 		// 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 | ||||
| 		for (var binding : bindings.entrySet()) | ||||
| 			if (binding.getKey().isAssignableFrom(eventClass)) | ||||
| 			if (binding.getKey().isAssignableFrom(eventType)) | ||||
| 				for (var handler : binding.getValue()) | ||||
| 					if (handler.isPolymorphic()) | ||||
| 						handlers.add(handler); | ||||
|  | ||||
| 		return handlers.iterator(); | ||||
| 		return handlers; | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| @@ -155,7 +198,7 @@ public final class EventBus { | ||||
| 	 */ | ||||
| 	public void cancel() { | ||||
| 		var state = dispatchState.get(); | ||||
| 		if (state.isDispatching && !state.isCancelled) | ||||
| 		if (state.nestingCount > 0 && !state.isCancelled) | ||||
| 			state.isCancelled = true; | ||||
| 		else | ||||
| 			throw new EventBusException("Calling thread not an active dispatching thread!"); | ||||
| @@ -165,8 +208,9 @@ public final class EventBus { | ||||
| 	 * 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 | ||||
| 	 * @throws EventBusException    if the listener is already registered or a declared event | ||||
| 	 *                              handler does not comply with the specification | ||||
| 	 * @throws NullPointerException if the specified listener is {@code null} | ||||
| 	 * @since 0.0.1 | ||||
| 	 * @see Event | ||||
| 	 */ | ||||
| @@ -196,11 +240,8 @@ public final class EventBus { | ||||
| 				continue; | ||||
|  | ||||
| 			// Initialize and bind the handler | ||||
| 			var handler = new EventHandler(listener, method, annotation, polymorphic, priority); | ||||
| 			bindings.putIfAbsent(handler.getEventType(), new TreeSet<>()); | ||||
| 			logger.log(Level.DEBUG, "Binding event handler {0}", handler); | ||||
| 			bindings.get(handler.getEventType()) | ||||
| 				.add(handler); | ||||
| 			bindHandler( | ||||
| 				new ReflectiveEventHandler(listener, method, annotation, polymorphic, priority)); | ||||
| 			handlerBound = true; | ||||
| 		} | ||||
|  | ||||
| @@ -211,6 +252,86 @@ public final class EventBus { | ||||
| 				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 eventListener 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> eventListener) { | ||||
| 		registerListener(eventType, eventListener, 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 eventListener 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> eventListener, | ||||
| 		boolean polymorphic) { | ||||
| 		registerListener(eventType, eventListener, 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 eventListener 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> eventListener, int priority) { | ||||
| 		registerListener(eventType, eventListener, 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 eventListener 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> eventListener, | ||||
| 		boolean polymorphic, | ||||
| 		int priority) { | ||||
| 		Objects.requireNonNull(eventListener); | ||||
| 		if (registeredListeners.contains(eventListener)) | ||||
| 			throw new EventBusException(eventListener + " already registered!"); | ||||
| 		logger.log(Level.INFO, "Registering callback event listener {0}", | ||||
| 			eventListener.getClass().getName()); | ||||
|  | ||||
| 		registeredListeners.add(eventListener); | ||||
| 		bindHandler(new CallbackEventHandler(eventType, eventListener, 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. | ||||
| 	 * | ||||
| @@ -221,6 +342,7 @@ public final class EventBus { | ||||
| 		Objects.requireNonNull(listener); | ||||
| 		logger.log(Level.INFO, "Removing event listener {0}", listener.getClass().getName()); | ||||
|  | ||||
| 		// Remove bindings from binding map | ||||
| 		for (var binding : bindings.values()) { | ||||
| 			var it = binding.iterator(); | ||||
| 			while (it.hasNext()) { | ||||
| @@ -231,6 +353,8 @@ public final class EventBus { | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		// Remove the listener itself | ||||
| 		registeredListeners.remove(listener); | ||||
| 	} | ||||
|  | ||||
| @@ -245,6 +369,39 @@ public final class EventBus { | ||||
| 		registeredListeners.clear(); | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * Generates a string describing the event handlers that would be executed for a specific event | ||||
| 	 * type, in order and without actually executing them. | ||||
| 	 * | ||||
| 	 * @apiNote Using this method is only recommended for debugging purposes, as the output depends | ||||
| 	 *          on implementation internals which may be subject to change. | ||||
| 	 * @implNote Nested dispatches are not accounted for, as this would require actually executing | ||||
| 	 *           the handlers. | ||||
| 	 * @param eventType the event type to generate the execution order for | ||||
| 	 * @return a human-readable event handler list suitable for debugging purposes | ||||
| 	 * @since 1.2.0 | ||||
| 	 */ | ||||
| 	public String printExecutionOrder(Class<?> eventType) { | ||||
| 		var	handlers	= getHandlersFor(eventType); | ||||
| 		var	sj			= new StringJoiner("\n"); | ||||
|  | ||||
| 		// Output header line | ||||
| 		sj.add(String.format("Event handler execution order for %s (%d handler(s)):", eventType, | ||||
| 			handlers.size())); | ||||
| 		sj.add( | ||||
| 			"=========================================================================================="); | ||||
|  | ||||
| 		// Individual handlers | ||||
| 		for (var handler : handlers) | ||||
| 			sj.add(handler.toString()); | ||||
|  | ||||
| 		// Bottom line | ||||
| 		sj.add( | ||||
| 			"=========================================================================================="); | ||||
|  | ||||
| 		return sj.toString(); | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * Provides an unmodifiable view of the event listeners registered at this event bus. | ||||
| 	 * | ||||
|   | ||||
| @@ -1,14 +1,18 @@ | ||||
| package dev.kske.eventbus.core; | ||||
|  | ||||
| /** | ||||
|  * 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. | ||||
|  * This unchecked exception is specific to the event bus and can be thrown under the following | ||||
|  * circumstances: | ||||
|  * <ul> | ||||
|  * <li>An event handler throws an exception (which is stored as the cause)</li> | ||||
|  * <li>An event listener with an invalid event handler is registered</li> | ||||
|  * <li>{@link EventBus#cancel()} is invoked from outside an active dispatch thread</li> | ||||
|  * </ul> | ||||
|  * | ||||
|  * @author Kai S. K. Engelbart | ||||
|  * @since 0.0.1 | ||||
|  */ | ||||
| public class EventBusException extends RuntimeException { | ||||
| public final class EventBusException extends RuntimeException { | ||||
|  | ||||
| 	private static final long serialVersionUID = 1L; | ||||
|  | ||||
|   | ||||
| @@ -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 final class CallbackEventHandler implements EventHandler { | ||||
|  | ||||
| 	private final Class<?>			eventType; | ||||
| 	private final Consumer<Object>	callback; | ||||
| 	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 | ||||
| 	 * @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; | ||||
| 		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; | ||||
| 	} | ||||
| } | ||||
| @@ -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 also accepts subtypes of the event type | ||||
| 	 * @since 1.2.0 | ||||
| 	 * @see Polymorphic | ||||
| 	 */ | ||||
| 	boolean isPolymorphic(); | ||||
| } | ||||
| @@ -1,17 +1,18 @@ | ||||
| package dev.kske.eventbus.core; | ||||
| package dev.kske.eventbus.core.handler; | ||||
| 
 | ||||
| import java.lang.reflect.*; | ||||
| 
 | ||||
| import dev.kske.eventbus.core.*; | ||||
| 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 | ||||
|  * @since 0.0.1 | ||||
|  * @see EventBus | ||||
|  * @since 1.2.0 | ||||
|  */ | ||||
| final class EventHandler implements Comparable<EventHandler> { | ||||
| public final class ReflectiveEventHandler implements EventHandler { | ||||
| 
 | ||||
| 	private final Object	listener; | ||||
| 	private final Method	method; | ||||
| @@ -21,7 +22,7 @@ final class EventHandler implements Comparable<EventHandler> { | ||||
| 	private final int		priority; | ||||
| 
 | ||||
| 	/** | ||||
| 	 * Constructs an event handler. | ||||
| 	 * Constructs a reflective event handler. | ||||
| 	 * | ||||
| 	 * @param listener        the listener containing the handler | ||||
| 	 * @param method          the handler method | ||||
| @@ -30,10 +31,10 @@ final class EventHandler implements Comparable<EventHandler> { | ||||
| 	 * @param defPriority     the predefined priority (default or listener-level) | ||||
| 	 * @throws EventBusException if the method or the annotation do not comply with the | ||||
| 	 *                           specification | ||||
| 	 * @since 0.0.1 | ||||
| 	 * @since 1.2.0 | ||||
| 	 */ | ||||
| 	EventHandler(Object listener, Method method, Event annotation, boolean defPolymorphism, | ||||
| 		int defPriority) throws EventBusException { | ||||
| 	public ReflectiveEventHandler(Object listener, Method method, Event annotation, | ||||
| 		boolean defPolymorphism, int defPriority) throws EventBusException { | ||||
| 		this.listener	= listener; | ||||
| 		this.method		= method; | ||||
| 		useParameter	= annotation.value() == USE_PARAMETER.class; | ||||
| @@ -61,45 +62,13 @@ final class EventHandler implements Comparable<EventHandler> { | ||||
| 		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 | ||||
| 	public int compareTo(EventHandler other) { | ||||
| 		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 { | ||||
| 	public void execute(Object event) throws EventBusException, InvocationTargetException { | ||||
| 		try { | ||||
| 			if (useParameter) | ||||
| 				method.invoke(listener, event); | ||||
| 				method.invoke(getListener(), event); | ||||
| 			else | ||||
| 				method.invoke(listener); | ||||
| 				method.invoke(getListener()); | ||||
| 		} catch (IllegalArgumentException e) { | ||||
| 			throw new EventBusException("Event handler rejected target / argument!", e); | ||||
| 		} catch (IllegalAccessException e) { | ||||
| @@ -107,29 +76,30 @@ final class EventHandler implements Comparable<EventHandler> { | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	/** | ||||
| 	 * @return the listener containing this handler | ||||
| 	 * @since 0.0.1 | ||||
| 	 */ | ||||
| 	Object getListener() { return listener; } | ||||
| 	@Override | ||||
| 	public String toString() { | ||||
| 		return String.format( | ||||
| 			"ReflectiveEventHandler[eventType=%s, polymorphic=%b, priority=%d, method=%s, useParameter=%b]", | ||||
| 			eventType, polymorphic, priority, method, useParameter); | ||||
| 	} | ||||
| 
 | ||||
| 	/** | ||||
| 	 * @return the event type this handler listens for | ||||
| 	 * @since 0.0.3 | ||||
| 	 */ | ||||
| 	Class<?> getEventType() { return eventType; } | ||||
| 	@Override | ||||
| 	public Object getListener() { | ||||
| 		return listener; | ||||
| 	} | ||||
| 
 | ||||
| 	/** | ||||
| 	 * @return the priority of this handler | ||||
| 	 * @since 0.0.1 | ||||
| 	 * @see Priority | ||||
| 	 */ | ||||
| 	int getPriority() { return priority; } | ||||
| 	@Override | ||||
| 	public Class<?> getEventType() { | ||||
| 		return eventType; | ||||
| 	} | ||||
| 
 | ||||
| 	/** | ||||
| 	 * @return whether this handler is polymorphic | ||||
| 	 * @since 1.0.0 | ||||
| 	 * @see Polymorphic | ||||
| 	 */ | ||||
| 	boolean isPolymorphic() { return polymorphic; } | ||||
| 	@Override | ||||
| 	public int getPriority() { | ||||
| 		return priority; | ||||
| 	} | ||||
| 
 | ||||
| 	@Override | ||||
| 	public boolean isPolymorphic() { | ||||
| 		return polymorphic; | ||||
| 	} | ||||
| } | ||||
| @@ -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; | ||||
| @@ -11,7 +11,7 @@ import org.junit.jupiter.api.*; | ||||
|  * @author Leon Hofmeister | ||||
|  * @since 0.1.0 | ||||
|  */ | ||||
| class CancelTest { | ||||
| public class CancelTest { | ||||
|  | ||||
| 	EventBus	bus; | ||||
| 	int			hits; | ||||
| @@ -22,7 +22,7 @@ class CancelTest { | ||||
| 	 * @since 0.1.0 | ||||
| 	 */ | ||||
| 	@BeforeEach | ||||
| 	void registerListener() { | ||||
| 	public void registerListener() { | ||||
| 		bus = new EventBus(); | ||||
| 		bus.registerListener(this); | ||||
| 	} | ||||
| @@ -34,7 +34,7 @@ class CancelTest { | ||||
| 	 * @since 0.1.0 | ||||
| 	 */ | ||||
| 	@Test | ||||
| 	void testCancellation() { | ||||
| 	public void testCancellation() { | ||||
| 		bus.dispatch(new SimpleEvent()); | ||||
| 		assertEquals(1, hits); | ||||
| 	} | ||||
|   | ||||
| @@ -10,7 +10,7 @@ import org.junit.jupiter.api.Test; | ||||
|  * @author Kai S. K. Engelbart | ||||
|  * @since 1.1.0 | ||||
|  */ | ||||
| class DeadTest { | ||||
| public class DeadTest { | ||||
|  | ||||
| 	EventBus	bus		= new EventBus(); | ||||
| 	String		event	= "This event has no handler"; | ||||
| @@ -18,11 +18,11 @@ class DeadTest { | ||||
|  | ||||
| 	/** | ||||
| 	 * Tests dead event delivery. | ||||
| 	 *  | ||||
| 	 * | ||||
| 	 * @since 1.1.0 | ||||
| 	 */ | ||||
| 	@Test | ||||
| 	void testDeadEvent() { | ||||
| 	public void testDeadEvent() { | ||||
| 		bus.registerListener(this); | ||||
| 		bus.dispatch(event); | ||||
| 		assertTrue(deadEventHandled); | ||||
| @@ -32,11 +32,11 @@ class DeadTest { | ||||
| 	/** | ||||
| 	 * Tests how the event bus reacts to an unhandled dead event. This should not lead to an | ||||
| 	 * exception or an endless recursion and should be logged instead. | ||||
| 	 *  | ||||
| 	 * | ||||
| 	 * @since 1.1.0 | ||||
| 	 */ | ||||
| 	@Test | ||||
| 	void testUnhandledDeadEvent() { | ||||
| 	public void testUnhandledDeadEvent() { | ||||
| 		bus.dispatch(event); | ||||
| 	} | ||||
|  | ||||
|   | ||||
| @@ -12,7 +12,7 @@ import org.junit.jupiter.api.*; | ||||
|  */ | ||||
| @Polymorphic | ||||
| @Priority(150) | ||||
| class DispatchTest { | ||||
| public class DispatchTest { | ||||
|  | ||||
| 	EventBus	bus; | ||||
| 	static int	hits; | ||||
| @@ -23,9 +23,13 @@ class DispatchTest { | ||||
| 	 * @since 0.0.1 | ||||
| 	 */ | ||||
| 	@BeforeEach | ||||
| 	void registerListener() { | ||||
| 	public void registerListener() { | ||||
| 		bus = new EventBus(); | ||||
| 		bus.registerListener(this); | ||||
| 		bus.registerListener(SimpleEvent.class, e -> { | ||||
| 			++hits; | ||||
| 			assertEquals(4, hits); | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| @@ -35,11 +39,30 @@ class DispatchTest { | ||||
| 	 * @since 0.0.1 | ||||
| 	 */ | ||||
| 	@Test | ||||
| 	void testDispatch() { | ||||
| 	public void testDispatch() { | ||||
| 		bus.dispatch(new SimpleEventSub()); | ||||
| 		bus.dispatch(new SimpleEvent()); | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * Tests {@link EventBus#printExecutionOrder(Class)} based on the currently registered handlers. | ||||
| 	 * | ||||
| 	 * @since 1.2.0 | ||||
| 	 */ | ||||
| 	@Test | ||||
| 	public void testPrintExecutionOrder() { | ||||
| 		String executionOrder = bus.printExecutionOrder(SimpleEvent.class); | ||||
| 		System.out.println(executionOrder); | ||||
| 		assertEquals( | ||||
| 			"Event handler execution order for class dev.kske.eventbus.core.SimpleEvent (3 handler(s)):\n" | ||||
| 				+ "==========================================================================================\n" | ||||
| 				+ "ReflectiveEventHandler[eventType=class dev.kske.eventbus.core.SimpleEvent, polymorphic=true, priority=200, method=void dev.kske.eventbus.core.DispatchTest.onSimpleEventFirst(), useParameter=false]\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" | ||||
| 				+ "CallbackEventHandler[eventType=class dev.kske.eventbus.core.SimpleEvent, polymorphic=false, priority=100]\n" | ||||
| 				+ "==========================================================================================", | ||||
| 			executionOrder); | ||||
| 	} | ||||
|  | ||||
| 	@Event(SimpleEvent.class) | ||||
| 	@Priority(200) | ||||
| 	void onSimpleEventFirst() { | ||||
| @@ -53,12 +76,4 @@ class DispatchTest { | ||||
| 		++hits; | ||||
| 		assertEquals(3, hits); | ||||
| 	} | ||||
|  | ||||
| 	@Event | ||||
| 	@Polymorphic(false) | ||||
| 	@Priority(100) | ||||
| 	void onSimpleEventThird(SimpleEvent event) { | ||||
| 		++hits; | ||||
| 		assertEquals(4, hits); | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -6,11 +6,11 @@ import org.junit.jupiter.api.Test; | ||||
|  | ||||
| /** | ||||
|  * Tests the dispatching of an exception event if an event handler threw an exception. | ||||
|  *  | ||||
|  * | ||||
|  * @author Kai S. K. Engelbart | ||||
|  * @since 1.1.0 | ||||
|  */ | ||||
| class ExceptionTest { | ||||
| public class ExceptionTest { | ||||
|  | ||||
| 	EventBus			bus			= new EventBus(); | ||||
| 	String				event		= "This event will cause an exception"; | ||||
| @@ -19,11 +19,11 @@ class ExceptionTest { | ||||
|  | ||||
| 	/** | ||||
| 	 * Tests exception event delivery. | ||||
| 	 *  | ||||
| 	 * | ||||
| 	 * @since 1.1.0 | ||||
| 	 */ | ||||
| 	@Test | ||||
| 	void testExceptionEvent() { | ||||
| 	public void testExceptionEvent() { | ||||
| 		bus.registerListener(this); | ||||
| 		bus.registerListener(new ExceptionListener()); | ||||
| 		bus.dispatch(event); | ||||
| @@ -34,11 +34,11 @@ class ExceptionTest { | ||||
| 	/** | ||||
| 	 * Tests how the event bus reacts to an unhandled exception event. This should not lead to an | ||||
| 	 * exception or an endless recursion and should be logged instead. | ||||
| 	 *  | ||||
| 	 * | ||||
| 	 * @since 1.1.0 | ||||
| 	 */ | ||||
| 	@Test | ||||
| 	void testUnhandledExceptionEvent() { | ||||
| 	public void testUnhandledExceptionEvent() { | ||||
| 		bus.registerListener(this); | ||||
| 		bus.dispatch(event); | ||||
| 		bus.removeListener(this); | ||||
|   | ||||
| @@ -0,0 +1,73 @@ | ||||
| package dev.kske.eventbus.core; | ||||
|  | ||||
| import static org.junit.jupiter.api.Assertions.*; | ||||
|  | ||||
| import org.junit.jupiter.api.*; | ||||
|  | ||||
| /** | ||||
|  * Tests nested event dispatches. | ||||
|  * | ||||
|  * @author Kai S. K. Engelbart | ||||
|  * @since 1.2.0 | ||||
|  */ | ||||
| public class NestedTest { | ||||
|  | ||||
| 	EventBus	bus; | ||||
| 	boolean		nestedHit; | ||||
|  | ||||
| 	/** | ||||
| 	 * Constructs an event bus and registers this test instance as an event listener. | ||||
| 	 * | ||||
| 	 * @since 1.2.0 | ||||
| 	 */ | ||||
| 	@BeforeEach | ||||
| 	public void registerListener() { | ||||
| 		bus = new EventBus(); | ||||
| 		bus.registerListener(this); | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * Dispatches a simple event, which should in turn cause a string to be dispatched as a nested | ||||
| 	 * event. If the corresponding handler sets {@link #nestedHit} to {@code true}, the test is | ||||
| 	 * successful. | ||||
| 	 * | ||||
| 	 * @since 1.2.0 | ||||
| 	 */ | ||||
| 	@Test | ||||
| 	public void testNestedDispatch() { | ||||
| 		bus.dispatch(new SimpleEvent()); | ||||
| 		assertTrue(nestedHit); | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * Dispatches a string as a nested event and cancels the current dispatch afterwards. | ||||
| 	 * | ||||
| 	 * @since 1.2.0 | ||||
| 	 */ | ||||
| 	@Event(SimpleEvent.class) | ||||
| 	void onSimpleEvent() { | ||||
| 		bus.dispatch("Nested event"); | ||||
| 		bus.cancel(); | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * Sets {@link #nestedHit} to {@code true} indicating that nested dispatches work. | ||||
| 	 * | ||||
| 	 * @since 1.2.0 | ||||
| 	 */ | ||||
| 	@Event(String.class) | ||||
| 	void onString() { | ||||
| 		nestedHit = true; | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * Fails the test if an exception is caused during the dispatch. | ||||
| 	 * | ||||
| 	 * @param e the event containing the exception | ||||
| 	 * @since 1.2.0 | ||||
| 	 */ | ||||
| 	@Event | ||||
| 	void onException(ExceptionEvent e) { | ||||
| 		fail("Exception during dispatch", e.getCause()); | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										24
									
								
								pom.xml
									
									
									
									
									
								
							
							
						
						
									
										24
									
								
								pom.xml
									
									
									
									
									
								
							| @@ -9,7 +9,7 @@ | ||||
| 	<packaging>pom</packaging> | ||||
|  | ||||
| 	<name>Event Bus</name> | ||||
| 	<description>An event handling framework for Java utilizing annotations.</description> | ||||
| 	<description>An event handling library for Java utilizing annotations.</description> | ||||
| 	<url>https://git.kske.dev/kske/event-bus</url> | ||||
|  | ||||
| 	<modules> | ||||
| @@ -120,6 +120,28 @@ | ||||
| 						</goals> | ||||
| 					</execution> | ||||
| 				</executions> | ||||
|  | ||||
| 				<!-- Support JDK-style Javadoc tags --> | ||||
| 				<configuration> | ||||
| 					<tags> | ||||
| 						<tag> | ||||
| 							<name>apiNote</name> | ||||
| 							<placement>a</placement> | ||||
| 							<head>API Note:</head> | ||||
| 						</tag> | ||||
| 						<tag> | ||||
| 							<name>implSpec</name> | ||||
| 							<placement>a</placement> | ||||
| 							<head>Implementation Requirements:</head> | ||||
| 						</tag> | ||||
| 						<tag> | ||||
| 							<name>implNote</name> | ||||
| 							<placement>a</placement> | ||||
| 							<head>Implementation Note:</head> | ||||
| 						</tag> | ||||
| 					</tags> | ||||
| 				</configuration> | ||||
|  | ||||
| 			</plugin> | ||||
|  | ||||
| 			<!-- GPG sign JAR --> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user