Skip to content

Complete JavaDocs for the API #78

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 11 commits into
base: master
Choose a base branch
from
17 changes: 10 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
<!-- NOTE: The overview, basic example, and note on nullability are also included in
`src/main/java/module-info.java` -->

# EventBus

A flexible, high-performance, thread-safe subscriber-publisher framework designed with modern Java in mind.
Expand All @@ -14,13 +17,13 @@ First, add the Forge Maven repository and the EventBus dependency to your projec
```gradle
repositories {
maven {
name = "Forge"
url = "https://maven.minecraftforge.net"
name = 'Forge'
url = 'https://maven.minecraftforge.net'
}
}

dependencies {
implementation "net.minecraftforge:eventbus:<version>"
implementation 'net.minecraftforge:eventbus:<version>'
}
```

Expand All @@ -47,11 +50,11 @@ Browse the `net.minecraftforge.eventbus.api` package and read the Javadocs for m
examples, check out Forge's extensive use of EventBus [here][Forge usages].

## Nullability
The entirety of EventBus' API is `@NullMarked` and compliant with the [jSpecify specification](https://jspecify.dev/) -
this means that everything is non-null by default unless otherwise specified.
The entirety of EventBus' API is `@NullMarked` and compliant with the [jSpecify specification](https://jspecify.dev/).
This means that everything is non-null by default unless otherwise specified.

Attempting to pass a `null` value to a method param that isn't explicitly marked as `@Nullable` is an unsupported
operation and won't be considered a breaking change if a future version throws an exception in such cases when it didn't
Attempting to pass a `null` value to a method param that isn't explicitly marked as `@Nullable` is an *unsupported
operation* and won't be considered a breaking change if a future version throws an exception in such cases when it didn't
before.

## Contributing
Expand Down
10 changes: 10 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ plugins {
id 'org.gradlex.extra-java-module-info' version '1.11'
id 'net.minecraftforge.gradleutils' version '2.4.13'
id 'net.minecraftforge.licenser' version '1.1.1'
alias libs.plugins.javadoc.links

// Enforce jSpecify annotations at compile-time
id 'net.ltgt.errorprone' version '4.1.0'
Expand All @@ -22,6 +23,7 @@ java {
toolchain.languageVersion = JavaLanguageVersion.of(21)
modularity.inferModulePath = true
withSourcesJar()
withJavadocJar()
}

repositories {
Expand All @@ -31,6 +33,7 @@ repositories {

dependencies {
api libs.jspecify.annotations
compileOnly libs.jetbrains.annotations
errorprone libs.errorprone.core
errorprone libs.nullaway
}
Expand All @@ -43,6 +46,13 @@ changelog {
from '1.0.0'
}

tasks.withType(Javadoc).configureEach {
options { StandardJavadocDocletOptions options ->
options.windowTitle = 'EventBus ' + project.version
options.tags 'apiNote:a:API Note:', 'implNote:a:Implementation Note:', 'implSpec:a:Implementation Specification:'
}
}

tasks.withType(JavaCompile).configureEach {
// Set up compile-time enforcement of the jSpecify spec via ErrorProne and NullAway
options.errorprone { ErrorProneOptions errorProne ->
Expand Down
4 changes: 4 additions & 0 deletions eventbus-jmh/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ extraJavaModuleInfo {
automaticModule('net.sf.jopt-simple:jopt-simple', 'jopt.simple')
}

tasks.named('javadoc', Javadoc) {
enabled = false
}

tasks.register('aggregateJmh', AggregateJmh) {
if (rootProject.file('jmh_data_input.json').exists())
inputData = rootProject.file('jmh_data_input.json')
Expand Down
4 changes: 4 additions & 0 deletions eventbus-test-jar/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ license {
newLine = false
}

tasks.named('javadoc', Javadoc) {
enabled = false
}

// Hack eclipse into knowing that the gradle deps are modules
eclipse.classpath {
containers 'org.eclipse.buildship.core.gradleclasspathcontainer'
Expand Down
4 changes: 4 additions & 0 deletions eventbus-test/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ extraJavaModuleInfo {
failOnMissingModuleInfo = false
}

tasks.named('javadoc', Javadoc) {
enabled = false
}

tasks.named('test', Test) {
useJUnitPlatform()
}
Expand Down
3 changes: 3 additions & 0 deletions settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,11 @@ plugins {
dependencyResolutionManagement {
versionCatalogs {
libs {
plugin 'javadoc-links', 'io.freefair.javadoc-links' version '8.13.1'

// https://mvnrepository.com/artifact/org.jspecify/jspecify
library('jspecify-annotations', 'org.jspecify', 'jspecify') version '1.0.0'
library 'jetbrains-annotations', 'org.jetbrains', 'annotations' version '26.0.2'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing mvnrepository link comment

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please approve #73 first.


// https://mvnrepository.com/artifact/com.google.errorprone/error_prone_core
library('errorprone-core', 'com.google.errorprone', 'error_prone_core') version '2.36.0'
Expand Down
47 changes: 45 additions & 2 deletions src/main/java/module-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,53 @@
*/
import org.jspecify.annotations.NullMarked;

/**
* EventBus is a flexible, high-performance, thread-safe subscriber-publisher framework designed with modern Java in
* mind.
*
* <h2>Overview</h2>
* <p>The core functionality of EventBus is to provide a simple and efficient way to handle
* {@linkplain net.minecraftforge.eventbus.api.event events} in a decoupled manner.</p>
* <p>Each event may have one or more {@linkplain net.minecraftforge.eventbus.api.bus.EventBus buses} associated with
* it, which are responsible for managing {@linkplain net.minecraftforge.eventbus.api.listener.EventListener listeners}
* and dispatching instances of the event object to them. To maximise performance, the underlying implementation is
* tailored on the fly based on the event's type,
* {@linkplain net.minecraftforge.eventbus.api.event.characteristic characteristics}, inheritance chain and the number
* and type of listeners registered to the bus.</p>
*
* <h2>Example</h2>
* <p>Here is a basic usage example of EventBus in action:</p>
* {@snippet :
* import net.minecraftforge.eventbus.api.event.RecordEvent;
* import net.minecraftforge.eventbus.api.bus.EventBus;
*
* // Define an event and a bus for it
* record PlayerLoggedInEvent(String username) implements RecordEvent {
* public static final EventBus<PlayerLoggedInEvent> BUS = EventBus.create(PlayerLoggedInEvent.class);
* }
*
* // Register an event listener
* PlayerLoggedInEvent.BUS.addListener(event -> System.out.println("Player logged in: " + event.username()));
*
* // Post an event to the registered listeners
* PlayerLoggedInEvent.BUS.post(new PlayerLoggedInEvent("Paint_Ninja"));
*}
* <p>There are several more example usages within the JavaDocs of the different packages and classes in this API
* module. These examples are non-exhaustive, but provide a good basis on which to build your usage of EventBus.</p>
*
* <h2>Nullability</h2>
* <p>The entirety of EventBus' API is {@link org.jspecify.annotations.NullMarked @NullMarked} and compliant with the
* <a href="https://jspecify.dev/">jSpecify specification</a>. This means that everything is
* {@linkplain org.jspecify.annotations.NonNull non-null} by default unless otherwise specified.</p>
* <p>Attempting to pass a {@code null} value to a method param that isn't explicitly marked as
* {@link org.jspecify.annotations.Nullable @Nullable} is an <i>unsupported operation</i> and won't be considered a
* breaking change if a future version throws an exception in such cases when it didn't before.</p>
*/
@NullMarked
module net.minecraftforge.eventbus {
requires java.logging;
requires org.jspecify;
requires java.logging; // Logging
requires org.jspecify; // Nullability
requires static org.jetbrains.annotations; // Other Static Analysis

exports net.minecraftforge.eventbus.api.bus;
exports net.minecraftforge.eventbus.api.event;
Expand Down
146 changes: 126 additions & 20 deletions src/main/java/net/minecraftforge/eventbus/api/bus/BusGroup.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,89 +4,195 @@
*/
package net.minecraftforge.eventbus.api.bus;

import net.minecraftforge.eventbus.internal.Event;
import net.minecraftforge.eventbus.api.listener.EventListener;
import net.minecraftforge.eventbus.api.listener.SubscribeEvent;
import net.minecraftforge.eventbus.internal.BusGroupImpl;
import net.minecraftforge.eventbus.internal.Event;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Contract;

import java.lang.invoke.MethodHandles;
import java.util.Collection;

/**
* A collection of {@link EventBus} instances that are grouped together for easier management.
* A bus group is a collection of {@link EventBus} instances that are grouped together for easier management.
* <p>Using a bus group allows consumers to manage all of their related event buses without needing to manually manage
* each one.</p>
*
* <h2>Example</h2>
* <p>Here is a small example showing the creation and disposal of a bus group.</p>
* {@snippet :
* import net.minecraftforge.eventbus.api.bus.BusGroup;
* import net.minecraftforge.eventbus.api.bus.EventBus;
* import net.minecraftforge.eventbus.api.event.RecordEvent;
* import net.minecraftforge.eventbus.api.listener.SubscribeEvent;
*
* import java.lang.invoke.MethodHandles;
*
* public class MyClass {
* public static final BusGroup BUS_GROUP = BusGroup.create("MyProject", RecordEvent.class);
*
* public record MyEvent(String message) implements RecordEvent {
* public static final EventBus<MyEvent> BUS = EventBus.create(BUS_GROUP, MyEvent.class);
* }
*
* @SubscribeEvent
* private static void onMyEvent(MyEvent event) {
* System.out.println("Received event: " + event.message());
* }
*
* // if we only have one listener in our class, EventBus will throw an exception saying you should use BusGroup#addListener instead
* @SubscribeEvent
* private static void alsoOnMyEvent(MyEvent event) {
* System.out.println("Double checking, received event: " + event.message());
* }
*
* // begin program!
* public static void run() {
* // the bus group is already started! no need to call startup() on it.
*
* // MethodHandles.lookup() gives EventBus the ability to get method references
* // for all the @SubscribeEvent methods in this class.
* BUS_GROUP.register(MethodHandles.lookup(), MyClass.class);
* }
*
* // close program!
* public static void shutdown() {
* // dispose will shutdown and then dispose this bus group
* // consider it "freed memory" that should not be reused
* BUS_GROUP.dispose();
* }
* }
*}
*/
public sealed interface BusGroup permits BusGroupImpl {
/**
* The default bus group, which is used when an {@linkplain EventBus event bus} is created without specifying a
* group.
*
* @apiNote If you require tight controls over your event buses, you should create your own bus group instead. This
* bus group can be used and mutated by other consumers within the same environment.
* @see EventBus#create(Class)
*/
BusGroup DEFAULT = create("default");

/**
* Creates a new bus group with the given name.
* <p>The name for this bus group <i>must be unique.</i> An attempt to create a bus group with a name that is
* already in use will result in an {@link IllegalArgumentException}. If you must create a new bus group with a name
* that is in use, the relevant bus group must be {@linkplain #dispose() disposed}.</p>
*
* @param name The name
* @return The new bus group
* @throws IllegalArgumentException If the name is already in use by another bus group
* @apiNote To enforce a base type with your bus group, use {@linkplain #create(String, Class)}.
*/
static BusGroup create(String name) {
return new BusGroupImpl(name, Event.class);
}

/**
* Creates a new bus group with the given name.
* <p>The given base type will enforce that all {@linkplain EventBus event buses} created within this group inherit
* it.</p>
* <p>The name for this bus group <i>must be unique.</i> An attempt to create a bus group with a name that is
* already in use will result in an {@link IllegalArgumentException}. If you must create a new bus group with a name
* that is in use, the relevant bus group must be {@linkplain #dispose() disposed}.</p>
*
* @param name The name
* @return The new bus group
* @throws IllegalArgumentException If the name is already in use by another bus group
*/
static BusGroup create(String name, Class<?> baseType) {
return new BusGroupImpl(name, baseType);
}

/**
* The unique name of this BusGroup.
* <p>The uniqueness of this name is enforced when the bus group is {@linkplain #create(String) created}.</p>
*/
@Contract(pure = true)
String name();

/**
* Starts up all EventBus instances associated with this BusGroup, allowing events to be posted again after a
* Starts up all EventBus instances associated with this bus group, allowing events to be posted again after a
* previous call to {@link #shutdown()}.
* <p>Calling this method without having previously called {@link #shutdown()} will have no effect.</p>
*/
void startup();

/**
* Shuts down all EventBus instances associated with this BusGroup, preventing any further events from being posted
* Shuts down all EventBus instances associated with this bus group, preventing any further events from being posted
* until {@link #startup()} is called.
* <p>Calling this method without having previously called {@link #startup()} will have no effect.</p>
*
* @apiNote If you need to destroy this bus group and free up the resources it uses, use {@link #dispose()}.
*/
void shutdown();

/**
* Shuts down all EventBus instances associated with this BusGroup, unregisters all listeners and frees resources
* no longer needed.
* <p>Warning: This is a destructive operation - this BusGroup should not be used again after calling this method.</p>
* {@linkplain #shutdown() Shuts down} all EventBus instances associated with this bus group,
* {@linkplain #unregister(Collection) unregisters} all listeners and frees resources no longer needed.
* <p><strong>This will effectively destroy this bus group.</strong> It should not be used again after calling this
* method.</p>
*
* @apiNote If you plan on using this bus group again, use {@link #shutdown()} instead.
*/
void dispose();

/**
* Experimental feature - may be removed, renamed or otherwise changed without notice.
* <p>Trims the backing lists of all EventBus instances associated with this BusGroup to free up resources.</p>
* <p>Warning: This is only intended to be called <b>once</b> after all listeners are registered - calling this
* Trims the backing lists of all EventBus instances associated with this BusGroup to free up resources.
* <p>This is only intended to be called <strong>once</strong> after all listeners are registered. Calling this
* repeatedly may hurt performance.</p>
*
* @apiNote <strong>This is an experimental feature!</strong> It may be removed, renamed or otherwise changed
* without notice.
*/
@ApiStatus.Experimental
void trim();

/**
* Registers all static methods annotated with {@link SubscribeEvent} in the given class.
* Registers all <i>static</i> methods annotated with {@link SubscribeEvent} in the given class.
* <p>This is done by getting method references for those methods using the given
* {@linkplain MethodHandles.Lookup method handles lookup}. This lookup <strong>must be acquiored from
* {@link MethodHandles#lookup()}.</strong> Using {@link MethodHandles#publicLookup()} is unsupported because it
* doesn't work with {@link java.lang.invoke.LambdaMetafactory} as it could allow for access to private fields
* through inner class generation.</p>
*
* @param callerLookup {@code MethodHandles.lookup()} from the class containing listeners
* @param callerLookup {@link MethodHandles#lookup()} from the class containing listeners
* @param utilityClassWithStaticListeners the class containing the static listeners
* @return A collection of the registered listeners, which can be used to optionally unregister them later
*
* @apiNote This method only registers static listeners.
* <p>If you want to register both instance and static methods, use
* {@link BusGroup#register(MethodHandles.Lookup, Object)} instead.</p>
* <p>If you want to register both instance and static methods, use
* {@link BusGroup#register(MethodHandles.Lookup, Object)} instead.</p>
*/
Collection<EventListener> register(MethodHandles.Lookup callerLookup, Class<?> utilityClassWithStaticListeners);

/**
* Registers all methods annotated with {@link SubscribeEvent} in the given object.
* <p>Both the static <i>and</i> instance methods for the given object are registered. Keep in mind that, unlike
* with {@link #register(MethodHandles.Lookup, Class)}, you will need to register each object instance of the class
* using this method.</p>
* <p>This is done by getting method references for those methods using the given
* {@linkplain MethodHandles.Lookup method handles lookup}. This lookup <strong>must be acquiored from
* {@link MethodHandles#lookup()}.</strong> Using {@link MethodHandles#publicLookup()} is unsupported because it
* doesn't work with {@link java.lang.invoke.LambdaMetafactory} as it could allow for access to private fields
* through inner class generation.</p>
*
* @param callerLookup {@code MethodHandles.lookup()} from the class containing the listeners
* @param listener the object containing the static and/or instance listeners
* @param listener the object containing the static and/or instance listeners
* @return A collection of the registered listeners, which can be used to optionally unregister them later
*
* @apiNote If you know all the listeners are static methods, use
* {@link BusGroup#register(MethodHandles.Lookup, Class)} instead for better registration performance.
* {@link BusGroup#register(MethodHandles.Lookup, Class)} instead for better registration performance.
*/
Collection<EventListener> register(MethodHandles.Lookup callerLookup, Object listener);

/**
* Unregisters the given listeners from this BusGroup.
* Unregisters the given listeners from this bus group.
*
* @param listeners A collection of listeners to unregister, obtained from
* {@link #register(MethodHandles.Lookup, Class)} or {@link #register(MethodHandles.Lookup, Object)}
* {@link #register(MethodHandles.Lookup, Class)} or
* {@link #register(MethodHandles.Lookup, Object)}
*/
void unregister(Collection<EventListener> listeners);
}
Loading