diff --git a/src/main/java/org/toop/Main.java b/src/main/java/org/toop/Main.java index cca3a60..136ab9b 100644 --- a/src/main/java/org/toop/Main.java +++ b/src/main/java/org/toop/Main.java @@ -48,8 +48,8 @@ public class Main { } private static void registerEvents() { - new EventPublisher<>(Events.WindowEvents.OnQuitRequested.class, _ -> quit()); - new EventPublisher<>(Events.WindowEvents.OnMouseMove.class, _ -> {}); + new EventPublisher().onEvent(Events.WindowEvents.OnQuitRequested.class).perform(_ -> quit()); + new EventPublisher().onEvent(Events.WindowEvents.OnMouseMove.class).perform(_ -> {}); } private static void quit() { diff --git a/src/main/java/org/toop/MainTest.java b/src/main/java/org/toop/MainTest.java index 54f8c96..79ac469 100644 --- a/src/main/java/org/toop/MainTest.java +++ b/src/main/java/org/toop/MainTest.java @@ -1,6 +1,6 @@ package org.toop; -import org.toop.eventbus.EventPublisher; +import org.toop.eventbus.EventFlow; import org.toop.eventbus.GlobalEventBus; import org.toop.eventbus.events.NetworkEvents; import org.toop.frontend.networking.NetworkingGameClientHandler; @@ -9,12 +9,13 @@ import java.util.function.Supplier; public class MainTest { MainTest() { - var a = new EventPublisher<>( - NetworkEvents.StartClient.class, - (Supplier) NetworkingGameClientHandler::new, - "127.0.0.1", - 5001 - ).onEventById(NetworkEvents.StartClientSuccess.class, this::handleStartClientSuccess) + var a = new EventFlow() + .addPostEvent(NetworkEvents.StartClient.class, + (Supplier) NetworkingGameClientHandler::new, + "127.0.0.1", + 5001) + .onResponse(NetworkEvents.StartClientSuccess.class) + .perform(this::handleStartClientSuccess) .unsubscribeAfterSuccess().asyncPostEvent(); } diff --git a/src/main/java/org/toop/eventbus/EventFlow.java b/src/main/java/org/toop/eventbus/EventFlow.java new file mode 100644 index 0000000..f5c4f14 --- /dev/null +++ b/src/main/java/org/toop/eventbus/EventFlow.java @@ -0,0 +1,221 @@ +package org.toop.eventbus; + +import org.toop.eventbus.events.EventWithUuid; +import org.toop.eventbus.events.IEvent; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Consumer; + +/** + * EventFlow is a utility class for creating, posting, and optionally subscribing to events + * in a type-safe and chainable manner. It is designed to work with the {@link GlobalEventBus}. + * + *

This class supports automatic UUID assignment for {@link EventWithUuid} events, + * and allows filtering subscribers so they only respond to events with a specific UUID. + * All subscription methods are chainable, and you can configure automatic unsubscription + * after an event has been successfully handled.

+ */ +public class EventFlow { + + /** Lookup object used for dynamically invoking constructors via MethodHandles. */ + private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup(); + + /** Cache of constructor handles for event classes to avoid repeated reflection lookups. */ + private static final Map, MethodHandle> CONSTRUCTOR_CACHE = new ConcurrentHashMap<>(); + + /** Automatically assigned UUID for {@link EventWithUuid} events. */ + private String eventId = null; + + /** The event instance created by this publisher. */ + private IEvent event = null; + + /** The listener returned by GlobalEventBus subscription. Used for unsubscription. */ + private Object listener; + + /** Flag indicating whether to automatically unsubscribe the listener after success. */ + private boolean unsubscribeAfterSuccess = false; + + /** Holds the results returned from the subscribed event, if any. */ + private Map result = null; + + /** Empty constructor (event must be added via {@link #addPostEvent}). */ + public EventFlow() {} + + /** + * Instantiate an event of the given class and store it in this publisher. + */ + public EventFlow addPostEvent(Class eventClass, Object... args) { + try { + boolean isUuidEvent = EventWithUuid.class.isAssignableFrom(eventClass); + + MethodHandle ctorHandle = CONSTRUCTOR_CACHE.computeIfAbsent(eventClass, cls -> { + try { + Class[] paramTypes = cls.getDeclaredConstructors()[0].getParameterTypes(); + MethodType mt = MethodType.methodType(void.class, paramTypes); + return LOOKUP.findConstructor(cls, mt); + } catch (Exception e) { + throw new RuntimeException("Failed to find constructor handle for " + cls, e); + } + }); + + Object[] finalArgs; + int expectedParamCount = ctorHandle.type().parameterCount(); + + if (isUuidEvent && args.length < expectedParamCount) { + this.eventId = UUID.randomUUID().toString(); + finalArgs = new Object[args.length + 1]; + System.arraycopy(args, 0, finalArgs, 0, args.length); + finalArgs[args.length] = this.eventId; + } else if (isUuidEvent) { + this.eventId = (String) args[args.length - 1]; + finalArgs = args; + } else { + finalArgs = args; + } + + this.event = (IEvent) ctorHandle.invokeWithArguments(finalArgs); + return this; + + } catch (Throwable e) { + throw new RuntimeException("Failed to instantiate event", e); + } + } + + /** + * Start listening for a response event type, chainable with perform(). + */ + public ResponseBuilder onResponse(Class eventClass) { + return new ResponseBuilder<>(this, eventClass); + } + + public static class ResponseBuilder { + private final EventFlow parent; + private final Class responseClass; + + ResponseBuilder(EventFlow parent, Class responseClass) { + this.parent = parent; + this.responseClass = responseClass; + } + + /** Finalize the subscription */ + public EventFlow perform(Consumer action) { + parent.listener = GlobalEventBus.subscribe(responseClass, event -> { + action.accept(responseClass.cast(event)); + if (parent.unsubscribeAfterSuccess && parent.listener != null) { + GlobalEventBus.unsubscribe(parent.listener); + } + }); + return parent; + } + } + + /** + * Subscribe by ID: only fires if UUID matches this publisher's eventId. + */ + public EventFlow onResponse(Class eventClass, Consumer action) { + this.listener = GlobalEventBus.subscribe(eventClass, event -> { + if (event.eventId().equals(this.eventId)) { + action.accept(event); + if (unsubscribeAfterSuccess && listener != null) { + GlobalEventBus.unsubscribe(listener); + } + this.result = event.result(); + } + }); + return this; + } + + /** + * Subscribe by ID without explicit class. + */ + @SuppressWarnings("unchecked") + public EventFlow onResponse(Consumer action) { + this.listener = GlobalEventBus.subscribe(event -> { + if (event instanceof EventWithUuid uuidEvent) { + if (uuidEvent.eventId().equals(this.eventId)) { + try { + TT typedEvent = (TT) uuidEvent; + action.accept(typedEvent); + if (unsubscribeAfterSuccess && listener != null) { + GlobalEventBus.unsubscribe(listener); + } + this.result = typedEvent.result(); + } catch (ClassCastException ignored) {} + } + } + }); + return this; + } + + // choose event type + public EventSubscriberBuilder onEvent(Class eventClass) { + return new EventSubscriberBuilder<>(this, eventClass); + } + + // One-liner shorthand + public EventFlow listen(Class eventClass, Consumer action) { + return this.onEvent(eventClass).perform(action); + } + + // Builder for chaining .onEvent(...).perform(...) + public static class EventSubscriberBuilder { + private final EventFlow publisher; + private final Class eventClass; + + EventSubscriberBuilder(EventFlow publisher, Class eventClass) { + this.publisher = publisher; + this.eventClass = eventClass; + } + + public EventFlow perform(Consumer action) { + publisher.listener = GlobalEventBus.subscribe(eventClass, event -> { + action.accept(eventClass.cast(event)); + if (publisher.unsubscribeAfterSuccess && publisher.listener != null) { + GlobalEventBus.unsubscribe(publisher.listener); + } + }); + return publisher; + } + } + + /** Post synchronously */ + public EventFlow postEvent() { + GlobalEventBus.post(event); + return this; + } + + /** Post asynchronously */ + public EventFlow asyncPostEvent() { + GlobalEventBus.postAsync(event); + return this; + } + + public EventFlow unsubscribeAfterSuccess() { + this.unsubscribeAfterSuccess = true; + return this; + } + + public EventFlow unsubscribeNow() { + if (unsubscribeAfterSuccess && listener != null) { + GlobalEventBus.unsubscribe(listener); + } + return this; + } + + public Map getResult() { + return this.result; + } + + public IEvent getEvent() { + return event; + } + + public String getEventId() { + return eventId; + } +} diff --git a/src/main/java/org/toop/eventbus/EventPublisher.java b/src/main/java/org/toop/eventbus/EventPublisher.java deleted file mode 100644 index 90e844f..0000000 --- a/src/main/java/org/toop/eventbus/EventPublisher.java +++ /dev/null @@ -1,316 +0,0 @@ -package org.toop.eventbus; - -import org.toop.eventbus.events.EventWithUuid; -import org.toop.eventbus.events.IEvent; - -import java.lang.invoke.MethodHandle; -import java.lang.invoke.MethodHandles; -import java.lang.invoke.MethodType; -import java.util.Map; -import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; -import java.util.function.Consumer; - -/** - * EventPublisher is a utility class for creating, posting, and optionally subscribing to events - * in a type-safe and chainable manner. It is designed to work with the {@link GlobalEventBus}. - * - *

This class supports automatic UUID assignment for {@link EventWithUuid} events, - * and allows filtering subscribers so they only respond to events with a specific UUID. - * All subscription methods are chainable, and you can configure automatic unsubscription - * after an event has been successfully handled.

- * - *

Usage patterns:

- * - *

1. Publish an event with optional subscription by UUID:

- *
{@code
- * new EventPublisher<>(StartClient.class, handlerFactory, "127.0.0.1", 5001)
- *     .onEventById(ClientReady.class, clientReadyEvent -> logger.info(clientReadyEvent))
- *     .unsubscribeAfterSuccess()
- *     .postEvent();
- * }
- * - *

2. Subscribe to a specific event type without UUID filtering:

- *
{@code
- * new EventPublisher<>(MyEvent.class)
- *     .onEvent(MyEvent.class, e -> logger.info("Received: " + e))
- *     .postEvent();
- * }
- * - *

3. Subscribe with runtime type inference:

- *
{@code
- * new EventPublisher<>((MyEvent e) -> logger.info("Received: " + e))
- *     .postEvent();
- * }
- * - *

Notes:

- *
    - *
  • For events extending {@link EventWithUuid}, a UUID is automatically generated - * and passed to the event constructor if none is provided.
  • - *
  • Listeners registered via {@code onEventById} will only be triggered - * if the event's UUID matches this publisher's UUID.
  • - *
  • Listeners can be unsubscribed automatically after the first successful trigger - * using {@link #unsubscribeAfterSuccess()}.
  • - *
  • All subscription and posting methods are chainable for fluent API usage.
  • - *
- * - * @param the type of event to publish; must implement {@link IEvent} - */ -public class EventPublisher { - - - /** Lookup object used for dynamically invoking constructors via MethodHandles. */ - private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup(); - - /** Cache of constructor handles for event classes to avoid repeated reflection lookups. */ - private static final Map, MethodHandle> CONSTRUCTOR_CACHE = new ConcurrentHashMap<>(); - - /** Automatically assigned UUID for {@link EventWithUuid} events. */ - private String eventId = null; - - /** The event instance created by this publisher. */ - private T event = null; - - /** The listener returned by GlobalEventBus subscription. Used for unsubscription. */ - private Object listener; - - /** Flag indicating whether to automatically unsubscribe the listener after success. */ - private boolean unsubscribeAfterSuccess = false; - - /** Holds the results returned from the subscribed event, if any. */ - private Map result = null; - - /** - * Constructs a new EventPublisher by instantiating the given event class. - * For {@link EventWithUuid} events, a UUID is automatically generated and passed as - * the last constructor argument if not explicitly provided. - * - * @param postEventClass the class of the event to instantiate - * @param args constructor arguments for the event (UUID may be excluded) - * @throws RuntimeException if instantiation fails - */ - public EventPublisher(Class postEventClass, Object... args) { - try { - boolean isUuidEvent = EventWithUuid.class.isAssignableFrom(postEventClass); - - MethodHandle ctorHandle = CONSTRUCTOR_CACHE.computeIfAbsent(postEventClass, cls -> { - try { - Class[] paramTypes = cls.getDeclaredConstructors()[0].getParameterTypes(); - MethodType mt = MethodType.methodType(void.class, paramTypes); - return LOOKUP.findConstructor(cls, mt); - } catch (Exception e) { - throw new RuntimeException("Failed to find constructor handle for " + cls, e); - } - }); - - Object[] finalArgs; - int expectedParamCount = ctorHandle.type().parameterCount(); - if (isUuidEvent && args.length < expectedParamCount) { - this.eventId = UUID.randomUUID().toString(); - finalArgs = new Object[args.length + 1]; - System.arraycopy(args, 0, finalArgs, 0, args.length); - finalArgs[args.length] = this.eventId; - } else if (isUuidEvent) { - this.eventId = (String) args[args.length - 1]; - finalArgs = args; - } else { - finalArgs = args; - } - - @SuppressWarnings("unchecked") - T instance = (T) ctorHandle.invokeWithArguments(finalArgs); - this.event = instance; - } catch (Throwable e) { - throw new RuntimeException("Failed to instantiate event", e); - } - } - - /** - * Creates a new EventPublisher and immediately subscribes a listener for the event class. - * - * @param eventClass the class of the event to subscribe to - * @param action the action to execute when an event of the given class is received - */ - public EventPublisher(Class eventClass, Consumer action) { - this.onEvent(eventClass, action); - } - - /** - * Creates a new EventPublisher and immediately subscribes a listener using runtime type inference. - * The event type is inferred at runtime. Wrong type casts are ignored silently. - * - * @param action the action to execute when a matching event is received - */ - public EventPublisher(Consumer action) { - this.onEvent(action); - } - - /** - * Subscribes a listener for a specific {@link EventWithUuid} event type. - * The listener is only triggered if the event UUID matches this publisher's UUID. - * - * @param eventClass the class of the event to subscribe to - * @param action the action to execute on a matching event - * @param type of event; must extend EventWithUuid - * @return this EventPublisher for chainable calls - */ - public EventPublisher onEventById( - Class eventClass, Consumer action) { - - this.listener = GlobalEventBus.subscribe(eventClass, event -> { - if (event.eventId().equals(this.eventId)) { - action.accept(event); - - if (unsubscribeAfterSuccess && listener != null) { - GlobalEventBus.unsubscribe(listener); - } - - this.result = event.result(); - } - }); - - return this; - } - - /** - * Subscribes a listener for {@link EventWithUuid} events without specifying class explicitly. - * Only triggers for events whose UUID matches this publisher's UUID. - * - * @param action the action to execute on a matching event - * @param type of event; must extend EventWithUuid - * @return this EventPublisher for chainable calls - */ - @SuppressWarnings("unchecked") - public EventPublisher onEventById(Consumer action) { - - this.listener = GlobalEventBus.subscribe(event -> { - if (event instanceof EventWithUuid uuidEvent) { - if (uuidEvent.eventId().equals(this.eventId)) { - try { - TT typedEvent = (TT) uuidEvent; - action.accept(typedEvent); - - if (unsubscribeAfterSuccess && listener != null) { - GlobalEventBus.unsubscribe(listener); - } - - this.result = typedEvent.result(); - } catch (ClassCastException ignored) {} - } - } - }); - - return this; - } - - /** - * Subscribes a listener for a specific event type without UUID filtering. - * - * @param eventClass the class of the event to subscribe to - * @param action the action to execute on the event - * @param type of event; must implement IEvent - * @return this EventPublisher for chainable calls - */ - public EventPublisher onEvent(Class eventClass, Consumer action) { - this.listener = GlobalEventBus.subscribe(eventClass, event -> { - action.accept(eventClass.cast(event)); - - if (unsubscribeAfterSuccess && listener != null) { - GlobalEventBus.unsubscribe(listener); - } - }); - return this; - } - - /** - * Subscribes a listener using runtime type inference. Wrong type casts are ignored silently. - * - * @param action the action to execute when a matching event is received - * @param type of event (inferred at runtime) - * @return this EventPublisher for chainable calls - */ - @SuppressWarnings("unchecked") - public EventPublisher onEvent(Consumer action) { - this.listener = GlobalEventBus.subscribe(event -> { - try { - TT typedEvent = (TT) event; - action.accept(typedEvent); - - if (unsubscribeAfterSuccess && listener != null) { - GlobalEventBus.unsubscribe(listener); - } - } catch (ClassCastException ignored) {} - }); - return this; - } - - /** - * Posts the event synchronously to {@link GlobalEventBus}. - * - * @return this EventPublisher for chainable calls - */ - public EventPublisher postEvent() { - GlobalEventBus.post(event); - return this; - } - - /** - * Posts the event asynchronously to {@link GlobalEventBus}. - * - * @return this EventPublisher for chainable calls - */ - public EventPublisher asyncPostEvent() { - GlobalEventBus.postAsync(event); - return this; - } - - /** - * Configures automatic unsubscription for listeners registered via onEventById - * after a successful trigger. - * - * @return this EventPublisher for chainable calls - */ - public EventPublisher unsubscribeAfterSuccess() { - this.unsubscribeAfterSuccess = true; - return this; - } - - /** - * Immediately unsubscribes the listener, if set. - * - * @return this EventPublisher for chainable calls - */ - public EventPublisher unsubscribeNow() { - if (unsubscribeAfterSuccess && listener != null) { - GlobalEventBus.unsubscribe(listener); - } - return this; - } - - /** - * Returns the results provided by the triggered event, if any. - * - * @return map of results, or null if none - */ - public Map getResult() { - return this.result; - } - - /** - * Returns the event instance created by this publisher. - * - * @return the event instance - */ - public T getEvent() { - return event; - } - - /** - * Returns the automatically assigned UUID for {@link EventWithUuid} events. - * - * @return the UUID string, or null for non-UUID events - */ - public String getEventId() { - return eventId; - } -} \ No newline at end of file