This commit is contained in:
ramollia
2025-09-24 15:44:38 +02:00
parent f80c565a32
commit 9fdd74326a
47 changed files with 139 additions and 286 deletions

View File

@@ -0,0 +1,202 @@
package org.toop.framework;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.core.LoggerContext;
import org.apache.logging.log4j.core.config.Configuration;
import org.apache.logging.log4j.core.config.LoggerConfig;
/**
* Utility class for configuring logging levels dynamically at runtime using Log4j 2.
*
* <p>Provides methods to enable or disable logs globally or per class, with support for specifying
* log levels either via {@link Level} enums or string names.
*/
// Todo: refactor
public final class Logging {
/** Disables all logging globally by setting the root logger level to {@link Level#OFF}. */
public static void disableAllLogs() {
LoggerContext ctx = (LoggerContext) LogManager.getContext(false);
Configuration config = ctx.getConfiguration();
LoggerConfig rootLoggerConfig = config.getRootLogger();
rootLoggerConfig.setLevel(Level.OFF);
ctx.updateLoggers();
}
/** Enables all logging globally by setting the root logger level to {@link Level#ALL}. */
public static void enableAllLogs() {
LoggerContext ctx = (LoggerContext) LogManager.getContext(false);
Configuration config = ctx.getConfiguration();
LoggerConfig rootLoggerConfig = config.getRootLogger();
rootLoggerConfig.setLevel(Level.ALL);
ctx.updateLoggers();
}
/**
* Enables global logging at a specific level by setting the root logger.
*
* @param level the logging level to enable for all logs
*/
public static void enableAllLogs(Level level) {
LoggerContext ctx = (LoggerContext) LogManager.getContext(false);
Configuration config = ctx.getConfiguration();
LoggerConfig rootLoggerConfig = config.getRootLogger();
rootLoggerConfig.setLevel(level);
ctx.updateLoggers();
}
/**
* Verifies whether the provided string corresponds to a valid class name.
*
* @param className fully-qualified class name to check
* @return true if the class exists, false otherwise
*/
private static boolean verifyStringIsActualClass(String className) {
try {
Class.forName(className);
return true;
} catch (ClassNotFoundException e) {
return false;
}
}
/**
* Internal helper to disable logs for a specific class by name.
*
* @param className fully-qualified class name
*/
private static void disableLogsForClassInternal(String className) {
LoggerContext ctx = (LoggerContext) LogManager.getContext(false);
Configuration config = ctx.getConfiguration();
config.removeLogger(className);
LoggerConfig specificConfig = new LoggerConfig(className, Level.OFF, false);
config.addLogger(className, specificConfig);
ctx.updateLoggers();
}
/**
* Disables logs for a specific class.
*
* @param class_ the class for which logs should be disabled
* @param <T> type of the class
*/
public static <T> void disableLogsForClass(Class<T> class_) {
disableLogsForClassInternal(class_.getName());
}
/**
* Disables logs for a class specified by fully-qualified name, if the class exists.
*
* @param className fully-qualified class name
*/
public static void disableLogsForClass(String className) {
if (verifyStringIsActualClass(className)) {
disableLogsForClassInternal(className);
}
}
/**
* Internal helper to enable logs for a specific class at a specific level.
*
* @param className fully-qualified class name
* @param level logging level to set
*/
private static void enableLogsForClassInternal(String className, Level level) {
LoggerContext ctx = (LoggerContext) LogManager.getContext(false);
Configuration config = ctx.getConfiguration();
LoggerConfig loggerConfig = config.getLoggers().get(className);
if (loggerConfig == null) {
loggerConfig = new LoggerConfig(className, level, false);
config.addLogger(className, loggerConfig);
} else {
loggerConfig.setLevel(level);
}
ctx.updateLoggers();
}
/**
* Enables logging for a class at a specific level.
*
* @param class_ class to configure
* @param levelToLog the logging level to set
* @param <T> type of the class
*/
public static <T> void enableLogsForClass(Class<T> class_, Level levelToLog) {
enableLogsForClassInternal(class_.getName(), levelToLog);
}
/**
* Enables logging for a class specified by name at a specific level, if the class exists.
*
* @param className fully-qualified class name
* @param levelToLog the logging level to set
*/
public static void enableLogsForClass(String className, Level levelToLog) {
if (verifyStringIsActualClass(className)) {
enableLogsForClassInternal(className, levelToLog);
}
}
/**
* Enables logging for a class specified by name at a specific level using a string.
*
* @param className fully-qualified class name
* @param levelToLog name of the logging level (e.g., "DEBUG", "INFO")
*/
public static void enableLogsForClass(String className, String levelToLog) {
Level level = Level.valueOf(levelToLog.trim().toUpperCase());
if (level != null && verifyStringIsActualClass(className)) {
enableLogsForClassInternal(className, level);
}
}
/** Convenience methods for enabling logs at specific levels for classes. */
public static <T> void enableAllLogsForClass(Class<T> class_) {
enableLogsForClass(class_, Level.ALL);
}
public static void enableAllLogsForClass(String className) {
enableLogsForClass(className, Level.ALL);
}
public static <T> void enableDebugLogsForClass(Class<T> class_) {
enableLogsForClass(class_, Level.DEBUG);
}
public static void enableDebugLogsForClass(String className) {
enableLogsForClass(className, Level.DEBUG);
}
public static <T> void enableErrorLogsForClass(Class<T> class_) {
enableLogsForClass(class_, Level.ERROR);
}
public static void enableErrorLogsForClass(String className) {
enableLogsForClass(className, Level.ERROR);
}
public static <T> void enableFatalLogsForClass(Class<T> class_) {
enableLogsForClass(class_, Level.FATAL);
}
public static void enableFatalLogsForClass(String className) {
enableLogsForClass(className, Level.FATAL);
}
public static <T> void enableInfoLogsForClass(Class<T> class_) {
enableLogsForClass(class_, Level.INFO);
}
public static void enableInfoLogsForClass(String className) {
enableLogsForClass(className, Level.INFO);
}
public static <T> void enableTraceLogsForClass(Class<T> class_) {
enableLogsForClass(class_, Level.TRACE);
}
public static void enableTraceLogsForClass(String className) {
enableLogsForClass(className, Level.TRACE);
}
}

View File

@@ -0,0 +1,221 @@
package org.toop.framework.eventbus;
import org.toop.framework.eventbus.events.EventWithUuid;
import org.toop.framework.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}.
*
* <p>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.</p>
*/
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<Class<?>, 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<String, Object> 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 <T extends IEvent> EventFlow addPostEvent(Class<T> 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 <TT extends IEvent> ResponseBuilder<TT> onResponse(Class<TT> eventClass) {
return new ResponseBuilder<>(this, eventClass);
}
public static class ResponseBuilder<R extends IEvent> {
private final EventFlow parent;
private final Class<R> responseClass;
ResponseBuilder(EventFlow parent, Class<R> responseClass) {
this.parent = parent;
this.responseClass = responseClass;
}
/** Finalize the subscription */
public EventFlow perform(Consumer<R> 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 <TT extends EventWithUuid> EventFlow onResponse(Class<TT> eventClass, Consumer<TT> 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 <TT extends EventWithUuid> EventFlow onResponse(Consumer<TT> 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 <TT extends IEvent> EventSubscriberBuilder<TT> onEvent(Class<TT> eventClass) {
return new EventSubscriberBuilder<>(this, eventClass);
}
// One-liner shorthand
public <TT extends IEvent> EventFlow listen(Class<TT> eventClass, Consumer<TT> action) {
return this.onEvent(eventClass).perform(action);
}
// Builder for chaining .onEvent(...).perform(...)
public static class EventSubscriberBuilder<TT extends IEvent> {
private final EventFlow publisher;
private final Class<TT> eventClass;
EventSubscriberBuilder(EventFlow publisher, Class<TT> eventClass) {
this.publisher = publisher;
this.eventClass = eventClass;
}
public EventFlow perform(Consumer<TT> 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<String, Object> getResult() {
return this.result;
}
public IEvent getEvent() {
return event;
}
public String getEventId() {
return eventId;
}
}

View File

@@ -0,0 +1,203 @@
package org.toop.framework.eventbus;
import org.toop.framework.eventbus.events.EventWithUuid;
import org.toop.framework.eventbus.events.IEvent;
import java.util.Map;
import java.util.concurrent.*;
import java.util.function.Consumer;
/**
* GlobalEventBus is a high-throughput, thread-safe event bus for publishing and subscribing
* to events within the application.
*
* <p>It supports:</p>
* <ul>
* <li>Type-specific subscriptions via {@link #subscribe(Class, Consumer)}</li>
* <li>UUID-specific subscriptions via {@link #subscribeById(Class, String, Consumer)}</li>
* <li>Asynchronous posting of events with automatic queueing and fallback</li>
* </ul>
*
* <p><b>Performance note:</b> Directly using {@link GlobalEventBus} is possible,
* but for safer type handling, automatic UUID management, and easier unsubscription,
* it is recommended to use {@link EventPublisher} whenever possible.</p>
*
* <p>The bus maintains a fixed pool of worker threads that continuously process queued events.</p>
*/
public final class GlobalEventBus {
/** Number of worker threads, set to the number of available CPU cores. */
private static final int WORKERS = Runtime.getRuntime().availableProcessors();
/** Queue for asynchronous event processing. */
private static final BlockingQueue<IEvent> EVENT_QUEUE = new LinkedBlockingQueue<>(WORKERS * 1024);
/** Map of event class to type-specific listeners. */
private static final Map<Class<?>, CopyOnWriteArrayList<Consumer<? super IEvent>>> LISTENERS = new ConcurrentHashMap<>();
/** Map of event class to UUID-specific listeners. */
private static final Map<Class<?>, ConcurrentHashMap<String, Consumer<? extends EventWithUuid>>> UUID_LISTENERS = new ConcurrentHashMap<>();
/** Thread pool for worker threads processing queued events. */
private static final ExecutorService WORKER_POOL = Executors.newFixedThreadPool(WORKERS, r -> {
Thread t = new Thread(r, "EventBus-Worker-" + r.hashCode());
t.setDaemon(true);
return t;
});
// Initialize worker threads
static {
for (int i = 0; i < WORKERS; i++) {
WORKER_POOL.submit(GlobalEventBus::workerLoop);
}
}
/** Private constructor to prevent instantiation. */
private GlobalEventBus() {}
/** Continuously processes events from the queue and dispatches them to listeners. */
private static void workerLoop() {
try {
while (true) {
IEvent event = EVENT_QUEUE.take();
dispatchEvent(event);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
/**
* Subscribes a type-specific listener for all events of a given class.
*
* @param eventClass the class of events to subscribe to
* @param listener the action to execute when the event is posted
* @param <T> the event type
* @return the provided listener for possible unsubscription
*/
public static <T extends IEvent> Consumer<T> subscribe(Class<T> eventClass, Consumer<T> listener) {
CopyOnWriteArrayList<Consumer<? super IEvent>> list =
LISTENERS.computeIfAbsent(eventClass, k -> new CopyOnWriteArrayList<>());
list.add(event -> listener.accept(eventClass.cast(event)));
return listener;
}
/**
* Subscribes a generic listener for all events (no type filtering).
*
* @param listener the action to execute on any event
* @return the provided listener for possible unsubscription
*/
public static Consumer<Object> subscribe(Consumer<Object> listener) {
LISTENERS.computeIfAbsent(Object.class, _ -> new CopyOnWriteArrayList<>())
.add(listener);
return listener;
}
/**
* Subscribes a listener for a specific {@link EventWithUuid} identified by its UUID.
*
* @param eventClass the class of the UUID event
* @param eventId the UUID of the event to listen for
* @param listener the action to execute when the event with the matching UUID is posted
* @param <T> the event type extending EventWithUuid
*/
public static <T extends EventWithUuid> void subscribeById(Class<T> eventClass, String eventId, Consumer<T> listener) {
UUID_LISTENERS
.computeIfAbsent(eventClass, _ -> new ConcurrentHashMap<>())
.put(eventId, listener);
}
/**
* Unsubscribes a previously registered listener.
*
* @param listener the listener to remove
*/
public static void unsubscribe(Object listener) {
LISTENERS.values().forEach(list -> list.remove(listener));
}
/**
* Unsubscribes a UUID-specific listener.
*
* @param eventClass the class of the UUID event
* @param eventId the UUID of the listener to remove
* @param <T> the event type extending EventWithUuid
*/
public static <T extends EventWithUuid> void unsubscribeById(Class<T> eventClass, String eventId) {
Map<String, Consumer<? extends EventWithUuid>> map = UUID_LISTENERS.get(eventClass);
if (map != null) map.remove(eventId);
}
/**
* Posts an event synchronously to all subscribed listeners.
*
* @param event the event instance to post
* @param <T> the event type
*/
public static <T extends IEvent> void post(T event) {
dispatchEvent(event);
}
/**
* Posts an event asynchronously by adding it to the internal queue.
* If the queue is full, the event is dispatched synchronously.
*
* @param event the event instance to post
* @param <T> the event type
*/
public static <T extends IEvent> void postAsync(T event) {
if (!EVENT_QUEUE.offer(event)) {
dispatchEvent(event);
}
}
/** Dispatches an event to all type-specific, generic, and UUID-specific listeners. */
@SuppressWarnings("unchecked")
private static void dispatchEvent(IEvent event) {
Class<?> clazz = event.getClass();
CopyOnWriteArrayList<Consumer<? super IEvent>> classListeners = LISTENERS.get(clazz);
if (classListeners != null) {
for (Consumer<? super IEvent> listener : classListeners) {
try { listener.accept(event); } catch (Throwable ignored) {}
}
}
CopyOnWriteArrayList<Consumer<? super IEvent>> genericListeners = LISTENERS.get(Object.class);
if (genericListeners != null) {
for (Consumer<? super IEvent> listener : genericListeners) {
try { listener.accept(event); } catch (Throwable ignored) {}
}
}
if (event instanceof EventWithUuid uuidEvent) {
Map<String, Consumer<? extends EventWithUuid>> map = UUID_LISTENERS.get(clazz);
if (map != null) {
Consumer<EventWithUuid> listener = (Consumer<EventWithUuid>) map.remove(uuidEvent.eventId());
if (listener != null) {
try { listener.accept(uuidEvent); } catch (Throwable ignored) {}
}
}
}
}
/**
* Shuts down the bus immediately, clearing all listeners and queued events.
* Worker threads are stopped.
*/
public static void shutdown() {
WORKER_POOL.shutdownNow();
LISTENERS.clear();
UUID_LISTENERS.clear();
EVENT_QUEUE.clear();
}
/**
* Clears all listeners and UUID-specific subscriptions without stopping worker threads.
*/
public static void reset() {
LISTENERS.clear();
UUID_LISTENERS.clear();
}
}

View File

@@ -0,0 +1,8 @@
package org.toop.framework.eventbus.events;
import java.util.Map;
public interface EventWithUuid extends IEvent {
Map<String, Object> result();
String eventId();
}

View File

@@ -0,0 +1,184 @@
package org.toop.framework.eventbus.events;
import java.lang.reflect.Constructor;
import java.util.Arrays;
import java.util.concurrent.CompletableFuture;
/** Events that are used in the GlobalEventBus class. */
public class Events implements IEvent {
/**
* WIP, DO NOT USE!
*
* @param eventName
* @param args
* @return
* @throws Exception
*/
public static Object get(String eventName, Object... args) throws Exception {
Class<?> clazz = Class.forName("org.toop.eventbus.events.Events$ServerEvents$" + eventName);
Class<?>[] paramTypes = Arrays.stream(args).map(Object::getClass).toArray(Class<?>[]::new);
Constructor<?> constructor = clazz.getConstructor(paramTypes);
return constructor.newInstance(args);
}
/**
* WIP, DO NOT USE!
*
* @param eventCategory
* @param eventName
* @param args
* @return
* @throws Exception
*/
public static Object get(String eventCategory, String eventName, Object... args)
throws Exception {
Class<?> clazz =
Class.forName("org.toop.eventbus.events.Events$" + eventCategory + "$" + eventName);
Class<?>[] paramTypes = Arrays.stream(args).map(Object::getClass).toArray(Class<?>[]::new);
Constructor<?> constructor = clazz.getConstructor(paramTypes);
return constructor.newInstance(args);
}
/**
* WIP, DO NOT USE!
*
* @param eventName
* @param args
* @return
* @throws Exception
*/
public static Object get2(String eventName, Object... args) throws Exception {
// Fully qualified class name
String className = "org.toop.server.backend.Events$ServerEvents$" + eventName;
// Load the class
Class<?> clazz = Class.forName(className);
// Build array of argument types
Class<?>[] paramTypes = new Class[args.length];
for (int i = 0; i < args.length; i++) {
paramTypes[i] = args[i].getClass();
}
// Get the constructor
Constructor<?> constructor = clazz.getConstructor(paramTypes);
// Create a new instance
return constructor.newInstance(args);
}
public static class ServerEvents {
/**
* BLOCKING Requests all active servers. The result is returned via the provided
* CompletableFuture.
*
* @param future List of all servers in string form.
*/
public record RequestsAllServers(CompletableFuture<String> future) {}
/** Forces closing all active servers immediately. */
public record ForceCloseAllServers() implements IEvent {}
/**
* Requests starting a server with a specific port and game type.
*
* @param port The port to open the server.
* @param gameType Either "tictactoe" or ...
*/
public record StartServer(int port, String gameType) implements IEvent {}
/**
* BLOCKING Requests starting a server with a specific port and game type, and returns a
* CompletableFuture that completes when the server has started.
*
* @param port The port to open the server.
* @param gameType Either "tictactoe" or ...
* @param future The uuid of the server.
*/
public record StartServerRequest(
int port, String gameType, CompletableFuture<String> future) implements IEvent{}
/**
* Represents a server that has successfully started.
*
* @param uuid The unique identifier of the server.
* @param port The port the server is listening on.
*/
public record ServerStarted(String uuid, int port) implements IEvent {}
/**
* BLOCKING Requests creation of a TicTacToe game on a specific server.
*
* @param serverUuid The unique identifier of the server where the game will be created.
* @param playerA The name of the first player.
* @param playerB The name of the second player.
* @param future The game UUID when the game is created.
*/
public record CreateTicTacToeGameRequest(
String serverUuid,
String playerA,
String playerB,
CompletableFuture<String> future) implements IEvent {}
/**
* Requests running a TicTacToe game on a specific server.
*
* @param serverUuid The unique identifier of the server.
* @param gameUuid The UUID of the game to run.
*/
public record RunTicTacToeGame(String serverUuid, String gameUuid) implements IEvent {}
/**
* Requests ending a TicTacToe game on a specific server.
*
* @param serverUuid The UUID of the server the game is running on.
* @param gameUuid The UUID of the game to end.
*/
public record EndTicTacToeGame(String serverUuid, String gameUuid) implements IEvent {}
// public record StartGameConnectionRequest(String ip, String port,
// CompletableFuture<String> future) {}
/**
* Triggers on changing the server IP.
*
* @param ip The new IP address.
*/
public record OnChangingServerIp(String ip) {}
/**
* Triggers on changing the server port.
*
* @param port The new port.
*/
public record OnChangingServerPort(int port) {}
/** Triggers when a cell is clicked in one of the game boards. */
public record CellClicked(int cell) {}
}
public static class EventBusEvents {}
public static class WindowEvents {
/** Triggers when the window wants to quit. */
public record OnQuitRequested() implements IEvent {}
/** Triggers when the window is resized. */
// public record OnResize(Window.Size size) {}
/** Triggers when the mouse is moved within the window. */
public record OnMouseMove(int x, int y) implements IEvent {}
/** Triggers when the mouse is clicked within the window. */
public record OnMouseClick(int button) {}
/** Triggers when the mouse is released within the window. */
public record OnMouseRelease(int button) {}
}
public static class TttEvents {}
public static class AiTttEvents {}
}

View File

@@ -0,0 +1,3 @@
package org.toop.framework.eventbus.events;
public interface IEvent {}

View File

@@ -0,0 +1,189 @@
package org.toop.framework.eventbus.events;
import org.toop.backend.tictactoe.TicTacToeServer;
import org.toop.framework.networking.NetworkingGameClientHandler;
import java.lang.reflect.RecordComponent;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class NetworkEvents extends Events {
/**
* BLOCKING Requests all active connections. The result is returned via the provided
* CompletableFuture.
*
* @param future List of all connections in string form.
*/
public record RequestsAllClients(CompletableFuture<String> future) implements IEvent {}
/** Forces closing all active connections immediately. */
public record ForceCloseAllClients() implements IEvent {}
public record CloseClientRequest(CompletableFuture<String> future) {}
public record CloseClient(String connectionId) implements IEvent {}
/**
* Event to start a new client connection to a server.
* <p>
* This event is typically posted to the {@code GlobalEventBus} to initiate the creation of
* a client connection, and carries all information needed to establish that connection:
* <br>
* - A factory for creating the Netty handler that will manage the connection
* <br>
* - The server's IP address and port
* <br>
* - A unique event identifier for correlation with follow-up events
* </p>
*
* <p>
* The {@link #eventId()} allows callers to correlate the {@code StartClient} event
* with subsequent success/failure events. For example, a {@code StartClientSuccess}
* or {@code StartClientFailure} event may carry the same {@code eventId}.
* </p>
*
* @param handlerFactory Factory for constructing a {@link NetworkingGameClientHandler}.
* @param ip The IP address of the server to connect to.
* @param port The port number of the server to connect to.
* @param eventId A unique identifier for this event, typically injected
* automatically by the {@link org.toop.eventbus.EventPublisher}.
*/
public record StartClient(
Supplier<? extends NetworkingGameClientHandler> handlerFactory,
String ip,
int port,
String eventId
) implements EventWithUuid {
/**
* Returns a map representation of this event, where keys are record component names
* and values are their corresponding values. Useful for generic logging, debugging,
* or serializing events without hardcoding field names.
*
* @return a {@code Map<String, Object>} containing field names and values
*/
@Override
public Map<String, Object> result() {
return Stream.of(this.getClass().getRecordComponents())
.collect(Collectors.toMap(
RecordComponent::getName,
rc -> {
try {
return rc.getAccessor().invoke(this);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
));
}
/**
* Returns the unique event identifier used for correlating this event.
*
* @return the event ID string
*/
@Override
public String eventId() {
return this.eventId;
}
}
/**
* TODO: Update docs new input.
* BLOCKING Triggers starting a server connection and returns a future.
*
* @param ip The IP address of the server to connect to.
* @param port The port of the server to connect to.
* @param future Returns the UUID of the connection, when connection is established.
*/
public record StartClientRequest(
Supplier<? extends NetworkingGameClientHandler> handlerFactory,
String ip, int port, CompletableFuture<String> future) implements IEvent {}
/**
*
* @param clientId The ID of the client to be used in requests.
* @param eventId The eventID used in checking if event is for you.
*/
public record StartClientSuccess(String clientId, String eventId)
implements EventWithUuid {
@Override
public Map<String, Object> result() {
return Stream.of(this.getClass().getRecordComponents())
.collect(Collectors.toMap(
RecordComponent::getName,
rc -> {
try {
return rc.getAccessor().invoke(this);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
));
}
@Override
public String eventId() {
return this.eventId;
}
}
/**
* Triggers sending a command to a server.
*
* @param connectionId The UUID of the connection to send the command on.
* @param args The command arguments.
*/
public record SendCommand(String connectionId, String... args) implements IEvent {}
/**
* WIP Triggers when a command is sent to a server.
*
* @param command The TicTacToeServer instance that executed the command.
* @param args The command arguments.
* @param result The result returned from executing the command.
*/
public record OnCommand(
TicTacToeServer command, String[] args, String result) {} // TODO old
/**
* Triggers reconnecting to a previous address.
*
* @param connectionId The identifier of the connection being reconnected.
*/
public record Reconnect(Object connectionId) {}
/**
* Triggers when the server client receives a message.
*
* @param ConnectionUuid The UUID of the connection that received the message.
* @param message The message received.
*/
public record ReceivedMessage(String ConnectionUuid, String message) implements IEvent {}
/**
* Triggers changing connection to a new address.
*
* @param connectionId The identifier of the connection being changed.
* @param ip The new IP address.
* @param port The new port.
*/
public record ChangeClient(Object connectionId, String ip, int port) {}
/**
* Triggers when the server couldn't connect to the desired address.
*
* @param connectionId The identifier of the connection that failed.
*/
public record CouldNotConnect(Object connectionId) {}
/** WIP Triggers when a connection closes. */
public record ClosedConnection() {}
}

View File

@@ -0,0 +1,4 @@
package org.toop.framework.eventbus.events;
public class ServerEvents {
}

View File

@@ -0,0 +1,140 @@
package org.toop.framework.networking;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioIoHandler;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.LineBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.util.function.Supplier;
public class NetworkingClient {
private static final Logger logger = LogManager.getLogger(NetworkingClient.class);
final Bootstrap bootstrap = new Bootstrap();
final EventLoopGroup workerGroup = new MultiThreadIoEventLoopGroup(NioIoHandler.newFactory());
private String connectionUuid;
private Channel channel;
private NetworkingGameClientHandler handler;
public NetworkingClient(
Supplier<? extends NetworkingGameClientHandler> handlerFactory,
String host,
int port) {
try {
this.bootstrap.group(this.workerGroup);
this.bootstrap.channel(NioSocketChannel.class);
this.bootstrap.option(ChannelOption.SO_KEEPALIVE, true);
this.bootstrap.handler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) {
handler = handlerFactory.get();
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new LineBasedFrameDecoder(1024)); // split at \n
pipeline.addLast(new StringDecoder()); // bytes -> String
pipeline.addLast(handler);
}
});
ChannelFuture channelFuture = this.bootstrap.connect(host, port).sync();
this.channel = channelFuture.channel();
} catch (Exception e) {
logger.error("Failed to create networking client instance", e);
}
}
public NetworkingGameClientHandler getHandler() {
return handler;
}
public void setConnectionUuid(String connectionUuid) {
this.connectionUuid = connectionUuid;
}
public boolean isChannelActive() {
return this.channel != null && this.channel.isActive();
}
public void writeAndFlush(String msg) {
String literalMsg = msg.replace("\n", "\\n").replace("\r", "\\r");
if (isChannelActive()) {
this.channel.writeAndFlush(msg);
logger.info("Connection {} sent message: {}", this.channel.remoteAddress(), literalMsg);
} else {
logger.warn("Cannot send message: {}, connection inactive.", literalMsg);
}
}
public void writeAndFlushnl(String msg) {
if (isChannelActive()) {
this.channel.writeAndFlush(msg + "\n");
logger.info("Connection {} sent message: {}", this.channel.remoteAddress(), msg);
} else {
logger.warn("Cannot send message: {}, connection inactive.", msg);
}
}
public void login(String username) {
this.writeAndFlush("login " + username + "\n");
}
public void logout() {
this.writeAndFlush("logout\n");
}
public void sendMove(int move) {
this.writeAndFlush("move " + move + "\n"); // append \n so server receives a full line
}
public void getGamelist() {
this.writeAndFlush("get gamelist\n");
}
public void getPlayerlist() {
this.writeAndFlush("get playerlist\n");
}
public void subscribe(String gameType) {
this.writeAndFlush("subscribe " + gameType + "\n");
}
public void forfeit() {
this.writeAndFlush("forfeit\n");
}
public void challenge(String playerName, String gameType) {
this.writeAndFlush("challenge " + playerName + " " + gameType + "\n");
}
public void acceptChallenge(String challengeNumber) {
this.writeAndFlush("challenge accept " + challengeNumber + "\n");
}
public void sendChatMessage(String message) {
this.writeAndFlush("message " + "\"" + message + "\"" + "\n");
}
public void help(String command) {
this.writeAndFlush("help " + command + "\n");
}
public void closeConnection() {
if (this.channel != null && this.channel.isActive()) {
this.channel.close().addListener(future -> {
if (future.isSuccess()) {
logger.info("Connection {} closed successfully", this.channel.remoteAddress());
} else {
logger.error("Error closing connection {}. Error: {}",
this.channel.remoteAddress(),
future.cause().getMessage());
}
});
}
}
}

View File

@@ -0,0 +1,121 @@
package org.toop.framework.networking;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Supplier;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.toop.eventbus.EventPublisher;
import org.toop.framework.eventbus.events.NetworkEvents;
public class NetworkingClientManager {
private static final Logger logger = LogManager.getLogger(NetworkingClientManager.class);
/** Map of serverId -> Server instances */
private final Map<String, NetworkingClient> networkClients = new ConcurrentHashMap<>();
/** Starts a connection manager, to manage, connections. */
public NetworkingClientManager() {
new EventPublisher<>(NetworkEvents.StartClientRequest.class, this::handleStartClientRequest);
new EventPublisher<>(NetworkEvents.StartClient.class, this::handleStartClient);
new EventPublisher<>(NetworkEvents.SendCommand.class, this::handleCommand);
new EventPublisher<>(NetworkEvents.CloseClient.class, this::handleCloseClient);
new EventPublisher<>(NetworkEvents.RequestsAllClients.class, this::getAllConnections);
new EventPublisher<>(NetworkEvents.ForceCloseAllClients.class, this::shutdownAll);
}
private String startClientRequest(Supplier<? extends NetworkingGameClientHandler> handlerFactory,
String ip,
int port) {
String connectionUuid = UUID.randomUUID().toString();
try {
NetworkingClient client = new NetworkingClient(
handlerFactory,
ip,
port);
this.networkClients.put(connectionUuid, client);
} catch (Exception e) {
logger.error(e);
}
logger.info("Client {} started", connectionUuid);
return connectionUuid;
}
private void handleStartClientRequest(NetworkEvents.StartClientRequest request) {
request.future()
.complete(
this.startClientRequest(
request.handlerFactory(),
request.ip(),
request.port())); // TODO: Maybe post ConnectionEstablished event.
}
private void handleStartClient(NetworkEvents.StartClient event) {
String uuid = this.startClientRequest(event.handlerFactory(), event.ip(), event.port());
new EventPublisher<>(NetworkEvents.StartClientSuccess.class,
uuid, event.eventId()
).asyncPostEvent();
}
private void handleCommand(
NetworkEvents.SendCommand
event) { // TODO: Move this to ServerConnection class, keep it internal.
NetworkingClient client = this.networkClients.get(event.connectionId());
logger.info("Preparing to send command: {} to server: {}", event.args(), client);
if (client != null) {
String args = String.join(" ", event.args()) + "\n";
client.writeAndFlush(args);
} else {
logger.warn("Server {} not found for command '{}'", event.connectionId(), event.args());
}
}
private void handleCloseClient(NetworkEvents.CloseClient event) {
NetworkingClient client = this.networkClients.get(event.connectionId());
client.closeConnection(); // TODO: Check if not blocking, what if error, mb not remove?
this.networkClients.remove(event.connectionId());
logger.info("Client {} closed successfully.", event.connectionId());
}
// private void handleReconnect(Events.ServerEvents.Reconnect event) {
// NetworkingClient client = this.networkClients.get(event.connectionId());
// if (client != null) {
// try {
// client;
// logger.info("Server {} reconnected", event.connectionId());
// } catch (Exception e) {
// logger.error("Server {} failed to reconnect", event.connectionId(), e);
// GlobalEventBus.post(new Events.ServerEvents.CouldNotConnect(event.connectionId()));
// }
// }
// } // TODO: Reconnect on disconnect
// private void handleChangeConnection(Events.ServerEvents.ChangeConnection event) {
// ServerConnection serverConnection = this.serverConnections.get(event.connectionId());
// if (serverConnection != null) {
// try {
// serverConnection.connect(event.ip(), event.port());
// logger.info("Server {} changed connection to {}:{}", event.connectionId(),
// event.ip(), event.port());
// } catch (Exception e) {
// logger.error("Server {} failed to change connection", event.connectionId(),
// e);
// GlobalEventBus.post(new
// Events.ServerEvents.CouldNotConnect(event.connectionId()));
// }
// }
// } TODO
private void getAllConnections(NetworkEvents.RequestsAllClients request) {
List<NetworkingClient> a = new ArrayList<>(this.networkClients.values());
request.future().complete(a.toString());
}
public void shutdownAll(NetworkEvents.ForceCloseAllClients request) {
this.networkClients.values().forEach(NetworkingClient::closeConnection);
this.networkClients.clear();
logger.info("All servers shut down");
}
}

View File

@@ -0,0 +1,26 @@
package org.toop.framework.networking;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class NetworkingGameClientHandler extends ChannelInboundHandlerAdapter {
private static final Logger logger = LogManager.getLogger(NetworkingGameClientHandler.class);
public NetworkingGameClientHandler() {}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
logger.debug("Received message from server-{}, data: {}", ctx.channel().remoteAddress(), msg);
// TODO: Handle server messages
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
logger.error(cause.getMessage(), cause);
ctx.close();
}
}

View File

@@ -0,0 +1,12 @@
//package org.toop.frontend.networking.handlers;
//
//import io.netty.channel.ChannelHandlerContext;
//import org.apache.logging.log4j.LogManager;
//import org.apache.logging.log4j.Logger;
//import org.toop.frontend.networking.NetworkingGameClientHandler;
//
//public class NetworkingTicTacToeClientHandler extends NetworkingGameClientHandler {
// static final Logger logger = LogManager.getLogger(NetworkingTicTacToeClientHandler.class);
//
//
//}

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="debug" name="AppConfig">
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} [%17.17t] %-5level %logger{36} - %msg%n"/>
</Console>
</Appenders>
<Loggers>
<Root level="info">
<AppenderRef ref="Console"/>
</Root>
</Loggers>
</Configuration>