fix: checkstyle and spotless

This commit is contained in:
Jona Walpert
2026-04-12 18:36:47 +02:00
parent 9cf6b468ad
commit 0f8f1bb918
4 changed files with 230 additions and 193 deletions
@@ -24,10 +24,8 @@ import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/**
* The ClientService class is responsible for managing the connection to the
* server, sending
* commands, and receiving responses. It uses a TcpTransport to communicate with
* the server and an
* The ClientService class is responsible for managing the connection to the server, sending
* commands, and receiving responses. It uses a TcpTransport to communicate with the server and an
* ExecutorService to handle asynchronous requests.
*/
public class ClientService {
@@ -41,17 +39,17 @@ public class ClientService {
private final AtomicInteger idGenerator;
private Logger logger;
private final Map<Integer, ArrayBlockingQueue<ParsedResponse>> pendingResponses = new ConcurrentHashMap<>();
private final CopyOnWriteArrayList<Consumer<List<String>>> eventListeners = new CopyOnWriteArrayList<>();
private final Map<Integer, ArrayBlockingQueue<ParsedResponse>> pendingResponses =
new ConcurrentHashMap<>();
private final CopyOnWriteArrayList<Consumer<List<String>>> eventListeners =
new CopyOnWriteArrayList<>();
private Thread readerThread = null;
private final AtomicBoolean running = new AtomicBoolean(false);
private static final int READER_JOIN_TIMEOUT_MS = 500;
/**
* Constructs a ClientService with the given server IP and port. It establishes
* a socket
* connection to the server and initializes the TcpTransport and ExecutorService
* for
* Constructs a ClientService with the given server IP and port. It establishes a socket
* connection to the server and initializes the TcpTransport and ExecutorService for
* communication.
*
* @param ip The IP address of the server to connect to.
@@ -79,7 +77,8 @@ public class ClientService {
private void startReaderThread() {
running.set(true);
readerThread = new Thread(
readerThread =
new Thread(
() -> {
while (running.get()) {
try {
@@ -170,8 +169,7 @@ public class ClientService {
}
/**
* Constructs a ClientService in offline mode. No network connection will be
* attempted and calls
* Constructs a ClientService in offline mode. No network connection will be attempted and calls
* to processCommand will throw a RuntimeException.
*
* @param offline true to create an offline (no-network) client service
@@ -184,21 +182,19 @@ public class ClientService {
this.executor = Executors.newSingleThreadExecutor();
}
/**
* Returns true if this ClientService is running in offline mode (no network).
*/
/** Returns true if this ClientService is running in offline mode (no network). */
public boolean isOffline() {
return offlineMode;
}
// Allow primitive values to contain hyphens (UUIDs) in addition to
// digits/words/colons
static Pattern responseRex = Pattern.compile(
static Pattern responseRex =
Pattern.compile(
"(?<key>\\w+)=(('(?<string>([^']|\\')+)')|(?<primVal>[+-]?[-\\d\\w:]+))");
/**
* Removes escape characters from a string, specifically converting escaped
* single quotes (\')
* Removes escape characters from a string, specifically converting escaped single quotes (\')
* back to regular single quotes (').
*
* @param input The escaped string to process.
@@ -209,10 +205,8 @@ public class ClientService {
}
/**
* Converts a list of raw string parameters into a list of
* {@link RequestParameter} objects. It
* uses a regex matcher to distinguish between quoted strings (which are
* unescaped) and
* Converts a list of raw string parameters into a list of {@link RequestParameter} objects. It
* uses a regex matcher to distinguish between quoted strings (which are unescaped) and
* primitive values.
*
* @param input A list of raw strings to be parsed.
@@ -237,12 +231,10 @@ public class ClientService {
.toList();
}
private static record ParsedResponse(boolean success, List<String> lines) {
}
private static record ParsedResponse(boolean success, List<String> lines) {}
/**
* Register an event listener that receives unsolicited event payload lines (no
* status prefix).
* Register an event listener that receives unsolicited event payload lines (no status prefix).
*/
public void addEventListener(Consumer<List<String>> listener) {
eventListeners.add(listener);
@@ -253,17 +245,13 @@ public class ClientService {
}
/**
* Sends a command to the server and processes the multi-line response. It
* handles the protocol
* handshake (expecting +OK), strips leading tabs from response lines, and
* collects them until
* Sends a command to the server and processes the multi-line response. It handles the protocol
* handshake (expecting +OK), strips leading tabs from response lines, and collects them until
* the "END" marker is reached.
*
* @param message The raw command string to be sent to the transport layer.
* @return A list of response lines received from the server (excluding protocol
* markers).
* @throws RuntimeException if the server responds with an error or if a
* communication failure
* @return A list of response lines received from the server (excluding protocol markers).
* @throws RuntimeException if the server responds with an error or if a communication failure
* occurs.
*/
protected List<String> processCommand(String message) {
@@ -275,7 +263,8 @@ public class ClientService {
ArrayBlockingQueue<ParsedResponse> q = new ArrayBlockingQueue<>(1);
pendingResponses.put(reqId, q);
Future<?> writeFuture = executor.submit(
Future<?> writeFuture =
executor.submit(
() -> {
try {
clienttcptransport.write(new RawPacket(reqId, message));
@@ -314,15 +303,11 @@ public class ClientService {
}
/**
* Helper method to send a request to the server using the ExecutorService. It
* submits the
* request as a Runnable task and waits for its completion. If the task is
* interrupted or
* encounters an execution exception, it throws a RuntimeException with the
* appropriate cause.
* Helper method to send a request to the server using the ExecutorService. It submits the
* request as a Runnable task and waits for its completion. If the task is interrupted or
* encounters an execution exception, it throws a RuntimeException with the appropriate cause.
*
* @param request The Runnable task representing the request to be sent to the
* server.
* @param request The Runnable task representing the request to be sent to the server.
*/
private void sendRequest(Runnable request) {
Future<?> future = executor.submit(request);
@@ -336,12 +321,9 @@ public class ClientService {
}
/**
* Helper method to extract the cause of an exception and return it as a
* RuntimeException. If
* the cause is null, it returns the original exception as a RuntimeException.
* If the cause is
* already a RuntimeException, it returns it directly. Otherwise, it wraps the
* cause in a new
* Helper method to extract the cause of an exception and return it as a RuntimeException. If
* the cause is null, it returns the original exception as a RuntimeException. If the cause is
* already a RuntimeException, it returns it directly. Otherwise, it wraps the cause in a new
* RuntimeException and returns it.
*
* @param e The exception from which to extract the cause.
@@ -359,10 +341,8 @@ public class ClientService {
}
/**
* Closes the socket connection to the server and shuts down the
* ExecutorService. It also closes
* the TcpTransport used for communication. If any IOException occurs during
* this process, it
* Closes the socket connection to the server and shuts down the ExecutorService. It also closes
* the TcpTransport used for communication. If any IOException occurs during this process, it
* prints the exception to the console.
*/
public void closeSocket() {
@@ -28,6 +28,7 @@ public class LobbyButtonGridManager {
private static final int REFRESH_INTERVAL_SECONDS = 5;
private static final int INITIAL_DELAY_SECONDS = 5;
private static final int COLS = 4;
private static final int MAX_BUTTONS = 8;
private static final Logger LOGGER = LogManager.getLogger(LobbyButtonGridManager.class);
@@ -66,9 +67,12 @@ public class LobbyButtonGridManager {
// Subscribe to server-initiated events so closed lobbies are removed
// immediately
clientService.addEventListener(
lines -> {
List<RequestParameter> params = ClientService.convertToRequestParameters(lines);
clientService.addEventListener(this::handleClientServiceEvent);
}
private static record EventInfo(String event, String lobbyId) {}
private EventInfo extractEventInfo(List<RequestParameter> params) {
String evt = null;
String lidStr = null;
for (RequestParameter p : params) {
@@ -78,14 +82,21 @@ public class LobbyButtonGridManager {
lidStr = p.value();
}
}
if (evt != null && "LOBBY_CLOSED".equalsIgnoreCase(evt) && lidStr != null) {
int lid;
try {
lid = Integer.parseInt(lidStr);
} catch (NumberFormatException ex) {
return;
return new EventInfo(evt, lidStr);
}
LOGGER.info("LOBBY_CLOSED event for lobby {} — mapping before: {}", lid,
private Integer tryParseInt(String s) {
try {
return Integer.parseInt(s);
} catch (NumberFormatException ex) {
return null;
}
}
private void handleLobbyClosed(int lid) {
LOGGER.info(
"LOBBY_CLOSED event for lobby {} — mapping before: {}",
lid,
translationManager.getButtonIdToLobbyId());
// find button id(s) for this lobby and remove mapping
Map<Integer, Integer> mapping = translationManager.getButtonIdToLobbyId();
@@ -97,21 +108,20 @@ public class LobbyButtonGridManager {
}
}
if (toRemove != null) {
LOGGER.info("Removing mapping for button {} -> lobby {} (LOBBY_CLOSED event)", toRemove,
LOGGER.info(
"Removing mapping for button {} -> lobby {} (LOBBY_CLOSED event)",
toRemove,
lid);
translationManager.removeLobbyButton(toRemove);
LOGGER.debug("Mapping after removal: {}", translationManager.getButtonIdToLobbyId());
javafx.application.Platform.runLater(this::renderLobbyButtons);
}
} else if (evt != null && "LOBBY_CREATED".equalsIgnoreCase(evt) && lidStr != null) {
int lid;
try {
lid = Integer.parseInt(lidStr);
} catch (NumberFormatException ex) {
return;
}
LOGGER.info("LOBBY_CREATED event for lobby {} — mapping before: {}", lid,
private void handleLobbyCreated(int lid) {
LOGGER.info(
"LOBBY_CREATED event for lobby {} — mapping before: {}",
lid,
translationManager.getButtonIdToLobbyId());
Map<Integer, Integer> mapping = translationManager.getButtonIdToLobbyId();
@@ -122,13 +132,14 @@ public class LobbyButtonGridManager {
return;
}
// find next free button id (1..8)
for (int candidate = 1; candidate <= 8; candidate++) {
// find next free button id (1..MAX_BUTTONS)
for (int candidate = 1; candidate <= MAX_BUTTONS; candidate++) {
if (!mapping.containsKey(candidate)) {
try {
translationManager.addLobbyButton(candidate, lid);
LOGGER.info("Mapped new lobby {} to button {}", lid, candidate);
LOGGER.debug("Mapping after add: {}", translationManager.getButtonIdToLobbyId());
LOGGER.debug(
"Mapping after add: {}", translationManager.getButtonIdToLobbyId());
javafx.application.Platform.runLater(this::renderLobbyButtons);
break;
} catch (Exception ex) {
@@ -138,7 +149,24 @@ public class LobbyButtonGridManager {
}
}
}
});
private void handleClientServiceEvent(List<String> lines) {
EventInfo info = extractEventInfo(ClientService.convertToRequestParameters(lines));
if (info.event() == null || info.lobbyId() == null) {
return;
}
Integer lid = tryParseInt(info.lobbyId());
if (lid == null) {
return;
}
if ("LOBBY_CLOSED".equalsIgnoreCase(info.event())) {
handleLobbyClosed(lid);
} else if ("LOBBY_CREATED".equalsIgnoreCase(info.event())) {
handleLobbyCreated(lid);
}
}
private void startPeriodicRefresh(long initialDelay, long period) {
@@ -203,10 +231,12 @@ public class LobbyButtonGridManager {
if (status == null) {
if (!serverIds.contains(lobbyId)) {
translationManager.removeLobbyButton(buttonId);
javafx.application.Platform.runLater(this::renderLobbyButtons);
javafx.application.Platform.runLater(
this::renderLobbyButtons);
} else {
// treat as created/running later; keep mapping
LOGGER.debug("Temporary missing status for lobby {} - keeping mapping",
LOGGER.debug(
"Missing status for lobby {} - keeping mapping",
lobbyId);
}
}
@@ -241,7 +271,9 @@ public class LobbyButtonGridManager {
}
}
for (Integer bid : toRemove) {
LOGGER.info("Reconciling: removing mapping for button {} (server no longer lists lobby)", bid);
LOGGER.info(
"Reconciling: removing mapping for button {} (server no longer lists lobby)",
bid);
translationManager.removeLobbyButton(bid);
}
@@ -250,16 +282,20 @@ public class LobbyButtonGridManager {
if (mapping.containsValue(li.id)) {
continue;
}
// find next free button id (1..8)
for (int candidate = 1; candidate <= 8; candidate++) {
// find next free button id (1..MAX_BUTTONS)
for (int candidate = 1; candidate <= MAX_BUTTONS; candidate++) {
if (!mapping.containsKey(candidate)) {
try {
translationManager.addLobbyButton(candidate, li.id);
LOGGER.info("Reconciling: added mapping button {} -> lobby {}", candidate, li.id);
LOGGER.info(
"Reconciling: added mapping button {} -> lobby {}",
candidate,
li.id);
break;
} catch (Exception ex) {
// grid full? try next candidate
LOGGER.debug("Could not add mapping for lobby {}: {}", li.id, ex.getMessage());
LOGGER.debug(
"Could not add mapping for lobby {}: {}", li.id, ex.getMessage());
}
}
}
@@ -340,7 +376,8 @@ public class LobbyButtonGridManager {
statusStr -> {
LobbyStatus status = parseLobbyStatus(statusStr);
String path = getImagePathForLobby(
String path =
getImagePathForLobby(
lobbyId, status == null ? LobbyStatus.CREATED : status);
Image img = safeLoadImage(path);
@@ -445,13 +482,15 @@ public class LobbyButtonGridManager {
for (Node node : gridPane.getChildren()) {
boolean isButton = node instanceof Button;
boolean idMatches = ("lobbyBtn-" + buttonId)
boolean idMatches =
("lobbyBtn-" + buttonId)
.equals(node.getId());
if (isButton && idMatches) {
Button btn = (Button) node;
ImageView iv = new ImageView(safeLoadImage(path));
ImageView iv =
new ImageView(safeLoadImage(path));
iv.setPreserveRatio(true);
iv.fitWidthProperty()
@@ -496,7 +535,8 @@ public class LobbyButtonGridManager {
javafx.application.Platform.runLater(
() -> {
javafx.stage.Stage currentStage = (javafx.stage.Stage) gridPane.getScene().getWindow();
javafx.stage.Stage currentStage =
(javafx.stage.Stage) gridPane.getScene().getWindow();
currentStage.hide();
@@ -94,7 +94,12 @@ public class ServerApp {
TimeUnit.SECONDS);
LobbyManager lobbyManager = new LobbyManager();
registerCommands(dispatcher, router, responseDispatcher, userRegistry, lobbyManager, sessionManager);
registerCommands(
dispatcher,
router,
responseDispatcher,
userRegistry,
new CommandContext(lobbyManager, sessionManager));
// Periodic cleanup: remove empty lobbies older than 30s and notify affected
// users
@@ -135,6 +140,9 @@ public class ServerApp {
networkManager.start();
}
private static record CommandContext(
LobbyManager lobbyManager, SessionManager sessionManager) {}
/**
* Registers command parsers and handlers.
*
@@ -147,8 +155,7 @@ public class ServerApp {
CommandRouter commandRouter,
ResponseDispatcher responseDispatcher,
UserRegistry userRegistry,
LobbyManager lobbyManager,
SessionManager sessionManager) {
CommandContext context) {
parserDispatcher.register("PING", new PingParser());
commandRouter.register(PingRequest.class, new PingHandler(responseDispatcher));
@@ -195,7 +202,7 @@ public class ServerApp {
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby
.get_lobby_list.GetLobbyListRequest>)
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.get_lobby_list
.GetLobbyListHandler(responseDispatcher, lobbyManager));
.GetLobbyListHandler(responseDispatcher, context.lobbyManager()));
// GET_GAME_STATE registration
parserDispatcher.register(
@@ -210,7 +217,7 @@ public class ServerApp {
.get_game_state.GetGameStateRequest>)
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.get_game_state
.GetGameStateHandler(
responseDispatcher, lobbyManager, userRegistry));
responseDispatcher, context.lobbyManager(), userRegistry));
// BET registration
parserDispatcher.register(
@@ -222,7 +229,8 @@ public class ServerApp {
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.bet
.PlayerBetRequest>)
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.bet
.PlayerBetHandler(responseDispatcher, userRegistry, lobbyManager));
.PlayerBetHandler(
responseDispatcher, userRegistry, context.lobbyManager()));
// RAISE registration
parserDispatcher.register(
@@ -237,7 +245,7 @@ public class ServerApp {
.PlayerRaiseRequest>)
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.raise
.PlayerRaiseHandler(
responseDispatcher, userRegistry, lobbyManager));
responseDispatcher, userRegistry, context.lobbyManager()));
// CALL registration
parserDispatcher.register(
@@ -251,7 +259,8 @@ public class ServerApp {
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.call
.PlayerCallRequest>)
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.call
.PlayerCallHandler(responseDispatcher, userRegistry, lobbyManager));
.PlayerCallHandler(
responseDispatcher, userRegistry, context.lobbyManager()));
// FOLD registration
parserDispatcher.register(
@@ -265,7 +274,8 @@ public class ServerApp {
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.fold
.PlayerFoldRequest>)
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.fold
.PlayerFoldHandler(responseDispatcher, userRegistry, lobbyManager));
.PlayerFoldHandler(
responseDispatcher, userRegistry, context.lobbyManager()));
// GET_LOBBY_STATUS registration
parserDispatcher.register(
@@ -280,7 +290,7 @@ public class ServerApp {
.get_lobby_status.GetLobbyStatusRequest>)
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby
.get_lobby_status.GetLobbyStatusHandler(
responseDispatcher, lobbyManager, userRegistry));
responseDispatcher, context.lobbyManager(), userRegistry));
// CREATE_LOBBY registration
parserDispatcher.register(
@@ -294,7 +304,10 @@ public class ServerApp {
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby
.create_lobby.CreateLobbyRequest>)
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.create_lobby
.CreateLobbyHandler(responseDispatcher, lobbyManager, sessionManager));
.CreateLobbyHandler(
responseDispatcher,
context.lobbyManager(),
context.sessionManager()));
// JOIN_LOBBY registration
parserDispatcher.register(
@@ -308,7 +321,8 @@ public class ServerApp {
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.join_lobby
.JoinLobbyRequest>)
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.join_lobby
.JoinLobbyHandler(responseDispatcher, lobbyManager, userRegistry));
.JoinLobbyHandler(
responseDispatcher, context.lobbyManager(), userRegistry));
// START_GAME registration
parserDispatcher.register(
@@ -322,6 +336,7 @@ public class ServerApp {
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.start_game
.StartGameRequest>)
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.start_game
.StartGameHandler(responseDispatcher, lobbyManager, userRegistry));
.StartGameHandler(
responseDispatcher, context.lobbyManager(), userRegistry));
}
}
@@ -3,20 +3,22 @@ package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.create_lobby;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.LobbyId;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.LobbyManager;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.ErrorResponse;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.SuccessResponse;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder.ResponseBody;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatcher;
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.Session;
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionManager;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
public class CreateLobbyHandler extends CommandHandler<CreateLobbyRequest> {
private final LobbyManager lobbyManager;
private final SessionManager sessionManager;
public CreateLobbyHandler(
ResponseDispatcher responseDispatcher, LobbyManager lobbyManager, SessionManager sessionManager) {
ResponseDispatcher responseDispatcher,
LobbyManager lobbyManager,
SessionManager sessionManager) {
super(responseDispatcher);
this.lobbyManager = lobbyManager;
this.sessionManager = sessionManager;