From 8263a1355a11af6880d2a8614cc1701aa1596593 Mon Sep 17 00:00:00 2001 From: Lars Simon Winzer Date: Sun, 15 Mar 2026 18:22:11 +0100 Subject: [PATCH 01/12] Add: UserId to uniquely identify user --- .../casono/server/domain/user/UserId.java | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/user/UserId.java diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/user/UserId.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/user/UserId.java new file mode 100644 index 0000000..c8e374e --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/user/UserId.java @@ -0,0 +1,31 @@ +package ch.unibas.dmi.dbis.cs108.casono.server.domain.user; + +import java.util.UUID; + +/** Represents a unique identifier for a user. */ +public class UserId { + private final UUID value; + + /** Creates a new UserId with a randomly generated UUID. */ + public UserId() { + this.value = UUID.randomUUID(); + } + + /** + * Creates a new UserId with the specified UUID. + * + * @param value the UUID to use for this UserId + */ + public UserId(UUID value) { + this.value = value; + } + + /** + * Returns the UUID value of this UserId. + * + * @return the UUID value + */ + public UUID value() { + return value; + } +} -- 2.52.0 From 2979c91ee0c8b52205f2681740da65eaf4f468fe Mon Sep 17 00:00:00 2001 From: Lars Simon Winzer Date: Sun, 15 Mar 2026 18:25:48 +0100 Subject: [PATCH 02/12] Add: User to link network to domain --- .../cs108/casono/server/domain/user/User.java | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/user/User.java diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/user/User.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/user/User.java new file mode 100644 index 0000000..b5b3c48 --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/user/User.java @@ -0,0 +1,28 @@ +package ch.unibas.dmi.dbis.cs108.casono.server.domain.user; + +import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionId; +import java.util.Optional; + +public class User { + private final UserId id; + private final String name; + private SessionId sessionId; + + public User(UserId id, String name, SessionId sessionId) { + this.id = id; + this.name = name; + this.sessionId = sessionId; + } + + public UserId getId() { + return id; + } + + public String getName() { + return name; + } + + public Optional getSessionId() { + return Optional.ofNullable(sessionId); + } +} -- 2.52.0 From 7c5a4675dd3ce347b92f252654c8b2baf097ca68 Mon Sep 17 00:00:00 2001 From: Lars Simon Winzer Date: Sun, 15 Mar 2026 18:35:25 +0100 Subject: [PATCH 03/12] Add: UserRegistry to store all active users and grand synchronized access --- .../server/domain/user/UserRegistry.java | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/user/UserRegistry.java diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/user/UserRegistry.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/user/UserRegistry.java new file mode 100644 index 0000000..600d901 --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/user/UserRegistry.java @@ -0,0 +1,42 @@ +package ch.unibas.dmi.dbis.cs108.casono.server.domain.user; + +import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionId; +import java.util.Iterator; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +public class UserRegistry { + private final ConcurrentHashMap byId = new ConcurrentHashMap<>(); + + public synchronized Optional registerIfAvailable(String name, SessionId sessionId) { + Iterator users = byId.elements().asIterator(); + while(users.hasNext()) { + User user = users.next(); + + if (user.getName() == name) { + return Optional.empty(); + } + } + + User user = new User(new UserId(), name, sessionId); + byId.put(user.getId(), user); + return Optional.of(user); + } + + public Optional findBySessionId(SessionId sessionId) { + Iterator users = byId.elements().asIterator(); + while(users.hasNext()) { + User user = users.next(); + Optional currentSessionId = user.getSessionId(); + + if (currentSessionId.isEmpty()) { + continue; + } + + if (currentSessionId.get() == sessionId) { + return Optional.of(user); + } + } + return Optional.empty(); + } +} \ No newline at end of file -- 2.52.0 From 82582dae83327ac3c0792f468c0ae1dc3e7baf20 Mon Sep 17 00:00:00 2001 From: Lars Simon Winzer Date: Sun, 15 Mar 2026 18:41:57 +0100 Subject: [PATCH 04/12] Add: UserFactory to create new users and resolve name conflicts --- .../server/domain/user/UserFactory.java | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/user/UserFactory.java diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/user/UserFactory.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/user/UserFactory.java new file mode 100644 index 0000000..cd94ae2 --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/user/UserFactory.java @@ -0,0 +1,28 @@ +package ch.unibas.dmi.dbis.cs108.casono.server.domain.user; + +import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionId; + +public class UserFactory { + private final UserRegistry registry; + + public UserFactory(UserRegistry registry) { + this.registry = registry; + } + + public User create(String desiredName, SessionId sessionId) { + var result = registry.registerIfAvailable(desiredName, sessionId); + if (result.isPresent()) { + return result.get(); + } + + int suffix = 1; + while (true) { + String candidate = desiredName + "_" + String.format("%03d", suffix); + result = registry.registerIfAvailable(candidate, sessionId); + if (result.isPresent()) { + return result.get(); + } + suffix++; + } + } +} -- 2.52.0 From a7cdf995c4dbb533d1a80a71d0a76c641a7c2a65 Mon Sep 17 00:00:00 2001 From: Lars Simon Winzer Date: Sun, 15 Mar 2026 18:44:03 +0100 Subject: [PATCH 05/12] Add: Timestamp to User to store time of disconnect --- .../cs108/casono/server/domain/user/User.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/user/User.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/user/User.java index b5b3c48..01fd428 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/user/User.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/user/User.java @@ -1,17 +1,20 @@ package ch.unibas.dmi.dbis.cs108.casono.server.domain.user; import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionId; +import java.time.Instant; import java.util.Optional; public class User { private final UserId id; private final String name; private SessionId sessionId; + private Instant disconnectedAt; public User(UserId id, String name, SessionId sessionId) { this.id = id; this.name = name; this.sessionId = sessionId; + this.disconnectedAt = null; } public UserId getId() { @@ -25,4 +28,18 @@ public class User { public Optional getSessionId() { return Optional.ofNullable(sessionId); } + + public Optional getDisconnectedAt() { + return Optional.ofNullable(disconnectedAt); + } + + public void reassignSession(SessionId sessionId) { + this.sessionId = sessionId; + this.disconnectedAt = null; + } + + public void markDisconnected() { + this.sessionId = null; + this.disconnectedAt = Instant.now(); + } } -- 2.52.0 From 9d9b6ae2c7a8595792dd4a117181f9c76f3489ad Mon Sep 17 00:00:00 2001 From: Lars Simon Winzer Date: Sun, 15 Mar 2026 18:57:38 +0100 Subject: [PATCH 06/12] Add: Handler for DisconnectEvent to SessionRegistry --- .../cs108/casono/server/domain/user/UserRegistry.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/user/UserRegistry.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/user/UserRegistry.java index 600d901..0ab6a44 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/user/UserRegistry.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/user/UserRegistry.java @@ -39,4 +39,14 @@ public class UserRegistry { } return Optional.empty(); } + + // TODO: Add to EventRegistry with DisconnectEvent + public synchronized void onDisconnect(SessionId sessionId) { + Optional user = findBySessionId(sessionId); + + if (user.isPresent()) { + user.get().markDisconnected(); + } + } + } \ No newline at end of file -- 2.52.0 From 1223e1a4a9a22de41e4cfeadbb645689b0da40c1 Mon Sep 17 00:00:00 2001 From: Lars Simon Winzer Date: Sun, 15 Mar 2026 19:27:41 +0100 Subject: [PATCH 07/12] Add: Extend UserRegistry to remove Disconnected User, reassign User to new session and get all users --- .../server/domain/user/UserRegistry.java | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/user/UserRegistry.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/user/UserRegistry.java index 0ab6a44..ec128bd 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/user/UserRegistry.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/user/UserRegistry.java @@ -1,6 +1,8 @@ package ch.unibas.dmi.dbis.cs108.casono.server.domain.user; import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionId; + +import java.util.Collection; import java.util.Iterator; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; @@ -49,4 +51,29 @@ public class UserRegistry { } } -} \ No newline at end of file + public synchronized boolean removeIfStillDisconnected(UserId userId) { + User user = byId.get(userId); + if (user == null) { + return false; + } + + if (user.getSessionId().isPresent()) { + return false; + } + + byId.remove(user.getId()); + return true; + } + + public synchronized Optional reassignSession(UserId userId, SessionId sessionId) { + User user = byId.get(userId); + if (user == null) return Optional.empty(); + + user.reassignSession(sessionId); + return Optional.of(user); + } + + public Collection getAllUsers() { + return byId.values(); + } +} -- 2.52.0 From 8dd99726cebc70945693b30646f36502bf498d98 Mon Sep 17 00:00:00 2001 From: Lars Simon Winzer Date: Sun, 15 Mar 2026 19:29:25 +0100 Subject: [PATCH 08/12] Add: UserCleanupJob as runnable to remove all expired users --- .../server/domain/user/UserCleanupJob.java | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/user/UserCleanupJob.java diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/user/UserCleanupJob.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/user/UserCleanupJob.java new file mode 100644 index 0000000..d39db57 --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/user/UserCleanupJob.java @@ -0,0 +1,39 @@ +package ch.unibas.dmi.dbis.cs108.casono.server.domain.user; + +import java.time.Duration; +import java.time.Instant; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class UserCleanupJob implements Runnable { + private static final Logger logger = LogManager.getLogger(UserCleanupJob.class); + + private final UserRegistry registry; + private final Duration reconnectThreshold; + + public UserCleanupJob(UserRegistry registry, Duration reconnectThreshold) { + this.registry = registry; + this.reconnectThreshold = reconnectThreshold; + } + + @Override + public void run() { + logger.debug("Job started."); + Instant threshold = Instant.now().minus(reconnectThreshold); + + for (User user : registry.getAllUsers()) { + if (user.getDisconnectedAt().isEmpty()) { + continue; + } + + Instant disconnectedAt = user.getDisconnectedAt().get(); + if (disconnectedAt.isBefore(threshold)) { + if (registry.removeIfStillDisconnected(user.getId())) { + logger.info("Removed expired user {} ({})", user.getName(), user.getId().value()); + } + } + } + + logger.debug("Job finished."); + } +} -- 2.52.0 From 8fae4789ad3cb8f6e5b4a777e41e0e4bf3fda40b Mon Sep 17 00:00:00 2001 From: Lars Simon Winzer Date: Sun, 15 Mar 2026 19:37:43 +0100 Subject: [PATCH 09/12] Add: Make UserRegistry use multiple Maps to accelerate lookups --- .../server/domain/user/UserRegistry.java | 63 ++++++++----------- 1 file changed, 26 insertions(+), 37 deletions(-) diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/user/UserRegistry.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/user/UserRegistry.java index ec128bd..061298d 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/user/UserRegistry.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/user/UserRegistry.java @@ -1,56 +1,27 @@ package ch.unibas.dmi.dbis.cs108.casono.server.domain.user; import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionId; - import java.util.Collection; -import java.util.Iterator; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; public class UserRegistry { private final ConcurrentHashMap byId = new ConcurrentHashMap<>(); + private final ConcurrentHashMap byName = new ConcurrentHashMap<>(); + private final ConcurrentHashMap bySessionId = new ConcurrentHashMap<>(); public synchronized Optional registerIfAvailable(String name, SessionId sessionId) { - Iterator users = byId.elements().asIterator(); - while(users.hasNext()) { - User user = users.next(); - - if (user.getName() == name) { - return Optional.empty(); - } + if (byName.containsKey(name)) { + return Optional.empty(); } User user = new User(new UserId(), name, sessionId); byId.put(user.getId(), user); + byName.put(user.getName(), user); + bySessionId.put(sessionId, user); return Optional.of(user); } - public Optional findBySessionId(SessionId sessionId) { - Iterator users = byId.elements().asIterator(); - while(users.hasNext()) { - User user = users.next(); - Optional currentSessionId = user.getSessionId(); - - if (currentSessionId.isEmpty()) { - continue; - } - - if (currentSessionId.get() == sessionId) { - return Optional.of(user); - } - } - return Optional.empty(); - } - - // TODO: Add to EventRegistry with DisconnectEvent - public synchronized void onDisconnect(SessionId sessionId) { - Optional user = findBySessionId(sessionId); - - if (user.isPresent()) { - user.get().markDisconnected(); - } - } - public synchronized boolean removeIfStillDisconnected(UserId userId) { User user = byId.get(userId); if (user == null) { @@ -62,17 +33,35 @@ public class UserRegistry { } byId.remove(user.getId()); + byName.remove(user.getName()); return true; } + // TODO: Add to EventRegistry with DisconnectEvent + public synchronized void onDisconnect(SessionId sessionId) { + User user = bySessionId.remove(sessionId); + if (user == null) { + return; + } + + user.markDisconnected(); + } + public synchronized Optional reassignSession(UserId userId, SessionId sessionId) { User user = byId.get(userId); - if (user == null) return Optional.empty(); - + if (user == null) { + return Optional.empty(); + } + user.reassignSession(sessionId); + bySessionId.put(sessionId, user); return Optional.of(user); } + public Optional findBySessionId(SessionId sessionId) { + return Optional.ofNullable(bySessionId.get(sessionId)); + } + public Collection getAllUsers() { return byId.values(); } -- 2.52.0 From 0a5eb5d11ce45894ad1e9aee1d2ff3be0c947f37 Mon Sep 17 00:00:00 2001 From: Lars Simon Winzer Date: Sun, 15 Mar 2026 19:48:55 +0100 Subject: [PATCH 10/12] Chore: JavaDoc Docstrings --- .../cs108/casono/server/domain/user/User.java | 34 ++++++++++++++++ .../server/domain/user/UserCleanupJob.java | 4 ++ .../server/domain/user/UserFactory.java | 15 +++++++ .../server/domain/user/UserRegistry.java | 39 +++++++++++++++++++ 4 files changed, 92 insertions(+) diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/user/User.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/user/User.java index 01fd428..904112b 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/user/User.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/user/User.java @@ -4,12 +4,20 @@ import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionId; import java.time.Instant; import java.util.Optional; +/** Represents an authenticated user on the server. */ public class User { private final UserId id; private final String name; private SessionId sessionId; private Instant disconnectedAt; + /** + * Creates a new User with the given ID, name and session. + * + * @param id the unique identifier for this user + * @param name the display name of this user + * @param sessionId the session currently associated with this user + */ public User(UserId id, String name, SessionId sessionId) { this.id = id; this.name = name; @@ -17,27 +25,53 @@ public class User { this.disconnectedAt = null; } + /** + * Returns the ID of this user. + * + * @return the user ID + */ public UserId getId() { return id; } + /** + * Returns the display name of this user. + * + * @return the user name + */ public String getName() { return name; } + /** + * Returns the session currently associated with this user, if any. + * + * @return an Optional containing the session ID, or empty if disconnected + */ public Optional getSessionId() { return Optional.ofNullable(sessionId); } + /** + * Returns the time at which this user disconnected, if applicable. + * + * @return an Optional containing the disconnect timestamp, or empty if connected + */ public Optional getDisconnectedAt() { return Optional.ofNullable(disconnectedAt); } + /** + * Associates this user with a new session, clearing the disconnect timestamp. + * + * @param sessionId the new session ID + */ public void reassignSession(SessionId sessionId) { this.sessionId = sessionId; this.disconnectedAt = null; } + /** Marks this user as disconnected by clearing the session and recording the timestamp. */ public void markDisconnected() { this.sessionId = null; this.disconnectedAt = Instant.now(); diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/user/UserCleanupJob.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/user/UserCleanupJob.java index d39db57..976670c 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/user/UserCleanupJob.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/user/UserCleanupJob.java @@ -5,6 +5,10 @@ import java.time.Instant; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +/** + * Periodically periodicly run job to remove disconnected users who have exceeded the reconnect + * threshold. + */ public class UserCleanupJob implements Runnable { private static final Logger logger = LogManager.getLogger(UserCleanupJob.class); diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/user/UserFactory.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/user/UserFactory.java index cd94ae2..84ab1ce 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/user/UserFactory.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/user/UserFactory.java @@ -2,13 +2,28 @@ package ch.unibas.dmi.dbis.cs108.casono.server.domain.user; import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionId; +/** Creates new users, resolving name conflicts automatically. */ public class UserFactory { private final UserRegistry registry; + /** + * Creates a new UserFactory backed by the given registry. + * + * @param registry the registry to register new users to + */ public UserFactory(UserRegistry registry) { this.registry = registry; } + /** + * Creates and registers a new user with the given name and session. If the name is already + * taken, a numeric suffix is appended and incremented until a free name is found (e.g. + * Lars_001, Lars_002, ...). + * + * @param desiredName the preferred display name + * @param sessionId the session to associate with the new user + * @return the newly created and registered user + */ public User create(String desiredName, SessionId sessionId) { var result = registry.registerIfAvailable(desiredName, sessionId); if (result.isPresent()) { diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/user/UserRegistry.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/user/UserRegistry.java index 061298d..6a60877 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/user/UserRegistry.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/user/UserRegistry.java @@ -5,11 +5,20 @@ import java.util.Collection; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; +/** Manages all active users on the server. */ public class UserRegistry { private final ConcurrentHashMap byId = new ConcurrentHashMap<>(); private final ConcurrentHashMap byName = new ConcurrentHashMap<>(); private final ConcurrentHashMap bySessionId = new ConcurrentHashMap<>(); + /** + * Attempts to register a user under the given name atomically. Returns the registered user, or + * empty if the name is already taken. + * + * @param name the desired display name + * @param sessionId the session to associate with the new user + * @return an Optional containing the new user, or empty if the name was taken + */ public synchronized Optional registerIfAvailable(String name, SessionId sessionId) { if (byName.containsKey(name)) { return Optional.empty(); @@ -22,6 +31,12 @@ public class UserRegistry { return Optional.of(user); } + /** + * Removes the user with the given ID, but only if they are still disconnected. This prevents + * removing a user who has reconnected between the cleanup job's check and its removal call. + * + * @param userId the ID of the user to remove + */ public synchronized boolean removeIfStillDisconnected(UserId userId) { User user = byId.get(userId); if (user == null) { @@ -37,6 +52,12 @@ public class UserRegistry { return true; } + /** + * Marks the user associated with the given session as disconnected, clearing the session + * association and recording the disconnect timestamp. + * + * @param sessionId the session ID of the disconnected client + */ // TODO: Add to EventRegistry with DisconnectEvent public synchronized void onDisconnect(SessionId sessionId) { User user = bySessionId.remove(sessionId); @@ -47,6 +68,13 @@ public class UserRegistry { user.markDisconnected(); } + /** + * Reassociates a user with a new session, effectively restoring them after a reconnect. + * + * @param userId the ID of the user to reconnect + * @param sessionId the new session ID + * @return an Optional containing the user, or empty if the user was not found + */ public synchronized Optional reassignSession(UserId userId, SessionId sessionId) { User user = byId.get(userId); if (user == null) { @@ -58,10 +86,21 @@ public class UserRegistry { return Optional.of(user); } + /** + * Looks up a user by their session ID. + * + * @param sessionId the session ID to look up + * @return an Optional containing the user, or empty if no user is associated with this session + */ public Optional findBySessionId(SessionId sessionId) { return Optional.ofNullable(bySessionId.get(sessionId)); } + /** + * Returns all currently registered users. + * + * @return a collection of all users + */ public Collection getAllUsers() { return byId.values(); } -- 2.52.0 From 68827e177ed590cf3ebd49683fc49b997f84d703 Mon Sep 17 00:00:00 2001 From: Lars Simon Winzer Date: Sun, 15 Mar 2026 19:49:23 +0100 Subject: [PATCH 11/12] Add: Move logger creation in CleanupJob to constructor --- .../cs108/casono/server/domain/user/UserCleanupJob.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/user/UserCleanupJob.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/user/UserCleanupJob.java index 976670c..32cfbb4 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/user/UserCleanupJob.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/user/UserCleanupJob.java @@ -10,12 +10,12 @@ import org.apache.logging.log4j.Logger; * threshold. */ public class UserCleanupJob implements Runnable { - private static final Logger logger = LogManager.getLogger(UserCleanupJob.class); - + private final Logger logger; private final UserRegistry registry; private final Duration reconnectThreshold; public UserCleanupJob(UserRegistry registry, Duration reconnectThreshold) { + this.logger = LogManager.getLogger(UserCleanupJob.class); this.registry = registry; this.reconnectThreshold = reconnectThreshold; } @@ -33,7 +33,8 @@ public class UserCleanupJob implements Runnable { Instant disconnectedAt = user.getDisconnectedAt().get(); if (disconnectedAt.isBefore(threshold)) { if (registry.removeIfStillDisconnected(user.getId())) { - logger.info("Removed expired user {} ({})", user.getName(), user.getId().value()); + logger.info( + "Removed expired user {} ({})", user.getName(), user.getId().value()); } } } -- 2.52.0 From 46b1388cad7fcd81fb5c59d4a65bacf7aab08fc2 Mon Sep 17 00:00:00 2001 From: Lars Simon Winzer Date: Sun, 15 Mar 2026 19:51:03 +0100 Subject: [PATCH 12/12] Add: Create UserRegistry and schedule UserCleanupJob --- .../dbis/cs108/casono/server/ServerApp.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/ServerApp.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/ServerApp.java index 702d1c8..d44df55 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/ServerApp.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/ServerApp.java @@ -1,14 +1,24 @@ package ch.unibas.dmi.dbis.cs108.casono.server; +import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserCleanupJob; +import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserRegistry; import ch.unibas.dmi.dbis.cs108.casono.server.network.NetworkManager; import ch.unibas.dmi.dbis.cs108.casono.server.network.events.DisconnectEvent; import ch.unibas.dmi.dbis.cs108.casono.server.network.events.EventBus; import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionManager; +import java.time.Duration; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; /** Application class for starting the server. */ public class ServerApp { + public static final int USER_CLEANUP_JOB_DELAY = 0; + public static final int USER_CLEANUP_JOB_PERIOD = 10; + public static final int USER_CLEANUP_JOB_RECONNECT_THRESHOLD = 10; + public static void start(String arg) { int port = Integer.parseInt(arg); @@ -20,6 +30,18 @@ public class ServerApp { eventBus.subscribe( DisconnectEvent.class, event -> sessionManager.removeSession(event.sessionId())); NetworkManager networkManager = new NetworkManager(port, sessionManager, eventBus); + + UserRegistry userRegistry = new UserRegistry(); + eventBus.subscribe( + DisconnectEvent.class, event -> userRegistry.onDisconnect(event.sessionId())); + ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); + scheduler.scheduleAtFixedRate( + new UserCleanupJob( + userRegistry, Duration.ofSeconds(USER_CLEANUP_JOB_RECONNECT_THRESHOLD)), + USER_CLEANUP_JOB_DELAY, + USER_CLEANUP_JOB_PERIOD, + TimeUnit.SECONDS); + networkManager.start(); } } -- 2.52.0