Compare commits
	
		
			10 Commits
		
	
	
		
			0.0.4
			...
			ec73be9046
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| ec73be9046 | |||
| 659bd7888f | |||
| 8aefb43823 | |||
| 9d1707de5b | |||
| 1d2102d729 | |||
| dbb816c6cb | |||
| 603a838640 | |||
| b6b73d335a | |||
| 8cf51441ad | |||
| 001c0eea7e | 
							
								
								
									
										43
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										43
									
								
								README.md
									
									
									
									
									
								
							| @@ -60,6 +60,23 @@ To include subtypes for an event handler, use the `includeSubtypes` parameter as | ||||
| @Event(includeSubtypes = true) | ||||
| ``` | ||||
|  | ||||
| ## Event handler execution order | ||||
|  | ||||
| Sometimes when using multiple handlers for one event, it might be useful to know in which order they will be executed. | ||||
| Event Bus provides a mechanism to ensure the correct propagation of events: the `priority`. | ||||
|  | ||||
| Priority can be set on the `@Event` annotation like that: | ||||
| ```java | ||||
| @Event(priority=100) | ||||
| ``` | ||||
|  | ||||
| The default priority for events is `100`. | ||||
|  | ||||
| **Important:** | ||||
| Events are dispatched top-down, meaning the event handler with the highest priority will be executed first. | ||||
|  | ||||
| If no priority is set or multiple handlers have the same priority, the order of execution is undefined. | ||||
|  | ||||
| ## Parameter-less event handlers | ||||
|  | ||||
| In some cases an event handler is not interested in the dispatched event instance. | ||||
| @@ -74,6 +91,30 @@ private void onSimpleEvent() { | ||||
|  | ||||
| Make sure that you **do not** declare both a parameter and the `eventType` value of the annotation, as this would be ambiguous. | ||||
|  | ||||
| ## Event consumption | ||||
|  | ||||
| There are cases when it would be useful to stop event propagation after a certain condition has been fulfilled. | ||||
| Event Bus provides a mechanism to consume events: | ||||
|  | ||||
| ```java | ||||
| @Event(eventType = SimpleEvent.class, priority=1000) | ||||
| private void onSimpleEvent() { | ||||
| 	EventBus.getInstance().cancel(); | ||||
| } | ||||
|  | ||||
| @Event(eventType = SimpleEvent.class, priority=900) | ||||
| private void onSimpleEvent2() { | ||||
| 	System.out.println("Will not be printed!"); | ||||
| } | ||||
| ``` | ||||
|  | ||||
| In this example, the second method will not be executed as the event will no longer be forwarded. | ||||
| Any event handler with a lower priority than the one canceling it will not get executed. | ||||
|  | ||||
| **Important:** | ||||
| Please avoid cancelling events when (multiple) event handlers have the same priority as the one cancelling it: | ||||
| It is undefined whether those will be executed or not. | ||||
|  | ||||
| ## Installation | ||||
|  | ||||
| Event Bus is currently hosted at [kske.dev](https://kske.dev). | ||||
| @@ -91,7 +132,7 @@ To include it inside your project, just add the Maven repository and the depende | ||||
|     <dependency> | ||||
|         <groupId>dev.kske</groupId> | ||||
|         <artifactId>event-bus</artifactId> | ||||
|         <version>0.0.4</version> | ||||
|         <version>0.1.0</version> | ||||
|     </dependency> | ||||
| </dependencies> | ||||
| ``` | ||||
|   | ||||
							
								
								
									
										4
									
								
								pom.xml
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								pom.xml
									
									
									
									
									
								
							| @@ -5,7 +5,7 @@ | ||||
|  | ||||
| 	<groupId>dev.kske</groupId> | ||||
| 	<artifactId>event-bus</artifactId> | ||||
| 	<version>0.0.4</version> | ||||
| 	<version>0.1.0</version> | ||||
|  | ||||
| 	<name>Event Bus</name> | ||||
| 	<description>An event handling framework for Java utilizing annotations.</description> | ||||
| @@ -63,6 +63,7 @@ | ||||
| 			<plugin> | ||||
| 				<groupId>org.apache.maven.plugins</groupId> | ||||
| 				<artifactId>maven-source-plugin</artifactId> | ||||
| 				<version>3.2.1</version> | ||||
| 				<executions> | ||||
| 					<execution> | ||||
| 						<id>attach-sources</id> | ||||
| @@ -75,6 +76,7 @@ | ||||
| 			<plugin> | ||||
| 				<groupId>org.apache.maven.plugins</groupId> | ||||
| 				<artifactId>maven-javadoc-plugin</artifactId> | ||||
| 				<version>3.2.0</version> | ||||
| 				<executions> | ||||
| 					<execution> | ||||
| 						<id>attach-javadocs</id> | ||||
|   | ||||
| @@ -10,11 +10,12 @@ import java.lang.annotation.*; | ||||
|  * comply with the following specifications: | ||||
|  * <ul> | ||||
|  * <li>Declared inside a class that implements {@link EventListener}</li> | ||||
|  * <li>Specifying an event type by either</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> | ||||
|  * | ||||
| @@ -32,6 +33,7 @@ public @interface Event { | ||||
| 	 * <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; | ||||
| @@ -39,6 +41,7 @@ public @interface Event { | ||||
| 	/** | ||||
| 	 * 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; | ||||
| @@ -49,6 +52,7 @@ public @interface Event { | ||||
| 	 * <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; | ||||
|   | ||||
| @@ -1,5 +1,7 @@ | ||||
| package dev.kske.eventbus; | ||||
|  | ||||
| import java.lang.System.Logger; | ||||
| import java.lang.System.Logger.Level; | ||||
| import java.util.*; | ||||
| import java.util.concurrent.ConcurrentHashMap; | ||||
|  | ||||
| @@ -17,7 +19,19 @@ import java.util.concurrent.ConcurrentHashMap; | ||||
|  */ | ||||
| public final class EventBus { | ||||
|  | ||||
| 	private static EventBus singletonInstance; | ||||
| 	/** | ||||
| 	 * 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. | ||||
| @@ -26,14 +40,22 @@ public final class EventBus { | ||||
| 	 * @since 0.0.2 | ||||
| 	 */ | ||||
| 	public static EventBus getInstance() { | ||||
| 		if (singletonInstance == null) | ||||
| 			singletonInstance = new EventBus(); | ||||
| 		return singletonInstance; | ||||
| 		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 | ||||
| @@ -44,7 +66,25 @@ public final class EventBus { | ||||
| 	 */ | ||||
| 	public void dispatch(IEvent event) { | ||||
| 		Objects.requireNonNull(event); | ||||
| 		getHandlersFor(event.getClass()).forEach(handler -> handler.execute(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); | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| @@ -71,6 +111,20 @@ public final class EventBus { | ||||
| 		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. | ||||
| 	 * | ||||
| @@ -84,6 +138,8 @@ public final class EventBus { | ||||
| 		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()) { | ||||
| @@ -97,9 +153,18 @@ public final class EventBus { | ||||
| 			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() | ||||
| 			); | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| @@ -110,11 +175,17 @@ public final class EventBus { | ||||
| 	 */ | ||||
| 	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()) | ||||
| 				if (it.next().getListener() == listener) | ||||
| 			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); | ||||
| 	} | ||||
| @@ -125,6 +196,7 @@ public final class EventBus { | ||||
| 	 * @since 0.0.1 | ||||
| 	 */ | ||||
| 	public void clearListeners() { | ||||
| 		logger.log(Level.INFO, "Clearing event listeners"); | ||||
| 		bindings.clear(); | ||||
| 		registeredListeners.clear(); | ||||
| 	} | ||||
|   | ||||
| @@ -1,8 +1,9 @@ | ||||
| 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. | ||||
|  * 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 | ||||
| @@ -11,10 +12,21 @@ 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); | ||||
| 	} | ||||
|   | ||||
| @@ -77,6 +77,11 @@ final class EventHandler implements Comparable<EventHandler> { | ||||
| 		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. | ||||
| 	 * | ||||
|   | ||||
							
								
								
									
										52
									
								
								src/test/java/dev/kske/eventbus/CancelTest.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								src/test/java/dev/kske/eventbus/CancelTest.java
									
									
									
									
									
										Normal file
									
								
							| @@ -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; | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										58
									
								
								src/test/java/dev/kske/eventbus/DispatchTest.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								src/test/java/dev/kske/eventbus/DispatchTest.java
									
									
									
									
									
										Normal file
									
								
							| @@ -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); | ||||
| 	} | ||||
| } | ||||
| @@ -1,49 +0,0 @@ | ||||
| package dev.kske.eventbus; | ||||
|  | ||||
| import static org.junit.jupiter.api.Assertions.*; | ||||
|  | ||||
| import org.junit.jupiter.api.*; | ||||
|  | ||||
| /** | ||||
|  * Tests the of the event bus library. | ||||
|  * | ||||
|  * @author Kai S. K. Engelbart | ||||
|  * @since 0.0.1 | ||||
|  */ | ||||
| class EventBusTest implements EventListener { | ||||
|  | ||||
| 	int hits; | ||||
|  | ||||
| 	@BeforeEach | ||||
| 	public void registerListener() { | ||||
| 		EventBus.getInstance().registerListener(this); | ||||
| 	} | ||||
|  | ||||
| 	@Test | ||||
| 	void testDispatch() { | ||||
| 		EventBus.getInstance().dispatch(new SimpleEventSub()); | ||||
| 		EventBus.getInstance().dispatch(new SimpleEvent()); | ||||
| 	} | ||||
|  | ||||
| 	@Event( | ||||
| 		eventType = SimpleEvent.class, | ||||
| 		includeSubtypes = true, | ||||
| 		priority = 200 | ||||
| 	) | ||||
| 	private void onSimpleEventFirst() { | ||||
| 		++hits; | ||||
| 		assertTrue(hits == 1 || hits == 2); | ||||
| 	} | ||||
|  | ||||
| 	@Event(eventType = SimpleEvent.class, priority = 150) | ||||
| 	private void onSimpleEventSecond() { | ||||
| 		++hits; | ||||
| 		assertEquals(3, hits); | ||||
| 	} | ||||
|  | ||||
| 	@Event(priority = 50) | ||||
| 	private void onSimpleEventThird(SimpleEvent event) { | ||||
| 		++hits; | ||||
| 		assertEquals(4, hits); | ||||
| 	} | ||||
| } | ||||
		Reference in New Issue
	
	Block a user