Compare commits
	
		
			63 Commits
		
	
	
		
			0.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 | |||
| 0dcad7d178 | |||
| c0cda7341b   | |||
| b804243f4e | |||
| 0aef1c299b | |||
| 51f10c4144   | |||
| f74b953db8 | |||
| 52719d22d4   | |||
| 122106bf39 | |||
| 7357198d45 | |||
| 32dfe64c0f | |||
| 2ec0a82a96 | |||
| 6c74af608c | |||
| d9ddc0e1a9   | |||
| 7c3cd017de | |||
| 6a2cad4ae5 | |||
| 0f9b64be48   | |||
| b2fe3a9d6c | |||
| 9379e6bb94   | |||
| 0036dc4829 | |||
| 8a30493c52 | |||
| b56f08e441 | |||
| 4a5b94a9b7 | |||
| ff35e7f37d | |||
| 1dd9e05c38 | |||
| 39c51c8953   | |||
| 002180ed3b | |||
| 603fe80df6   | |||
| cd2e7ad023 | |||
| 9b1c708514 | |||
| 3a6ebe9a19 | |||
| e040f6ab1b | |||
| ebc11555f6 | |||
| 955e2d82b4   | |||
| ab01845178 | |||
| 023acb9172 | |||
| fd255d65cc | |||
| 9701e862df | |||
| dcc578076a | |||
| 883efed342 | |||
| 273531e352   | |||
| 8b1e3a8c4a   | |||
| d098b83d85   | |||
| cc266ca408 | 
							
								
								
									
										10
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										10
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -1,2 +1,8 @@ | ||||
| /target/ | ||||
| /.settings/ | ||||
| # Maven build directories | ||||
| target/ | ||||
|  | ||||
| # Dependency reduced POM from Maven Shade Plugin | ||||
| dependency-reduced-pom.xml | ||||
|  | ||||
| # Eclipse settings directories | ||||
| .settings/ | ||||
|   | ||||
							
								
								
									
										6
									
								
								.project
									
									
									
									
									
								
							
							
						
						
									
										6
									
								
								.project
									
									
									
									
									
								
							| @@ -5,11 +5,6 @@ | ||||
| 	<projects> | ||||
| 	</projects> | ||||
| 	<buildSpec> | ||||
| 		<buildCommand> | ||||
| 			<name>org.eclipse.jdt.core.javabuilder</name> | ||||
| 			<arguments> | ||||
| 			</arguments> | ||||
| 		</buildCommand> | ||||
| 		<buildCommand> | ||||
| 			<name>org.eclipse.m2e.core.maven2Builder</name> | ||||
| 			<arguments> | ||||
| @@ -17,7 +12,6 @@ | ||||
| 		</buildCommand> | ||||
| 	</buildSpec> | ||||
| 	<natures> | ||||
| 		<nature>org.eclipse.jdt.core.javanature</nature> | ||||
| 		<nature>org.eclipse.m2e.core.maven2Nature</nature> | ||||
| 	</natures> | ||||
| </projectDescription> | ||||
|   | ||||
							
								
								
									
										5
									
								
								CODE_OF_CONDUCT.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								CODE_OF_CONDUCT.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| # Contributor Code of Conduct | ||||
|  | ||||
| This project adheres to No Code of Conduct.  We are all adults.  We accept anyone's contributions.  Nothing else matters. | ||||
|  | ||||
| For more information please visit the [No Code of Conduct](https://nocodeofconduct.com) homepage. | ||||
							
								
								
									
										26
									
								
								LICENSE
									
									
									
									
									
								
							
							
						
						
									
										26
									
								
								LICENSE
									
									
									
									
									
								
							| @@ -1,19 +1,13 @@ | ||||
| MIT License Copyright (c) 2020 Kai S. K. Engelbart | ||||
| Copyright 2021 Kai S. K. Engelbart | ||||
|  | ||||
| Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
| of this software and associated documentation files (the "Software"), to deal | ||||
| in the Software without restriction, including without limitation the rights | ||||
| to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||||
| copies of the Software, and to permit persons to whom the Software is furnished | ||||
| to do so, subject to the following conditions: | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
|  | ||||
| The above copyright notice and this permission notice (including the next | ||||
| paragraph) shall be included in all copies or substantial portions of the | ||||
| Software. | ||||
|     https://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||||
| IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS | ||||
| FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS | ||||
| OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, | ||||
| WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF | ||||
| OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
|   | ||||
							
								
								
									
										172
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										172
									
								
								README.md
									
									
									
									
									
								
							| @@ -3,34 +3,31 @@ | ||||
| ## Introduction | ||||
|  | ||||
| This library allows passing events between different objects without them having a direct reference to each other. | ||||
| Any class can be made an event by implementing the `IEvent` interface. | ||||
| Any object can serve as an event. | ||||
|  | ||||
| Using an instance of the `EventBus` class, an instant of the event class can be dispatched. | ||||
| Using an instance of the `EventBus` class, an instance of the event class can be dispatched. | ||||
| This means that it will be forwarded to all listeners registered for it at the event bus. | ||||
|  | ||||
| In addition, a singleton instance of the event bus is provided by the `EventBus#getInstance()` method. | ||||
|  | ||||
| To listen to events, register event handling methods using the `Event` annotation. | ||||
| For this to work, the method must have a return type of `void` and declare a single parameter of the desired event type. | ||||
| For this to work, the method must declare a single parameter of the desired event type. | ||||
| Alternatively, a parameter-less event handler can be declared as shown [below](#parameter-less-event-handlers). | ||||
| Additionally, the class containing the method must implement the `EventListener` interface. | ||||
|  | ||||
| ## A Simple Example | ||||
|  | ||||
| Lets look at a simple example: we declare the empty class `SimpleEvent` that implements `IEvent` and can thus be used as an event. | ||||
| Lets look at a simple example: we declare the empty class `SimpleEvent` whose objects can be used as events. | ||||
|  | ||||
| ```java | ||||
| import dev.kske.eventbus.IEvent; | ||||
|  | ||||
| public class SimpleEvent implements IEvent {} | ||||
| public class SimpleEvent {} | ||||
| ``` | ||||
|  | ||||
| Next, an event listener for the `SimpleEvent` is declared: | ||||
|  | ||||
| ```java | ||||
| import dev.kske.eventbus.*; | ||||
| import dev.kske.eventbus.core.*; | ||||
|  | ||||
| public class SimpleEventListener implements EventListener { | ||||
| public class SimpleEventListener { | ||||
|  | ||||
|     public SimpleEventListener() { | ||||
|  | ||||
| @@ -48,100 +45,171 @@ public class SimpleEventListener implements EventListener { | ||||
| } | ||||
| ``` | ||||
|  | ||||
| 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 | ||||
|     @Event | ||||
|     private static void onSimpleEvent(SimpleEvent event) ... | ||||
| @Event | ||||
| private static void onSimpleEvent(SimpleEvent event) { ... } | ||||
| ``` | ||||
|  | ||||
| is technically possible, however you would still have to create an instance of the event listener to register it at an event bus. | ||||
|  | ||||
| ## Event handlers for subtypes | ||||
| ## Polymorphic Event Handlers | ||||
|  | ||||
| On certain occasions its practical for an event handler to accept both events of the specified type, as well as subclasses of that event. | ||||
| To include subtypes for an event handler, use the `includeSubtypes` parameter as follows: | ||||
| On certain occasions it's practical for an event handler to accept both events of the specified type, as well as subclasses of that event. | ||||
| To include subtypes for an event handler, use the `@Polymorphic` annotation in addition to `@Event`: | ||||
|  | ||||
| ```java | ||||
| @Event(includeSubtypes = true) | ||||
| @Event | ||||
| @Polymorphic | ||||
| private void onSimpleEvent(SimpleEvent event) { ... } | ||||
| ``` | ||||
|  | ||||
| ## Event handler execution order | ||||
| ## 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`. | ||||
| Sometimes when using multiple handlers for one event, it might be useful to define in which order they will be executed. | ||||
| Event Bus assigns a priority to every handler, which is `100` by default, but can be explicitly set using the `@Priority` annotation in addition to `@Event`: | ||||
|  | ||||
| Priority can be set on the `@Event` annotation like that: | ||||
| ```java | ||||
| @Event(priority=100) | ||||
| @Event | ||||
| @Priority(250) | ||||
| private void onSimpleEvent(SimpleEvent event) { ... } | ||||
| ``` | ||||
|  | ||||
| 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. | ||||
| Events are dispatched to handlers in descending order of their priority. | ||||
| The execution order is undefined for handlers with the same priority. | ||||
|  | ||||
| If no priority is set or multiple handlers have the same priority, the order of execution is undefined. | ||||
|  | ||||
| ## Parameter-less event handlers | ||||
| ## Parameter-Less Event Handlers | ||||
|  | ||||
| In some cases an event handler is not interested in the dispatched event instance. | ||||
| To avoid declaring a useless parameter just to specify the event type of the handler, there is an alternative: | ||||
|  | ||||
| ```java | ||||
| @Event(eventType = SimpleEvent.class) | ||||
| @Event(SimpleEvent.class) | ||||
| private void onSimpleEvent() { | ||||
| 	System.out.println("SimpleEvent received!"); | ||||
| } | ||||
| ``` | ||||
|  | ||||
| Make sure that you **do not** declare both a parameter and the `eventType` value of the annotation, as this would be ambiguous. | ||||
| Make sure that you **do not** both declare a parameter and specify the event type in the annotation, as this would be ambiguous. | ||||
|  | ||||
| ## Event consumption | ||||
| ## Listener-Level Properties | ||||
|  | ||||
| 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: | ||||
| When defining a dedicated event listener that, for example, performs pre- or post-processing, all event handlers will probably have the same non-standard priority. | ||||
| Instead of defining that priority for each handler, it can be defined at the listener level by annotating the listener itself. | ||||
|  | ||||
| The same applies to polymorphism. | ||||
|  | ||||
| ## Event Consumption | ||||
|  | ||||
| In some cases it might be useful to stop the propagation of an event. | ||||
| Event Bus makes this possible with event consumption: | ||||
|  | ||||
| ```java | ||||
| @Event(eventType = SimpleEvent.class, priority=1000) | ||||
| @Event(SimpleEvent.class) | ||||
| @Priority(100) | ||||
| private void onSimpleEvent() { | ||||
| 	EventBus.getInstance().cancel(); | ||||
| } | ||||
|  | ||||
| @Event(eventType = SimpleEvent.class, priority=900) | ||||
| @Event(SimpleEvent.class) | ||||
| @Priority(50) | ||||
| 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. | ||||
| In this example, the second method will not be executed as it has a lower priority and the event will not be propagated after consumption. | ||||
| This applies to all event handlers that would have been executed after the one consuming the event. | ||||
|  | ||||
| **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. | ||||
| Avoid cancelling events while using multiple event handlers with the same priority. | ||||
| As event handlers are ordered by priority, it is not defined which of them will be executed after the event has been consumed. | ||||
|  | ||||
| ## System Events | ||||
|  | ||||
| To accommodate for special circumstances in an event distribution, system events have been introduced. | ||||
| At the moment, there are two system events, which are explained in this section. | ||||
|  | ||||
| ### Detecting Unhandled Events | ||||
|  | ||||
| When an event is dispatched but not delivered to any handler, a dead event is dispatched that wraps the original event. | ||||
| You can declare a dead event handler to respond to this situation: | ||||
|  | ||||
| ```java | ||||
| private void onDeadEvent(DeadEvent deadEvent) { ... } | ||||
| ``` | ||||
|  | ||||
| ### Detecting Exceptions Thrown by Event Handlers | ||||
|  | ||||
| When an event handler throws an exception, an exception event is dispatched that wraps the original event. | ||||
| A exception handler is declared as follows: | ||||
|  | ||||
| ```java | ||||
| private void onExceptionEvent(ExceptionEvent ExceptionEvent) { ... } | ||||
| ``` | ||||
|  | ||||
| Both system events reference the event bus that caused them and a warning is logged if they are unhandled. | ||||
|  | ||||
| ### What About Endless Recursion Caused By Dead Events and Exception Events? | ||||
|  | ||||
| As one might imagine, an unhandled dead event would theoretically lead to an endless recursion. | ||||
| The same applies when an exception event handler throws an exception. | ||||
|  | ||||
| To avoid this, system events never cause system events and instead just issue a warning to the logger. | ||||
|  | ||||
| ## Installation | ||||
|  | ||||
| Event Bus is currently hosted at [kske.dev](https://kske.dev). | ||||
| To include it inside your project, just add the Maven repository and the dependency to your `pom.xml`: | ||||
| Event Bus is available in Maven Central. | ||||
| To include it inside your project, just add the following dependency to your `pom.xml`: | ||||
|  | ||||
| ```xml | ||||
| <repositories> | ||||
| 	<repository> | ||||
| 		<id>kske-repo</id> | ||||
| 		<url>https://kske.dev/maven-repo</url> | ||||
| 	</repository> | ||||
| </repositories> | ||||
|  | ||||
| <dependencies> | ||||
|     <dependency> | ||||
|         <groupId>dev.kske</groupId> | ||||
|         <artifactId>event-bus</artifactId> | ||||
|         <version>0.1.0</version> | ||||
|         <artifactId>event-bus-core</artifactId> | ||||
|         <version>1.1.0</version> | ||||
|     </dependency> | ||||
| </dependencies> | ||||
| ``` | ||||
|  | ||||
| Then, require the Event Bus Core module in your `module-info.java`: | ||||
|  | ||||
| ```java | ||||
| requires dev.kske.eventbus.core; | ||||
| ``` | ||||
|  | ||||
| If you intend to use event handlers that are inaccessible to Event Bus by means of Java language access control, make sure to allow reflective access from your module: | ||||
|  | ||||
| ```java | ||||
| opens my.module to dev.kske.eventbus.core; | ||||
| ``` | ||||
|  | ||||
| ## Compile-Time Error Checking with Event Bus Proc | ||||
|  | ||||
| To assist you with writing event listeners, the Event Bus Proc (Annotation Processor) module enforces correct usage of the `@Event` annotation during compile time. | ||||
| This reduces difficult-to-debug bugs that occur during runtime to compile-time errors which can be easily fixed. | ||||
|  | ||||
| The event annotation processor detects invalid event handlers and event type issues with more to come in future versions. | ||||
|  | ||||
| When using Maven, it can be registered using the Maven Compiler Plugin: | ||||
|  | ||||
| ```xml | ||||
| <plugin> | ||||
|     <groupId>org.apache.maven.plugins</groupId> | ||||
|     <artifactId>maven-compiler-plugin</artifactId> | ||||
|     <version>3.8.1</version> | ||||
|     <configuration> | ||||
|         <annotationProcessorPaths> | ||||
|             <annotationProcessorPath> | ||||
|                 <groupId>dev.kske</groupId> | ||||
|                 <artifactId>event-bus-proc</artifactId> | ||||
|                 <version>1.1.0</version> | ||||
|             </annotationProcessorPath> | ||||
|         </annotationProcessorPaths> | ||||
|     </configuration> | ||||
| </plugin> | ||||
| ``` | ||||
|  | ||||
| Alternatively, a JAR file containing the processor is offered with each release for the use within IDEs and environments without Maven support. | ||||
|   | ||||
							
								
								
									
										23
									
								
								event-bus-core/.project
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								event-bus-core/.project
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <projectDescription> | ||||
| 	<name>event-bus-core</name> | ||||
| 	<comment></comment> | ||||
| 	<projects> | ||||
| 	</projects> | ||||
| 	<buildSpec> | ||||
| 		<buildCommand> | ||||
| 			<name>org.eclipse.jdt.core.javabuilder</name> | ||||
| 			<arguments> | ||||
| 			</arguments> | ||||
| 		</buildCommand> | ||||
| 		<buildCommand> | ||||
| 			<name>org.eclipse.m2e.core.maven2Builder</name> | ||||
| 			<arguments> | ||||
| 			</arguments> | ||||
| 		</buildCommand> | ||||
| 	</buildSpec> | ||||
| 	<natures> | ||||
| 		<nature>org.eclipse.jdt.core.javanature</nature> | ||||
| 		<nature>org.eclipse.m2e.core.maven2Nature</nature> | ||||
| 	</natures> | ||||
| </projectDescription> | ||||
							
								
								
									
										39
									
								
								event-bus-core/pom.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								event-bus-core/pom.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,39 @@ | ||||
| <project xmlns="http://maven.apache.org/POM/4.0.0" | ||||
| 	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||||
| 	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> | ||||
| 	<modelVersion>4.0.0</modelVersion> | ||||
|  | ||||
| 	<artifactId>event-bus-core</artifactId> | ||||
| 	<name>Event Bus Core</name> | ||||
|  | ||||
| 	<parent> | ||||
| 		<groupId>dev.kske</groupId> | ||||
| 		<artifactId>event-bus</artifactId> | ||||
| 		<version>1.1.0</version> | ||||
| 	</parent> | ||||
|  | ||||
| 	<dependencies> | ||||
| 		<dependency> | ||||
| 			<groupId>org.junit.jupiter</groupId> | ||||
| 			<artifactId>junit-jupiter-api</artifactId> | ||||
| 			<version>5.8.1</version> | ||||
| 			<scope>test</scope> | ||||
| 		</dependency> | ||||
| 	</dependencies> | ||||
|  | ||||
| 	<build> | ||||
|  | ||||
| 		<!-- 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> | ||||
| @@ -0,0 +1,37 @@ | ||||
| package dev.kske.eventbus.core; | ||||
|  | ||||
| /** | ||||
|  * Wraps an event that was dispatched but for which no handler has been bound. | ||||
|  * <p> | ||||
|  * Handling dead events is useful as it can identify a poorly configured event distribution. | ||||
|  * | ||||
|  * @author Kai S. K. Engelbart | ||||
|  * @since 1.1.0 | ||||
|  */ | ||||
| public final class DeadEvent { | ||||
|  | ||||
| 	private final EventBus	eventBus; | ||||
| 	private final Object	event; | ||||
|  | ||||
| 	DeadEvent(EventBus eventBus, Object event) { | ||||
| 		this.eventBus	= eventBus; | ||||
| 		this.event		= event; | ||||
| 	} | ||||
|  | ||||
| 	@Override | ||||
| 	public String toString() { | ||||
| 		return String.format("DeadEvent[eventBus=%s, event=%s]", eventBus, event); | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * @return the event bus that dispatched this event | ||||
| 	 * @since 1.1.0 | ||||
| 	 */ | ||||
| 	public EventBus getEventBus() { return eventBus; } | ||||
|  | ||||
| 	/** | ||||
| 	 * @return the event that could not be delivered | ||||
| 	 * @since 1.1.0 | ||||
| 	 */ | ||||
| 	public Object getEvent() { return event; } | ||||
| } | ||||
| @@ -0,0 +1,42 @@ | ||||
| package dev.kske.eventbus.core; | ||||
|  | ||||
| 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. | ||||
|  * <p> | ||||
|  * To be successfully used as such, the method has to specify the event type by either declaring one | ||||
|  * parameter of that type or setting the annotation value to the corresponding class. | ||||
|  * | ||||
|  * @author Kai S. K. Engelbart | ||||
|  * @since 0.0.1 | ||||
|  * @see Polymorphic | ||||
|  * @see Priority | ||||
|  */ | ||||
| @Documented | ||||
| @Retention(RUNTIME) | ||||
| @Target(METHOD) | ||||
| public @interface Event { | ||||
|  | ||||
| 	/** | ||||
| 	 * 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. | ||||
| 	 * | ||||
| 	 * @return the event type accepted by the handler | ||||
| 	 * @since 1.0.0 | ||||
| 	 */ | ||||
| 	Class<?> value() 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 {} | ||||
| } | ||||
| @@ -0,0 +1,414 @@ | ||||
| package dev.kske.eventbus.core; | ||||
|  | ||||
| import java.lang.System.Logger; | ||||
| 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. | ||||
|  * <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 { | ||||
|  | ||||
| 		/** | ||||
| 		 * 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; | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * The priority assigned to every event handler without an explicitly defined priority. | ||||
| 	 * | ||||
| 	 * @since 1.1.0 | ||||
| 	 * @see Priority | ||||
| 	 */ | ||||
| 	public static final int DEFAULT_PRIORITY = 100; | ||||
|  | ||||
| 	private static final EventBus singletonInstance = new EventBus(); | ||||
|  | ||||
| 	private static final Logger logger = System.getLogger(EventBus.class.getName()); | ||||
|  | ||||
| 	/** | ||||
| 	 * Compares event handlers based on priority, but uses hash codes for equal priorities. | ||||
| 	 * | ||||
| 	 * @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() { | ||||
| 		return singletonInstance; | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * 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); | ||||
|  | ||||
| 	/** | ||||
| 	 * Dispatches an event to all event handlers registered for it in descending order of their | ||||
| 	 * priority. | ||||
| 	 * | ||||
| 	 * @param event the event to dispatch | ||||
| 	 * @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); | ||||
|  | ||||
| 		// Look up dispatch state | ||||
| 		var state = dispatchState.get(); | ||||
|  | ||||
| 		// 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) { | ||||
| 					logger.log(Level.INFO, "Cancelled dispatching event {0}", event); | ||||
| 					state.isCancelled = false; | ||||
| 					break; | ||||
| 				} else { | ||||
| 					try { | ||||
| 						handlers.next().execute(event); | ||||
| 					} catch (InvocationTargetException e) { | ||||
| 						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 | ||||
| 							dispatch(new ExceptionEvent(this, event, e.getCause())); | ||||
| 					} | ||||
| 				} | ||||
| 		} else if (event instanceof DeadEvent || event instanceof ExceptionEvent) { | ||||
|  | ||||
| 			// Warn about the dead event not being handled | ||||
| 			logger.log(Level.WARNING, "{0} not handled", event); | ||||
| 		} else { | ||||
|  | ||||
| 			// Dispatch dead event | ||||
| 			dispatch(new DeadEvent(this, event)); | ||||
| 		} | ||||
|  | ||||
| 		// Decrement nesting count (becomes 0 when all dispatches on the thread are finished) | ||||
| 		--state.nestingCount; | ||||
|  | ||||
| 		logger.log(Level.DEBUG, "Finished dispatching event {0}", event); | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * 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 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 NavigableSet<EventHandler> getHandlersFor(Class<?> eventType) { | ||||
|  | ||||
| 		// Get handlers defined for the event class | ||||
| 		TreeSet<EventHandler> handlers = | ||||
| 			bindings.getOrDefault(eventType, new TreeSet<>(byPriority)); | ||||
|  | ||||
| 		// Get polymorphic handlers | ||||
| 		for (var binding : bindings.entrySet()) | ||||
| 			if (binding.getKey().isAssignableFrom(eventType)) | ||||
| 				for (var handler : binding.getValue()) | ||||
| 					if (handler.isPolymorphic()) | ||||
| 						handlers.add(handler); | ||||
|  | ||||
| 		return 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.nestingCount > 0 && !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 | ||||
| 	 * @throws NullPointerException if the specified listener is {@code null} | ||||
| 	 * @since 0.0.1 | ||||
| 	 * @see Event | ||||
| 	 */ | ||||
| 	public void registerListener(Object 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; | ||||
|  | ||||
| 		// Predefined handler polymorphism | ||||
| 		boolean polymorphic = false; | ||||
| 		if (listener.getClass().isAnnotationPresent(Polymorphic.class)) | ||||
| 			polymorphic = listener.getClass().getAnnotation(Polymorphic.class).value(); | ||||
|  | ||||
| 		// Predefined handler priority | ||||
| 		int priority = DEFAULT_PRIORITY; | ||||
| 		if (listener.getClass().isAnnotationPresent(Priority.class)) | ||||
| 			priority = listener.getClass().getAnnotation(Priority.class).value(); | ||||
|  | ||||
| 		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 | ||||
| 			bindHandler( | ||||
| 				new ReflectiveEventHandler(listener, method, annotation, polymorphic, priority)); | ||||
| 			handlerBound = true; | ||||
| 		} | ||||
|  | ||||
| 		if (!handlerBound) | ||||
| 			logger.log( | ||||
| 				Level.WARNING, | ||||
| 				"No event handlers bound for event listener {0}", | ||||
| 				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. | ||||
| 	 * | ||||
| 	 * @param listener the listener to remove | ||||
| 	 * @since 0.0.1 | ||||
| 	 */ | ||||
| 	public void removeListener(Object listener) { | ||||
| 		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()) { | ||||
| 				var handler = it.next(); | ||||
| 				if (handler.getListener() == listener) { | ||||
| 					logger.log(Level.DEBUG, "Unbinding event handler {0}", handler); | ||||
| 					it.remove(); | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		// Remove the listener itself | ||||
| 		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(); | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * 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. | ||||
| 	 * | ||||
| 	 * @return all registered event listeners | ||||
| 	 * @since 0.0.1 | ||||
| 	 */ | ||||
| 	public Set<Object> getRegisteredListeners() { | ||||
| 		return Collections.unmodifiableSet(registeredListeners); | ||||
| 	} | ||||
| } | ||||
| @@ -1,14 +1,18 @@ | ||||
| package dev.kske.eventbus; | ||||
| 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,47 @@ | ||||
| package dev.kske.eventbus.core; | ||||
|  | ||||
| /** | ||||
|  * Wraps an event that was dispatched but caused an exception in one of its handlers. | ||||
|  * <p> | ||||
|  * Handling exception events is useful as it allows the creation of a centralized exception handling | ||||
|  * mechanism for unexpected exceptions. | ||||
|  *  | ||||
|  * @author Kai S. K. Engelbart | ||||
|  * @since 1.1.0 | ||||
|  */ | ||||
| public final class ExceptionEvent { | ||||
|  | ||||
| 	private final EventBus	eventBus; | ||||
| 	private final Object	event; | ||||
| 	private final Throwable	cause; | ||||
|  | ||||
| 	ExceptionEvent(EventBus eventBus, Object event, Throwable cause) { | ||||
| 		this.eventBus	= eventBus; | ||||
| 		this.event		= event; | ||||
| 		this.cause		= cause; | ||||
| 	} | ||||
|  | ||||
| 	@Override | ||||
| 	public String toString() { | ||||
| 		return String.format("ExceptionEvent[eventBus=%s, event=%s, cause=%s]", eventBus, event, | ||||
| 			cause); | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * @return the event bus that dispatched this event | ||||
| 	 * @since 1.1.0 | ||||
| 	 */ | ||||
| 	public EventBus getEventBus() { return eventBus; } | ||||
|  | ||||
| 	/** | ||||
| 	 * @return the event that could not be handled because of an exception | ||||
| 	 * @since 1.1.0 | ||||
| 	 */ | ||||
| 	public Object getEvent() { return event; } | ||||
|  | ||||
| 	/** | ||||
| 	 * @return the exception that was thrown while handling the event | ||||
| 	 * @since 1.1.0 | ||||
| 	 */ | ||||
| 	public Throwable getCause() { return cause; } | ||||
| } | ||||
| @@ -0,0 +1,30 @@ | ||||
| package dev.kske.eventbus.core; | ||||
|  | ||||
| import static java.lang.annotation.ElementType.*; | ||||
| import static java.lang.annotation.RetentionPolicy.RUNTIME; | ||||
|  | ||||
| import java.lang.annotation.*; | ||||
|  | ||||
| /** | ||||
|  * Allows an event handler to receive events that are subtypes of the declared event type. | ||||
|  * <p> | ||||
|  * When used on a type, the value applies to all event handlers declared within that type that don't | ||||
|  * define a value on their own. | ||||
|  * <p> | ||||
|  * This is useful when defining an event handler for an interface or an abstract class. | ||||
|  * | ||||
|  * @author Kai S. K. Engelbart | ||||
|  * @since 1.0.0 | ||||
|  * @see Event | ||||
|  */ | ||||
| @Documented | ||||
| @Retention(RUNTIME) | ||||
| @Target({ METHOD, TYPE }) | ||||
| public @interface Polymorphic { | ||||
|  | ||||
| 	/** | ||||
| 	 * @return whether the event handler is polymorphic | ||||
| 	 * @since 1.1.0 | ||||
| 	 */ | ||||
| 	boolean value() default true; | ||||
| } | ||||
| @@ -0,0 +1,33 @@ | ||||
| package dev.kske.eventbus.core; | ||||
|  | ||||
| import static java.lang.annotation.ElementType.*; | ||||
| import static java.lang.annotation.RetentionPolicy.RUNTIME; | ||||
|  | ||||
| import java.lang.annotation.*; | ||||
|  | ||||
| /** | ||||
|  * Defines the priority of an event handler. Handlers are executed in descending order of their | ||||
|  * priority. | ||||
|  * <p> | ||||
|  * When used on a type, the value applies to all event handlers declared within that type that don't | ||||
|  * define a value on their own. | ||||
|  * <p> | ||||
|  * Handlers without this annotation have the default priority of 100. | ||||
|  * <p> | ||||
|  * The execution order of handlers with the same priority is undefined. | ||||
|  * | ||||
|  * @author Kai S. K. Engelbart | ||||
|  * @since 1.0.0 | ||||
|  * @see Event | ||||
|  */ | ||||
| @Documented | ||||
| @Retention(RUNTIME) | ||||
| @Target({ METHOD, TYPE }) | ||||
| public @interface Priority { | ||||
|  | ||||
| 	/** | ||||
| 	 * @return the priority of the event handler | ||||
| 	 * @since 1.0.0 | ||||
| 	 */ | ||||
| 	int value(); | ||||
| } | ||||
| @@ -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(); | ||||
| } | ||||
| @@ -0,0 +1,105 @@ | ||||
| package dev.kske.eventbus.core.handler; | ||||
|  | ||||
| import java.lang.reflect.*; | ||||
|  | ||||
| import dev.kske.eventbus.core.*; | ||||
| import dev.kske.eventbus.core.Event.USE_PARAMETER; | ||||
|  | ||||
| /** | ||||
|  * An event handler wrapping a method annotated with {@link Event} and executing it using | ||||
|  * reflection. | ||||
|  * | ||||
|  * @author Kai S. K. Engelbart | ||||
|  * @since 1.2.0 | ||||
|  */ | ||||
| public final class ReflectiveEventHandler implements EventHandler { | ||||
|  | ||||
| 	private final Object	listener; | ||||
| 	private final Method	method; | ||||
| 	private final Class<?>	eventType; | ||||
| 	private final boolean	useParameter; | ||||
| 	private final boolean	polymorphic; | ||||
| 	private final int		priority; | ||||
|  | ||||
| 	/** | ||||
| 	 * Constructs a reflective event handler. | ||||
| 	 * | ||||
| 	 * @param listener        the listener containing the handler | ||||
| 	 * @param method          the handler method | ||||
| 	 * @param annotation      the event annotation | ||||
| 	 * @param defPolymorphism the predefined polymorphism (default or listener-level) | ||||
| 	 * @param defPriority     the predefined priority (default or listener-level) | ||||
| 	 * @throws EventBusException if the method or the annotation do not comply with the | ||||
| 	 *                           specification | ||||
| 	 * @since 1.2.0 | ||||
| 	 */ | ||||
| 	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; | ||||
|  | ||||
| 		// Check handler signature | ||||
| 		if (method.getParameterCount() == 0 && useParameter) | ||||
| 			throw new EventBusException(method + " does not define an event type!"); | ||||
|  | ||||
| 		if (method.getParameterCount() == 1 && !useParameter) | ||||
| 			throw new EventBusException(method + " defines an ambiguous event type!"); | ||||
|  | ||||
| 		if (method.getParameterCount() > 1) | ||||
| 			throw new EventBusException(method + " defines more than one parameter!"); | ||||
|  | ||||
| 		// Determine handler properties | ||||
| 		eventType	= useParameter ? method.getParameterTypes()[0] : annotation.value(); | ||||
| 		polymorphic	= method.isAnnotationPresent(Polymorphic.class) | ||||
| 			? method.getAnnotation(Polymorphic.class).value() | ||||
| 			: defPolymorphism; | ||||
| 		priority	= method.isAnnotationPresent(Priority.class) | ||||
| 			? method.getAnnotation(Priority.class).value() | ||||
| 			: defPriority; | ||||
|  | ||||
| 		// Allow access if the method is non-public | ||||
| 		method.setAccessible(true); | ||||
| 	} | ||||
|  | ||||
| 	@Override | ||||
| 	public void execute(Object event) throws EventBusException, InvocationTargetException { | ||||
| 		try { | ||||
| 			if (useParameter) | ||||
| 				method.invoke(getListener(), event); | ||||
| 			else | ||||
| 				method.invoke(getListener()); | ||||
| 		} catch (IllegalArgumentException e) { | ||||
| 			throw new EventBusException("Event handler rejected target / argument!", e); | ||||
| 		} catch (IllegalAccessException e) { | ||||
| 			throw new EventBusException("Event handler is not accessible!", e); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	@Override | ||||
| 	public String toString() { | ||||
| 		return String.format( | ||||
| 			"ReflectiveEventHandler[eventType=%s, polymorphic=%b, priority=%d, method=%s, useParameter=%b]", | ||||
| 			eventType, polymorphic, priority, method, useParameter); | ||||
| 	} | ||||
|  | ||||
| 	@Override | ||||
| 	public Object getListener() { | ||||
| 		return listener; | ||||
| 	} | ||||
|  | ||||
| 	@Override | ||||
| 	public Class<?> getEventType() { | ||||
| 		return eventType; | ||||
| 	} | ||||
|  | ||||
| 	@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; | ||||
| @@ -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.core.Event | ||||
|  * @see dev.kske.eventbus.core.EventBus | ||||
|  */ | ||||
| package dev.kske.eventbus.core; | ||||
							
								
								
									
										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.core.Event | ||||
|  * @see dev.kske.eventbus.core.EventBus | ||||
|  */ | ||||
| module dev.kske.eventbus.core { | ||||
|  | ||||
| 	exports dev.kske.eventbus.core; | ||||
| } | ||||
| @@ -1,4 +1,4 @@ | ||||
| package dev.kske.eventbus; | ||||
| package dev.kske.eventbus.core; | ||||
| 
 | ||||
| import static org.junit.jupiter.api.Assertions.assertEquals; | ||||
| 
 | ||||
| @@ -11,7 +11,7 @@ import org.junit.jupiter.api.*; | ||||
|  * @author Leon Hofmeister | ||||
|  * @since 0.1.0 | ||||
|  */ | ||||
| class CancelTest implements EventListener { | ||||
| public class CancelTest { | ||||
| 
 | ||||
| 	EventBus	bus; | ||||
| 	int			hits; | ||||
| @@ -22,7 +22,7 @@ class CancelTest implements EventListener { | ||||
| 	 * @since 0.1.0 | ||||
| 	 */ | ||||
| 	@BeforeEach | ||||
| 	void registerListener() { | ||||
| 	public void registerListener() { | ||||
| 		bus = new EventBus(); | ||||
| 		bus.registerListener(this); | ||||
| 	} | ||||
| @@ -34,18 +34,20 @@ class CancelTest implements EventListener { | ||||
| 	 * @since 0.1.0 | ||||
| 	 */ | ||||
| 	@Test | ||||
| 	void testCancellation() { | ||||
| 	public void testCancellation() { | ||||
| 		bus.dispatch(new SimpleEvent()); | ||||
| 		assertEquals(1, hits); | ||||
| 	} | ||||
| 
 | ||||
| 	@Event(eventType = SimpleEvent.class, priority = 100) | ||||
| 	@Event(SimpleEvent.class) | ||||
| 	@Priority(100) | ||||
| 	void onSimpleFirst() { | ||||
| 		++hits; | ||||
| 		bus.cancel(); | ||||
| 	} | ||||
| 
 | ||||
| 	@Event(eventType = SimpleEvent.class, priority = 50) | ||||
| 	@Event(SimpleEvent.class) | ||||
| 	@Priority(50) | ||||
| 	void onSimpleSecond() { | ||||
| 		++hits; | ||||
| 	} | ||||
| @@ -0,0 +1,49 @@ | ||||
| package dev.kske.eventbus.core; | ||||
|  | ||||
| import static org.junit.jupiter.api.Assertions.*; | ||||
|  | ||||
| import org.junit.jupiter.api.Test; | ||||
|  | ||||
| /** | ||||
|  * Tests the dispatching of a dead event if an event could not be delivered. | ||||
|  * | ||||
|  * @author Kai S. K. Engelbart | ||||
|  * @since 1.1.0 | ||||
|  */ | ||||
| public class DeadTest { | ||||
|  | ||||
| 	EventBus	bus		= new EventBus(); | ||||
| 	String		event	= "This event has no handler"; | ||||
| 	boolean		deadEventHandled; | ||||
|  | ||||
| 	/** | ||||
| 	 * Tests dead event delivery. | ||||
| 	 * | ||||
| 	 * @since 1.1.0 | ||||
| 	 */ | ||||
| 	@Test | ||||
| 	public void testDeadEvent() { | ||||
| 		bus.registerListener(this); | ||||
| 		bus.dispatch(event); | ||||
| 		assertTrue(deadEventHandled); | ||||
| 		bus.removeListener(this); | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * 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 | ||||
| 	public void testUnhandledDeadEvent() { | ||||
| 		bus.dispatch(event); | ||||
| 	} | ||||
|  | ||||
| 	@Event | ||||
| 	void onDeadEvent(DeadEvent deadEvent) { | ||||
| 		assertEquals(bus, deadEvent.getEventBus()); | ||||
| 		assertEquals(event, deadEvent.getEvent()); | ||||
| 		deadEventHandled = true; | ||||
| 	} | ||||
| } | ||||
| @@ -0,0 +1,79 @@ | ||||
| package dev.kske.eventbus.core; | ||||
|  | ||||
| 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 | ||||
|  */ | ||||
| @Polymorphic | ||||
| @Priority(150) | ||||
| public class DispatchTest { | ||||
|  | ||||
| 	EventBus	bus; | ||||
| 	static int	hits; | ||||
|  | ||||
| 	/** | ||||
| 	 * Constructs an event bus and registers this test instance as an event listener. | ||||
| 	 * | ||||
| 	 * @since 0.0.1 | ||||
| 	 */ | ||||
| 	@BeforeEach | ||||
| 	public void registerListener() { | ||||
| 		bus = new EventBus(); | ||||
| 		bus.registerListener(this); | ||||
| 		bus.registerListener(SimpleEvent.class, e -> { | ||||
| 			++hits; | ||||
| 			assertEquals(4, hits); | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * Tests {@link EventBus#dispatch(Object)} with multiple handler priorities, a polymorphic | ||||
| 	 * handler and a static handler. | ||||
| 	 * | ||||
| 	 * @since 0.0.1 | ||||
| 	 */ | ||||
| 	@Test | ||||
| 	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() { | ||||
| 		++hits; | ||||
| 		assertTrue(hits == 1 || hits == 2); | ||||
| 	} | ||||
|  | ||||
| 	@Event(SimpleEvent.class) | ||||
| 	@Polymorphic(false) | ||||
| 	static void onSimpleEventSecond() { | ||||
| 		++hits; | ||||
| 		assertEquals(3, hits); | ||||
| 	} | ||||
| } | ||||
| @@ -0,0 +1,62 @@ | ||||
| package dev.kske.eventbus.core; | ||||
|  | ||||
| import static org.junit.jupiter.api.Assertions.*; | ||||
|  | ||||
| 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 | ||||
|  */ | ||||
| public class ExceptionTest { | ||||
|  | ||||
| 	EventBus			bus			= new EventBus(); | ||||
| 	String				event		= "This event will cause an exception"; | ||||
| 	RuntimeException	exception	= new RuntimeException("I failed"); | ||||
| 	boolean				exceptionEventHandled; | ||||
|  | ||||
| 	/** | ||||
| 	 * Tests exception event delivery. | ||||
| 	 * | ||||
| 	 * @since 1.1.0 | ||||
| 	 */ | ||||
| 	@Test | ||||
| 	public void testExceptionEvent() { | ||||
| 		bus.registerListener(this); | ||||
| 		bus.registerListener(new ExceptionListener()); | ||||
| 		bus.dispatch(event); | ||||
| 		assertTrue(exceptionEventHandled); | ||||
| 		bus.clearListeners(); | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * 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 | ||||
| 	public void testUnhandledExceptionEvent() { | ||||
| 		bus.registerListener(this); | ||||
| 		bus.dispatch(event); | ||||
| 		bus.removeListener(this); | ||||
| 	} | ||||
|  | ||||
| 	@Event(String.class) | ||||
| 	void onString() { | ||||
| 		throw exception; | ||||
| 	} | ||||
|  | ||||
| 	class ExceptionListener { | ||||
|  | ||||
| 		@Event | ||||
| 		void onExceptionEvent(ExceptionEvent exceptionEvent) { | ||||
| 			assertEquals(bus, exceptionEvent.getEventBus()); | ||||
| 			assertEquals(event, exceptionEvent.getEvent()); | ||||
| 			assertEquals(exception, exceptionEvent.getCause()); | ||||
| 			exceptionEventHandled = true; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @@ -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()); | ||||
| 	} | ||||
| } | ||||
| @@ -1,4 +1,4 @@ | ||||
| package dev.kske.eventbus; | ||||
| package dev.kske.eventbus.core; | ||||
| 
 | ||||
| /** | ||||
|  * A simple event for testing purposes. | ||||
| @@ -6,4 +6,4 @@ package dev.kske.eventbus; | ||||
|  * @author Kai S. K. Engelbart | ||||
|  * @since 0.0.1 | ||||
|  */ | ||||
| public class SimpleEvent implements IEvent {} | ||||
| public class SimpleEvent {} | ||||
| @@ -1,4 +1,4 @@ | ||||
| package dev.kske.eventbus; | ||||
| package dev.kske.eventbus.core; | ||||
| 
 | ||||
| /** | ||||
|  * Subclass of {@link SimpleEvent} for testing purposes. | ||||
							
								
								
									
										32
									
								
								event-bus-proc/.classpath
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								event-bus-proc/.classpath
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | ||||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <classpath> | ||||
| 	<classpathentry kind="src" output="target/classes" path="src/main/java"> | ||||
| 		<attributes> | ||||
| 			<attribute name="optional" value="true"/> | ||||
| 			<attribute name="maven.pomderived" value="true"/> | ||||
| 		</attributes> | ||||
| 	</classpathentry> | ||||
| 	<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11"> | ||||
| 		<attributes> | ||||
| 			<attribute name="maven.pomderived" value="true"/> | ||||
| 		</attributes> | ||||
| 	</classpathentry> | ||||
| 	<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER"> | ||||
| 		<attributes> | ||||
| 			<attribute name="maven.pomderived" value="true"/> | ||||
| 		</attributes> | ||||
| 	</classpathentry> | ||||
| 	<classpathentry kind="src" output="target/test-classes" path="home/kske/git/event-bus/event-bus-ap"> | ||||
| 		<attributes> | ||||
| 			<attribute name="optional" value="true"/> | ||||
| 			<attribute name="maven.pomderived" value="true"/> | ||||
| 			<attribute name="test" value="true"/> | ||||
| 		</attributes> | ||||
| 	</classpathentry> | ||||
| 	<classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources"> | ||||
| 		<attributes> | ||||
| 			<attribute name="maven.pomderived" value="true"/> | ||||
| 		</attributes> | ||||
| 	</classpathentry> | ||||
| 	<classpathentry kind="output" path="target/classes"/> | ||||
| </classpath> | ||||
							
								
								
									
										23
									
								
								event-bus-proc/.project
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								event-bus-proc/.project
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <projectDescription> | ||||
| 	<name>event-bus-proc</name> | ||||
| 	<comment></comment> | ||||
| 	<projects> | ||||
| 	</projects> | ||||
| 	<buildSpec> | ||||
| 		<buildCommand> | ||||
| 			<name>org.eclipse.jdt.core.javabuilder</name> | ||||
| 			<arguments> | ||||
| 			</arguments> | ||||
| 		</buildCommand> | ||||
| 		<buildCommand> | ||||
| 			<name>org.eclipse.m2e.core.maven2Builder</name> | ||||
| 			<arguments> | ||||
| 			</arguments> | ||||
| 		</buildCommand> | ||||
| 	</buildSpec> | ||||
| 	<natures> | ||||
| 		<nature>org.eclipse.jdt.core.javanature</nature> | ||||
| 		<nature>org.eclipse.m2e.core.maven2Nature</nature> | ||||
| 	</natures> | ||||
| </projectDescription> | ||||
							
								
								
									
										75
									
								
								event-bus-proc/pom.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								event-bus-proc/pom.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,75 @@ | ||||
| <project xmlns="http://maven.apache.org/POM/4.0.0" | ||||
| 	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||||
| 	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> | ||||
| 	<modelVersion>4.0.0</modelVersion> | ||||
|  | ||||
| 	<artifactId>event-bus-proc</artifactId> | ||||
|  | ||||
| 	<name>Event Bus Annotation Processor</name> | ||||
| 	<description>Annotation processor checking for errors related to the @Event annotation from Event Bus.</description> | ||||
|  | ||||
| 	<parent> | ||||
| 		<groupId>dev.kske</groupId> | ||||
| 		<artifactId>event-bus</artifactId> | ||||
| 		<version>1.1.0</version> | ||||
| 	</parent> | ||||
|  | ||||
| 	<dependencies> | ||||
| 		<dependency> | ||||
| 			<groupId>dev.kske</groupId> | ||||
| 			<artifactId>event-bus-core</artifactId> | ||||
| 			<version>${project.version}</version> | ||||
| 		</dependency> | ||||
| 	</dependencies> | ||||
|  | ||||
| 	<build> | ||||
|  | ||||
| 		<!-- Disable test folder --> | ||||
| 		<testSourceDirectory /> | ||||
|  | ||||
| 		<plugins> | ||||
|  | ||||
| 			<!-- Prevent annotation processing error during compilation --> | ||||
| 			<plugin> | ||||
| 				<artifactId>maven-compiler-plugin</artifactId> | ||||
| 				<configuration> | ||||
| 					<compilerArgument>-proc:none</compilerArgument> | ||||
| 				</configuration> | ||||
| 			</plugin> | ||||
|  | ||||
| 			<!-- Include event-bus-core classes into JAR --> | ||||
| 			<plugin> | ||||
| 				<groupId>org.apache.maven.plugins</groupId> | ||||
| 				<artifactId>maven-shade-plugin</artifactId> | ||||
| 				<version>3.2.4</version> | ||||
| 				<executions> | ||||
| 					<execution> | ||||
| 						<phase>package</phase> | ||||
| 						<goals> | ||||
| 							<goal>shade</goal> | ||||
| 						</goals> | ||||
| 						<configuration> | ||||
| 							<minimizeJar>true</minimizeJar> | ||||
| 							<filters> | ||||
| 								<filter> | ||||
| 									<artifact>dev.kske:event-bus-core</artifact> | ||||
| 									<excludes> | ||||
| 										<exclude>META-INF/MANIFEST.MF</exclude> | ||||
| 									</excludes> | ||||
| 								</filter> | ||||
| 								<filter> | ||||
| 									<artifact>*:*</artifact> | ||||
| 									<excludes> | ||||
| 										<exclude>module-info.class</exclude> | ||||
| 										<exclude>META-INF/maven/**</exclude> | ||||
| 									</excludes> | ||||
| 								</filter> | ||||
| 							</filters> | ||||
| 						</configuration> | ||||
| 					</execution> | ||||
| 				</executions> | ||||
| 			</plugin> | ||||
| 		</plugins> | ||||
|  | ||||
| 	</build> | ||||
| </project> | ||||
| @@ -0,0 +1,155 @@ | ||||
| package dev.kske.eventbus.proc; | ||||
|  | ||||
| import java.util.Set; | ||||
|  | ||||
| import javax.annotation.processing.*; | ||||
| import javax.lang.model.SourceVersion; | ||||
| import javax.lang.model.element.*; | ||||
| import javax.lang.model.type.*; | ||||
| import javax.tools.Diagnostic.Kind; | ||||
|  | ||||
| import dev.kske.eventbus.core.*; | ||||
|  | ||||
| /** | ||||
|  * This annotation processor checks event handlers for common mistakes which can only be detected | ||||
|  * during runtime otherwise. | ||||
|  * | ||||
|  * @author Kai S. K. Engelbart | ||||
|  * @since 1.0.0 | ||||
|  */ | ||||
| @SupportedAnnotationTypes("dev.kske.eventbus.core.Event") | ||||
| @SupportedSourceVersion(SourceVersion.RELEASE_11) | ||||
| public class EventProcessor extends AbstractProcessor { | ||||
|  | ||||
| 	@SuppressWarnings("unchecked") | ||||
| 	@Override | ||||
| 	public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { | ||||
| 		if (!roundEnv.errorRaised() && !roundEnv.processingOver()) | ||||
| 			processRound( | ||||
| 				(Set<ExecutableElement>) roundEnv.getElementsAnnotatedWith(Event.class)); | ||||
|  | ||||
| 		// Do not claim the processed annotations | ||||
| 		return false; | ||||
| 	} | ||||
|  | ||||
| 	private void processRound(Set<ExecutableElement> eventHandlers) { | ||||
| 		for (ExecutableElement eventHandler : eventHandlers) { | ||||
| 			Event		eventAnnotation	= eventHandler.getAnnotation(Event.class); | ||||
| 			TypeMirror	eventType; | ||||
|  | ||||
| 			// Determine the event type and how it is defined | ||||
| 			boolean useParameter; | ||||
| 			try { | ||||
| 				eventAnnotation.value(); | ||||
| 				throw new EventBusException( | ||||
| 					"Could not determine event type of handler " + eventHandler); | ||||
| 			} catch (MirroredTypeException e) { | ||||
|  | ||||
| 				// Task failed successfully | ||||
| 				eventType		= e.getTypeMirror(); | ||||
| 				useParameter	= processingEnv.getTypeUtils().isSameType(eventType, | ||||
| 					getTypeMirror(Event.USE_PARAMETER.class)); | ||||
| 			} | ||||
|  | ||||
| 			// Check handler signature | ||||
| 			boolean pass = false; | ||||
| 			if (useParameter && eventHandler.getParameters().size() == 0) | ||||
| 				error(eventHandler, "The method or the annotation must define the event type"); | ||||
| 			else if (!useParameter && eventHandler.getParameters().size() == 1) | ||||
| 				error(eventHandler, | ||||
| 					"Either the method or the annotation must define the event type"); | ||||
| 			else if (eventHandler.getParameters().size() > 1) | ||||
| 				error(eventHandler, "Method must not have more than one parameter"); | ||||
| 			else | ||||
| 				pass = true; | ||||
|  | ||||
| 			// Warn the user about unused return values | ||||
| 			if (useParameter && eventHandler.getReturnType().getKind() != TypeKind.VOID) | ||||
| 				warning(eventHandler, "Unused return value"); | ||||
|  | ||||
| 			// Abort checking if the handler signature is incorrect | ||||
| 			if (!pass) | ||||
| 				continue; | ||||
|  | ||||
| 			// Additional checks if parameter is used | ||||
| 			if (useParameter) { | ||||
| 				VariableElement paramElement = eventHandler.getParameters().get(0); | ||||
| 				eventType = paramElement.asType(); | ||||
|  | ||||
| 				// Check if parameter is object | ||||
| 				// Abort checking otherwise | ||||
| 				if (eventType.getKind() != TypeKind.DECLARED) { | ||||
| 					error(paramElement, "Event must be an object"); | ||||
| 					continue; | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			// Get the listener containing this handler | ||||
| 			TypeElement listener = (TypeElement) eventHandler.getEnclosingElement(); | ||||
|  | ||||
| 			// Default properties | ||||
| 			boolean	defPolymorphic	= false; | ||||
| 			int		defPriority		= 100; | ||||
|  | ||||
| 			// Listener-level polymorphism | ||||
| 			Polymorphic	listenerPolymorphic		= listener.getAnnotation(Polymorphic.class); | ||||
| 			boolean		hasListenerPolymorphic	= listenerPolymorphic != null; | ||||
|  | ||||
| 			// Listener-level priority | ||||
| 			Priority	listenerPriority	= listener.getAnnotation(Priority.class); | ||||
| 			boolean		hasListenerPriority	= listenerPriority != null; | ||||
|  | ||||
| 			// Effective polymorphism | ||||
| 			boolean polymorphic = | ||||
| 				hasListenerPolymorphic ? listenerPolymorphic.value() : defPolymorphic; | ||||
| 			boolean	hasHandlerPolymorphic	= eventHandler.getAnnotation(Polymorphic.class) != null; | ||||
| 			if (hasHandlerPolymorphic) | ||||
| 				polymorphic = eventHandler.getAnnotation(Polymorphic.class).value(); | ||||
|  | ||||
| 			// Effective priority | ||||
| 			int priority = hasListenerPriority ? listenerPriority.value() : defPriority; | ||||
| 			boolean	hasHandlerPriority	= eventHandler.getAnnotation(Priority.class) != null; | ||||
| 			if (hasHandlerPriority) | ||||
| 				priority = eventHandler.getAnnotation(Priority.class).value(); | ||||
|  | ||||
| 			// Detect useless polymorphism redefinition | ||||
| 			if (hasListenerPolymorphic && hasHandlerPolymorphic | ||||
| 				&& listenerPolymorphic.value() == polymorphic) | ||||
| 				warning(eventHandler, "@Polymorphism is already defined at listener level"); | ||||
|  | ||||
| 			// Detect useless priority redefinition | ||||
| 			if (hasListenerPriority && hasHandlerPriority && listenerPriority.value() == priority) | ||||
| 				warning(eventHandler, "@Priority is already defined at the listener level"); | ||||
|  | ||||
| 			// Detect missing or useless @Polymorphic | ||||
| 			Element eventElement = ((DeclaredType) eventType).asElement(); | ||||
|  | ||||
| 			// Check for handlers for abstract types that aren't polymorphic | ||||
| 			if (!polymorphic && (eventElement.getKind() == ElementKind.INTERFACE | ||||
| 				|| eventElement.getModifiers().contains(Modifier.ABSTRACT))) | ||||
| 				warning(eventHandler, | ||||
| 					"Parameter should be instantiable or handler should use @Polymorphic"); | ||||
|  | ||||
| 			// Check for handlers for final types that are polymorphic | ||||
| 			else if (polymorphic && eventElement.getModifiers().contains(Modifier.FINAL)) | ||||
| 				warning(eventHandler, | ||||
| 					"@Polymorphic should be removed as parameter cannot be subclassed"); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	private TypeMirror getTypeMirror(Class<?> clazz) { | ||||
| 		return getTypeElement(clazz).asType(); | ||||
| 	} | ||||
|  | ||||
| 	private TypeElement getTypeElement(Class<?> clazz) { | ||||
| 		return processingEnv.getElementUtils().getTypeElement(clazz.getCanonicalName()); | ||||
| 	} | ||||
|  | ||||
| 	private void warning(Element e, String msg, Object... args) { | ||||
| 		processingEnv.getMessager().printMessage(Kind.WARNING, String.format(msg, args), e); | ||||
| 	} | ||||
|  | ||||
| 	private void error(Element e, String msg, Object... args) { | ||||
| 		processingEnv.getMessager().printMessage(Kind.ERROR, String.format(msg, args), e); | ||||
| 	} | ||||
| } | ||||
| @@ -0,0 +1,7 @@ | ||||
| /** | ||||
|  * Contains the Event Bus annotation processor. | ||||
|  * | ||||
|  * @author Kai S. K. Engelbart | ||||
|  * @since 1.0.0 | ||||
|  */ | ||||
| package dev.kske.eventbus.proc; | ||||
							
								
								
									
										12
									
								
								event-bus-proc/src/main/java/module-info.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								event-bus-proc/src/main/java/module-info.java
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | ||||
| /** | ||||
|  * Contains an annotation processor for checking for errors related to the | ||||
|  * {@link dev.kske.eventbus.core.Event} annotation from Event Bus. | ||||
|  * | ||||
|  * @author Kai S. K. Engelbart | ||||
|  * @since 1.0.0 | ||||
|  */ | ||||
| module dev.kske.eventbus.ap { | ||||
|  | ||||
| 	requires java.compiler; | ||||
| 	requires dev.kske.eventbus.core; | ||||
| } | ||||
| @@ -0,0 +1 @@ | ||||
| dev.kske.eventbus.proc.EventProcessor | ||||
							
								
								
									
										104
									
								
								pom.xml
									
									
									
									
									
								
							
							
						
						
									
										104
									
								
								pom.xml
									
									
									
									
									
								
							| @@ -5,16 +5,24 @@ | ||||
|  | ||||
| 	<groupId>dev.kske</groupId> | ||||
| 	<artifactId>event-bus</artifactId> | ||||
| 	<version>0.1.0</version> | ||||
| 	<version>1.1.0</version> | ||||
| 	<packaging>pom</packaging> | ||||
|  | ||||
| 	<name>Event Bus</name> | ||||
| 	<description>An event handling framework for Java utilizing annotations.</description> | ||||
| 	<url>https://git.kske.dev/zdm/event-bus</url> | ||||
| 	<description>An event handling library for Java utilizing annotations.</description> | ||||
| 	<url>https://git.kske.dev/kske/event-bus</url> | ||||
|  | ||||
| 	<modules> | ||||
| 		<module>event-bus-core</module> | ||||
| 		<module>event-bus-proc</module> | ||||
| 	</modules> | ||||
|  | ||||
| 	<licenses> | ||||
| 		<license> | ||||
| 			<name>MIT License</name> | ||||
| 			<url>http://www.opensource.org/licenses/mit-license.php</url> | ||||
| 			<name>Apache License, Version 2.0</name> | ||||
| 			<url>https://www.apache.org/licenses/LICENSE-2.0.txt</url> | ||||
| 			<distribution>repo</distribution> | ||||
| 			<comments>A business-friendly OSS license</comments> | ||||
| 		</license> | ||||
| 	</licenses> | ||||
|  | ||||
| @@ -32,9 +40,23 @@ | ||||
| 		</developer> | ||||
| 	</developers> | ||||
|  | ||||
| 	<contributors> | ||||
| 		<contributor> | ||||
| 			<name>Leon Hofmeister</name> | ||||
| 			<email>leon@kske.dev</email> | ||||
| 			<url>https://git.kske.dev/delvh</url> | ||||
| 			<roles> | ||||
| 				<role>qa</role> | ||||
| 				<role>tester</role> | ||||
| 			</roles> | ||||
| 			<timezone>Europe/Berlin</timezone> | ||||
| 		</contributor> | ||||
| 	</contributors> | ||||
|  | ||||
| 	<scm> | ||||
| 		<connection>scm:git:https://git.kske.dev/zdm/event-bus.git</connection> | ||||
| 		<developerConnection>scm:git:ssh:git@git.kske.dev:zdm/event-bus.git</developerConnection> | ||||
| 		<connection>scm:git:https://git.kske.dev/kske/event-bus.git</connection> | ||||
| 		<developerConnection>scm:git:ssh://git@git.kske.dev:420/kske/event-bus.git</developerConnection> | ||||
| 		<url>https://git.kske.dev/kske/event-bus</url> | ||||
| 	</scm> | ||||
|  | ||||
| 	<properties> | ||||
| @@ -44,10 +66,21 @@ | ||||
| 		<maven.compiler.target>11</maven.compiler.target> | ||||
| 	</properties> | ||||
|  | ||||
| 	<!-- Configure deployment to OSSRH --> | ||||
| 	<distributionManagement> | ||||
| 		<snapshotRepository> | ||||
| 			<id>ossrh</id> | ||||
| 			<url>https://oss.sonatype.org/content/repositories/snapshots</url> | ||||
| 		</snapshotRepository> | ||||
| 		<repository> | ||||
| 			<id>ossrh</id> | ||||
| 			<url>https://oss.sonatype.org/service/local/staging/deploy/maven2/</url> | ||||
| 		</repository> | ||||
| 	</distributionManagement> | ||||
|  | ||||
| 	<build> | ||||
|  | ||||
| 		<!-- Disable resource folders --> | ||||
| 		<resources /> | ||||
| 		<!-- Disable test resource folder --> | ||||
| 		<testResources /> | ||||
|  | ||||
| 		<plugins> | ||||
| @@ -59,7 +92,7 @@ | ||||
| 				<version>3.8.1</version> | ||||
| 			</plugin> | ||||
|  | ||||
| 			<!-- Attach sources and Javadoc to JAR --> | ||||
| 			<!-- Attach sources to JAR --> | ||||
| 			<plugin> | ||||
| 				<groupId>org.apache.maven.plugins</groupId> | ||||
| 				<artifactId>maven-source-plugin</artifactId> | ||||
| @@ -73,6 +106,8 @@ | ||||
| 					</execution> | ||||
| 				</executions> | ||||
| 			</plugin> | ||||
|  | ||||
| 			<!-- Attach Javadoc to JAR --> | ||||
| 			<plugin> | ||||
| 				<groupId>org.apache.maven.plugins</groupId> | ||||
| 				<artifactId>maven-javadoc-plugin</artifactId> | ||||
| @@ -85,17 +120,46 @@ | ||||
| 						</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 --> | ||||
| 			<plugin> | ||||
| 				<groupId>org.apache.maven.plugins</groupId> | ||||
| 				<artifactId>maven-gpg-plugin</artifactId> | ||||
| 				<version>1.6</version> | ||||
| 				<executions> | ||||
| 					<execution> | ||||
| 						<id>sign-artifacts</id> | ||||
| 						<phase>verify</phase> | ||||
| 						<goals> | ||||
| 							<goal>sign</goal> | ||||
| 						</goals> | ||||
| 					</execution> | ||||
| 				</executions> | ||||
| 			</plugin> | ||||
|  | ||||
| 		</plugins> | ||||
| 	</build> | ||||
|  | ||||
| 	<dependencies> | ||||
| 		<dependency> | ||||
| 			<groupId>org.junit.jupiter</groupId> | ||||
| 			<artifactId>junit-jupiter-api</artifactId> | ||||
| 			<version>5.6.2</version> | ||||
| 			<scope>test</scope> | ||||
| 		</dependency> | ||||
| 	</dependencies> | ||||
| </project> | ||||
| </project> | ||||
|   | ||||
| @@ -1,67 +0,0 @@ | ||||
| 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 {} | ||||
| } | ||||
| @@ -1,213 +0,0 @@ | ||||
| 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); | ||||
| 	} | ||||
| } | ||||
| @@ -1,136 +0,0 @@ | ||||
| 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; } | ||||
| } | ||||
| @@ -1,12 +0,0 @@ | ||||
| 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 {} | ||||
| @@ -1,12 +0,0 @@ | ||||
| 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 {} | ||||
| @@ -1,9 +0,0 @@ | ||||
| /** | ||||
|  * 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; | ||||
| @@ -1,12 +0,0 @@ | ||||
| /** | ||||
|  * 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; | ||||
| } | ||||
| @@ -1,58 +0,0 @@ | ||||
| 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); | ||||
| 	} | ||||
| } | ||||
		Reference in New Issue
	
	Block a user