Merge pull request 'Restructure Project, Add Annotation Processor' (#4) from f/annotation-processor into develop

Reviewed-on: https://git.kske.dev/kske/event-bus/pulls/4
Reviewed-by: delvh <leon@kske.dev>
This commit is contained in:
Kai S. K. Engelbart 2021-02-15 13:42:27 +01:00
commit 603fe80df6
Signed by: Käfer & Engelbart Git
GPG Key ID: 70F2F9206EDC1FCE
28 changed files with 391 additions and 72 deletions

10
.gitignore vendored
View File

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

View File

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

View File

@ -20,7 +20,7 @@ Additionally, the class containing the method must implement the `EventListener`
Lets look at a simple example: we declare the empty class `SimpleEvent` that implements `IEvent` and can thus be used as an event.
```java
import dev.kske.eventbus.IEvent;
import dev.kske.eventbus.core.IEvent;
public class SimpleEvent implements IEvent {}
```
@ -28,7 +28,7 @@ public class SimpleEvent implements IEvent {}
Next, an event listener for the `SimpleEvent` is declared:
```java
import dev.kske.eventbus.*;
import dev.kske.eventbus.core.*;
public class SimpleEventListener implements EventListener {
@ -62,7 +62,7 @@ is technically possible, however you would still have to create an instance of t
## Event handlers for subtypes
On certain occasions its practical for an event handler to accept both events of the specified type, as well as subclasses of that event.
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 `includeSubtypes` parameter as follows:
```java
@ -141,7 +141,41 @@ To include it inside your project, just add the Maven repository and the depende
<dependency>
<groupId>dev.kske</groupId>
<artifactId>event-bus</artifactId>
<version>0.1.0</version>
<version>1.0.0</version>
</dependency>
</dependencies>
```
Then, require the Event Bus Core module in your `module-info.java`:
```java
requires dev.kske.eventbus.core;
```
# Compile-Time Error Checking with Event Bus AP
To assist you with writing event listeners, the Event Bus AP (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, missing `EventListener` implementations, 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-ap</artifactId>
<version>1.0.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.

32
event-bus-ap/.classpath Normal file
View 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-ap/.project Normal file
View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>event-bus-ap</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>

57
event-bus-ap/pom.xml Normal file
View File

@ -0,0 +1,57 @@
<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-ap</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.0.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>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<compilerArgument>-proc:none</compilerArgument>
</configuration>
</plugin>
<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>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,107 @@
package dev.kske.eventbus.ap;
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) {
TypeElement eventListener = (TypeElement) eventHandler.getEnclosingElement();
Event eventAnnotation = eventHandler.getAnnotation(Event.class);
// Determine how the event type is defined
boolean useParameter;
try {
eventAnnotation.eventType();
throw new EventBusException(
"Could not determine event type of handler " + eventHandler);
} catch (MirroredTypeException e) {
// Task failed successfully
useParameter = processingEnv.getTypeUtils().isSameType(e.getTypeMirror(),
getTypeMirror(Event.USE_PARAMETER.class));
}
// Check for correct method signature and return type
if (eventHandler.getParameters().size() == 0 && useParameter)
error(eventHandler, "The method or the annotation must define the event type");
if (eventHandler.getParameters().size() == 1 && !useParameter)
error(eventHandler,
"Either the method or the annotation must define the event type");
if (eventHandler.getParameters().size() > 1)
error(eventHandler, "Method must not have more than one parameter");
if (eventHandler.getReturnType().getKind() != TypeKind.VOID)
error(eventHandler, "Method must return void");
// Get first parameter as type and element
var paramElement = eventHandler.getParameters().get(0);
var paramType = paramElement.asType();
// Check for valid event type
if (useParameter && !processingEnv.getTypeUtils().isAssignable(paramType,
getTypeMirror(IEvent.class)))
error(paramElement, "Parameter must implement IEvent");
// Check for handlers for abstract types that don't include subtypes
if (!eventAnnotation.includeSubtypes() && paramType.getKind() == TypeKind.DECLARED) {
var declaredElement = ((DeclaredType) paramType).asElement();
if (declaredElement.getKind() == ElementKind.INTERFACE
|| declaredElement.getModifiers().contains(Modifier.ABSTRACT))
warning(paramElement,
"Parameter should be instantiable or handler should include subtypes");
}
// Check listener for interface implementation
if (!eventListener.getInterfaces().contains(getTypeMirror(EventListener.class)))
warning(eventHandler.getEnclosingElement(),
"Class should implement EventListener interface");
}
}
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);
}
}

View File

@ -0,0 +1,7 @@
/**
* Contains the Event Bus annotation processor.
*
* @author Kai S. K. Engelbart
* @since 1.0.0
*/
package dev.kske.eventbus.ap;

View 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;
}

View File

@ -0,0 +1 @@
dev.kske.eventbus.ap.EventProcessor

23
event-bus-core/.project Normal file
View 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>

30
event-bus-core/pom.xml Normal file
View File

@ -0,0 +1,30 @@
<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.0.0</version>
</parent>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.6.2</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<!-- Disable resource folder -->
<resources />
</build>
</project>

View File

@ -1,4 +1,4 @@
package dev.kske.eventbus;
package dev.kske.eventbus.core;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

View File

@ -1,4 +1,4 @@
package dev.kske.eventbus;
package dev.kske.eventbus.core;
import java.lang.System.Logger;
import java.lang.System.Logger.Level;
@ -51,11 +51,12 @@ public final class 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);
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
@ -97,9 +98,7 @@ public final class EventBus {
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<>();
Set<EventHandler> handlers = bindings.getOrDefault(eventClass, new TreeSet<>());
// Get subtype handlers
for (var binding : bindings.entrySet())
@ -151,20 +150,18 @@ public final class EventBus {
// Initialize and bind the handler
var handler = new EventHandler(listener, method, annotation);
if (!bindings.containsKey(handler.getEventType()))
bindings.put(handler.getEventType(), new TreeSet<>());
bindings.putIfAbsent(handler.getEventType(), new TreeSet<>());
logger.log(Level.DEBUG, "Binding event handler {0}", handler);
bindings.get(handler.getEventType())
.add(handler);
handlerBound = true;
}
if(!handlerBound)
if (!handlerBound)
logger.log(
Level.WARNING,
"No event handlers bound for event listener {0}",
listener.getClass().getName()
);
listener.getClass().getName());
}
/**

View File

@ -1,4 +1,4 @@
package dev.kske.eventbus;
package dev.kske.eventbus.core;
/**
* This runtime exception is thrown when an event bus error occurs. This can

View File

@ -1,8 +1,8 @@
package dev.kske.eventbus;
package dev.kske.eventbus.core;
import java.lang.reflect.*;
import dev.kske.eventbus.Event.USE_PARAMETER;
import dev.kske.eventbus.core.Event.USE_PARAMETER;
/**
* Internal representation of an event handling method.

View File

@ -1,4 +1,4 @@
package dev.kske.eventbus;
package dev.kske.eventbus.core;
/**
* Marker interface for event listeners. Event listeners can contain event handling methods to which

View File

@ -1,4 +1,4 @@
package dev.kske.eventbus;
package dev.kske.eventbus.core;
/**
* Marker interface for event objects. Event objects can be used as event handler parameters and

View File

@ -0,0 +1,9 @@
/**
* Contains the public API and implementation of the Event Bus library.
*
* @author Kai S. K. Engelbart
* @since 0.0.1
* @see dev.kske.eventbus.core.Event
* @see dev.kske.eventbus.core.EventBus
*/
package dev.kske.eventbus.core;

View 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;
}

View File

@ -1,4 +1,4 @@
package dev.kske.eventbus;
package dev.kske.eventbus.core;
import static org.junit.jupiter.api.Assertions.assertEquals;

View File

@ -1,4 +1,4 @@
package dev.kske.eventbus;
package dev.kske.eventbus.core;
import static org.junit.jupiter.api.Assertions.*;

View File

@ -1,4 +1,4 @@
package dev.kske.eventbus;
package dev.kske.eventbus.core;
/**
* A simple event for testing purposes.

View File

@ -1,4 +1,4 @@
package dev.kske.eventbus;
package dev.kske.eventbus.core;
/**
* Subclass of {@link SimpleEvent} for testing purposes.

26
pom.xml
View File

@ -5,11 +5,17 @@
<groupId>dev.kske</groupId>
<artifactId>event-bus</artifactId>
<version>0.1.0</version>
<version>1.0.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>
<url>https://git.kske.dev/kske/event-bus</url>
<modules>
<module>event-bus-core</module>
<module>event-bus-ap</module>
</modules>
<licenses>
<license>
@ -33,8 +39,8 @@
</developers>
<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>
</scm>
<properties>
@ -46,8 +52,7 @@
<build>
<!-- Disable resource folders -->
<resources />
<!-- Disable test resource folder -->
<testResources />
<plugins>
@ -89,13 +94,4 @@
</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>

View File

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

View File

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