fix: checkstyle methode length

This commit is contained in:
Jona Walpert
2026-04-13 02:24:29 +02:00
parent 1f8cf35e55
commit 52b5d5b8c2
3 changed files with 341 additions and 258 deletions
@@ -10,10 +10,11 @@ import org.apache.logging.log4j.Logger;
/**
* Entry point and bootstrap helper for the Casono client application.
*
* Responsibilities:
* - Parse CLI args (host:port, optional username)
* - Store username centrally so UI can reuse it
* - Create a shared ClientService and do startup LOGIN (optional)
* <p>
* Responsibilities: - Parse CLI args (host:port, optional username) - Store
* username centrally
* so UI can reuse it - Create a shared ClientService and do startup LOGIN
* (optional)
*/
public class ClientApp {
@@ -47,31 +48,40 @@ public class ClientApp {
}
public static void start(String arg, String username) {
String[] hostPort = parseHostAndPort(arg);
String host = hostPort[0];
int port = Integer.parseInt(hostPort[1]);
configureServerProperties(host, port);
setSharedUsername(username != null && !username.isBlank() ? username.trim() : null);
if (getSharedUsername() != null) {
performStartupLogin(host, port, username);
}
launchUI(arg, username);
}
private static String[] parseHostAndPort(String arg) {
String[] parts = arg.split(":", 2);
if (parts.length != 2) {
throw new IllegalArgumentException("The address must be in the format <ip>:<port>.");
}
String host = parts[0];
int port = Integer.parseInt(parts[1]);
LOGGER.info("You've selected the client. It will connect port {} at host {}", port, host);
System.setProperty("casono.server.host", host);
System.setProperty("casono.server.port", String.valueOf(port));
if (username != null && !username.isBlank()) {
setSharedUsername(username.trim());
} else {
setSharedUsername(null);
return parts;
}
if (username != null && !username.isBlank()) {
private static void configureServerProperties(String host, int port) {
LOGGER.info("You've selected the client. It will connect port {} at host {}", port, host);
System.setProperty("casono.server.host", host);
System.setProperty("casono.server.port", String.valueOf(port));
}
private static void performStartupLogin(String host, int port, String username) {
try {
ClientService clientService = new ClientService(host, port);
setSharedClientService(clientService);
java.util.concurrent.ExecutorService bg =
java.util.concurrent.Executors.newSingleThreadExecutor(
java.util.concurrent.ExecutorService bg = java.util.concurrent.Executors.newSingleThreadExecutor(
r -> {
Thread t = new Thread(r);
t.setDaemon(true);
@@ -85,7 +95,9 @@ public class ClientApp {
LobbyClient lobbyClient = new LobbyClient(clientService);
LoginResult res = lobbyClient.login(username);
if (res != null && res.getUsername() != null && !res.getUsername().isBlank()) {
if (res != null
&& res.getUsername() != null
&& !res.getUsername().isBlank()) {
setSharedUsername(res.getUsername().trim());
}
@@ -105,14 +117,14 @@ public class ClientApp {
LOGGER.warn(
"Could not establish initial connection for startup login: {}",
e.getMessage());
// UI will create its own connection when it initializes
}
}
private static void launchUI(String arg, String username) {
if (username != null && !username.isBlank()) {
Launcher.main(new String[] {arg, username});
Launcher.main(new String[] { arg, username });
} else {
Launcher.main(new String[] {arg});
Launcher.main(new String[] { arg });
}
}
@@ -121,8 +133,7 @@ public class ClientApp {
throw new IllegalArgumentException("Address argument required: <host:port>");
}
String username =
args.length > 1 && args[1] != null && !args[1].isBlank() ? args[1] : null;
String username = args.length > 1 && args[1] != null && !args[1].isBlank() ? args[1] : null;
start(args[0], username);
}
@@ -5,8 +5,8 @@ import ch.unibas.dmi.dbis.cs108.casono.server.network.transport.RawPacket;
import ch.unibas.dmi.dbis.cs108.casono.server.network.transport.TcpTransport;
import java.io.IOException;
import java.net.Socket;
import java.util.ArrayList;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
import java.util.Map;
@@ -26,8 +26,10 @@ 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 +43,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,8 +81,7 @@ public class ClientService {
private void startReaderThread() {
running.set(true);
readerThread =
new Thread(
readerThread = new Thread(
() -> {
while (running.get()) {
try {
@@ -107,11 +108,22 @@ public class ClientService {
String responseText = rp.payload();
logger.info("Raw message '{}'", responseText);
ParsedPacket parsed = parsePacketContent(responseText);
logger.debug(
"Parsed response lines (rid={}, success={}, openBlocks={}): {}",
rid,
parsed.success,
parsed.blockStack.size(),
parsed.lines);
handleParsedResponse(rid, parsed.success, parsed.lines);
}
private ParsedPacket parsePacketContent(String responseText) {
boolean hasStatus = false;
boolean success = false;
List<String> lines = new ArrayList<>();
Deque<String> blockStack = new ArrayDeque<>();
for (String rawLine : responseText.split("\\n")) {
@@ -119,20 +131,16 @@ public class ClientService {
String trimmed = line.trim();
if (!hasStatus) {
if ("+OK".equals(trimmed)) {
success = true;
StatusCheckResult status = checkStatus(trimmed);
if (status != null) {
success = status.success;
hasStatus = true;
continue;
}
if (trimmed.startsWith("-ERR") || trimmed.startsWith("-ERROR")) {
success = false;
hasStatus = true;
continue;
}
continue;
}
if (trimmed.isEmpty()) continue;
if (trimmed.isEmpty())
continue;
line = line.replaceFirst("^\\s+", "");
trimmed = line.trim();
@@ -149,7 +157,6 @@ public class ClientService {
lines.add("END");
continue;
}
break;
}
@@ -157,21 +164,34 @@ public class ClientService {
}
if (!hasStatus) {
success = inferStatus(responseText);
}
return new ParsedPacket(success, lines, blockStack);
}
private StatusCheckResult checkStatus(String trimmed) {
if ("+OK".equals(trimmed)) {
return new StatusCheckResult(true);
}
if (trimmed.startsWith("-ERR") || trimmed.startsWith("-ERROR")) {
return new StatusCheckResult(false);
}
return null;
}
private boolean inferStatus(String responseText) {
if (responseText.contains("+OK")) {
success = true;
hasStatus = true;
} else if (responseText.contains("-ERR") || responseText.contains("-ERROR")) {
success = false;
hasStatus = true;
} else {
success = true;
hasStatus = true;
return true;
}
if (responseText.contains("-ERR") || responseText.contains("-ERROR")) {
return false;
}
return true;
}
logger.debug("Parsed response lines (rid={}, success={}, openBlocks={}): {}",
rid, success, blockStack.size(), lines);
private void handleParsedResponse(int rid, boolean success, List<String> lines)
throws InterruptedException {
if (rid == 0) {
for (Consumer<List<String>> l : eventListeners) {
try {
@@ -190,6 +210,12 @@ public class ClientService {
}
}
private record ParsedPacket(boolean success, List<String> lines, Deque<String> blockStack) {
}
private record StatusCheckResult(boolean success) {
}
private boolean isContainerStart(String token) {
return token.equals("LOBBIES")
|| token.equals("LOBBY")
@@ -202,7 +228,8 @@ 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
@@ -215,19 +242,21 @@ 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.
@@ -238,8 +267,10 @@ 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.
@@ -264,10 +295,12 @@ 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);
@@ -278,13 +311,17 @@ 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) {
@@ -296,8 +333,7 @@ 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));
@@ -336,11 +372,15 @@ 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);
@@ -354,9 +394,12 @@ 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.
@@ -374,8 +417,10 @@ 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() {
@@ -13,17 +13,21 @@ import java.util.logging.Logger;
/**
* Fetches and parses game state from server.
*
* Protocol notes (based on your current server implementation):
* - Success replies contain: PHASE, POT, CURRENT_BET, DEALER, ACTIVE_PLAYER, then blocks:
* - CARD ... END (community cards on root level)
* - PLAYER ... (NAME/CHIPS/BET/STATE + optional CARD blocks for requesting player) ... END
* - Error replies contain: -ERR then CODE=..., MSG=..., END
* <p>
* Protocol notes (based on your current server implementation): - Success
* replies contain:
* PHASE, POT, CURRENT_BET, DEALER, ACTIVE_PLAYER, then blocks: - CARD ... END
* (community cards on
* root level) - PLAYER ... (NAME/CHIPS/BET/STATE + optional CARD blocks for
* requesting player) ...
* END - Error replies contain: -ERR then CODE=..., MSG=..., END
*
* Important:
* - The server does NOT wrap cards in a "CARDS" container.
* - CARD blocks appear either:
* - at root level -> community cards
* - inside PLAYER -> hole cards for that player (usually only for the requesting user)
* <p>
* Important: - The server does NOT wrap cards in a "CARDS" container. - CARD
* blocks appear
* either: - at root level -> community cards - inside PLAYER -> hole cards for
* that player (usually
* only for the requesting user)
*/
public class GameClient {
@@ -33,7 +37,8 @@ public class GameClient {
private final int gameId;
public GameClient(ClientService client, int gameId) {
if (client == null) throw new IllegalArgumentException("ClientService must not be null");
if (client == null)
throw new IllegalArgumentException("ClientService must not be null");
this.client = client;
this.gameId = gameId;
LOG.info(() -> "GameClient initialized for gameId=" + gameId);
@@ -72,10 +77,17 @@ public class GameClient {
try {
GameState state = parseGameState(joined);
LOG.info(() -> "Parsed state: phase=" + state.phase
+ " pot=" + state.pot
+ " players=" + (state.players != null ? state.players.size() : 0)
+ " community=" + (state.communityCards != null ? state.communityCards.size() : 0));
LOG.info(
() -> "Parsed state: phase="
+ state.phase
+ " pot="
+ state.pot
+ " players="
+ (state.players != null ? state.players.size() : 0)
+ " community="
+ (state.communityCards != null
? state.communityCards.size()
: 0));
return state;
} catch (Exception e) {
LOG.log(Level.SEVERE, "Failed parsing game state. Payload=\n" + joined, e);
@@ -101,115 +113,128 @@ public class GameClient {
private GameState parseGameState(String input) {
GameState s = new GameState();
s.players = new ArrayList<>();
s.communityCards = new ArrayList<>();
if (s.players == null) s.players = new ArrayList<>();
if (s.communityCards == null) s.communityCards = new ArrayList<>();
Player currentPlayer = null;
Card currentCard = null;
boolean insidePlayer = false;
ParseState state = new ParseState();
for (String raw : input.split("\n")) {
String line = raw.trim();
if (line.isEmpty()) continue;
if (line.startsWith("+OK")) continue;
// Global fields
if (line.startsWith("PHASE=")) {
s.phase = value(line);
continue;
}
if (line.startsWith("POT=")) {
s.pot = intVal(line);
continue;
}
if (line.startsWith("CURRENT_BET=")) {
s.currentBet = intVal(line);
continue;
}
if (line.startsWith("DEALER=")) {
s.dealer = intVal(line);
continue;
}
if (line.startsWith("ACTIVE_PLAYER=")) {
s.activePlayer = intVal(line);
continue;
}
if (line.startsWith("WINNER=")) {
s.winnerIndex = intVal(line);
continue;
}
if (line.equals("PLAYER")) {
currentPlayer = new Player();
s.players.add(currentPlayer);
currentCard = null;
insidePlayer = true;
continue;
}
if (line.equals("CARDS")) {
continue;
}
if (line.equals("CARD")) {
currentCard = new Card("", "");
if (insidePlayer && currentPlayer != null) {
currentPlayer.addCard(currentCard); // hole card
} else {
s.communityCards.add(currentCard); // community card
}
continue;
}
if (line.startsWith("VALUE=") && currentCard != null) {
currentCard.setValue(value(line));
continue;
}
if (line.startsWith("SUIT=") && currentCard != null) {
currentCard.setSuit(value(line));
continue;
}
if (line.equals("END")) {
if (currentCard != null) { // schließt CARD
currentCard = null;
continue;
}
if (insidePlayer) { // schließt PLAYER
insidePlayer = false;
currentPlayer = null;
continue;
}
continue; // root END
}
if (currentPlayer != null) {
if (line.startsWith("USERNAME=") || line.startsWith("NAME=")) {
currentPlayer.setId(PlayerId.of(value(line)));
continue;
}
if (line.startsWith("CHIPS=")) {
currentPlayer.setChips(intVal(line));
continue;
}
if (line.startsWith("BET=")) {
currentPlayer.setBet(intVal(line));
continue;
}
if (line.startsWith("STATE=")) {
currentPlayer.setState(parseState(value(line)));
continue;
}
}
if (line.isEmpty() || line.startsWith("+OK"))
continue;
if (!parseGlobalField(s, line)
&& !parsePlayerStructure(s, state, line)
&& !parseCardData(state, line)
&& !parsePlayerData(state, line)) {
LOG.fine(() -> "Ignored line: '" + line + "'");
}
}
return s;
}
private boolean parseGlobalField(GameState s, String line) {
return switch (line) {
case String l when l.startsWith("PHASE=") -> {
s.phase = value(l);
yield true;
}
case String l when l.startsWith("POT=") -> {
s.pot = intVal(l);
yield true;
}
case String l when l.startsWith("CURRENT_BET=") -> {
s.currentBet = intVal(l);
yield true;
}
case String l when l.startsWith("DEALER=") -> {
s.dealer = intVal(l);
yield true;
}
case String l when l.startsWith("ACTIVE_PLAYER=") -> {
s.activePlayer = intVal(l);
yield true;
}
case String l when l.startsWith("WINNER=") -> {
s.winnerIndex = intVal(l);
yield true;
}
default -> false;
};
}
private boolean parsePlayerStructure(GameState s, ParseState state, String line) {
if (line.equals("PLAYER")) {
state.currentPlayer = new Player();
s.players.add(state.currentPlayer);
state.currentCard = null;
state.insidePlayer = true;
return true;
}
if (line.equals("CARDS"))
return true;
if (line.equals("CARD")) {
state.currentCard = new Card("", "");
if (state.insidePlayer && state.currentPlayer != null) {
state.currentPlayer.addCard(state.currentCard);
} else {
s.communityCards.add(state.currentCard);
}
return true;
}
if (line.equals("END")) {
if (state.currentCard != null) {
state.currentCard = null;
} else if (state.insidePlayer) {
state.insidePlayer = false;
state.currentPlayer = null;
}
return true;
}
return false;
}
private boolean parseCardData(ParseState state, String line) {
if (state.currentCard == null)
return false;
if (line.startsWith("VALUE=")) {
state.currentCard.setValue(value(line));
return true;
}
if (line.startsWith("SUIT=")) {
state.currentCard.setSuit(value(line));
return true;
}
return false;
}
private boolean parsePlayerData(ParseState state, String line) {
if (state.currentPlayer == null)
return false;
if (line.startsWith("USERNAME=") || line.startsWith("NAME=")) {
state.currentPlayer.setId(PlayerId.of(value(line)));
return true;
}
if (line.startsWith("CHIPS=")) {
state.currentPlayer.setChips(intVal(line));
return true;
}
if (line.startsWith("BET=")) {
state.currentPlayer.setBet(intVal(line));
return true;
}
if (line.startsWith("STATE=")) {
state.currentPlayer.setState(parseState(value(line)));
return true;
}
return false;
}
private static class ParseState {
Player currentPlayer;
Card currentCard;
boolean insidePlayer;
}
private static String value(String line) {
return line.split("=", 2)[1];
}
@@ -219,7 +244,8 @@ public class GameClient {
}
private static PlayerState parseState(String s) {
if (s == null) return PlayerState.ACTIVE;
if (s == null)
return PlayerState.ACTIVE;
return switch (s.trim().toUpperCase()) {
case "ACTIVE" -> PlayerState.ACTIVE;
case "FOLDED" -> PlayerState.FOLDED;
@@ -229,7 +255,8 @@ public class GameClient {
}
private static String extractValue(String joined, String key) {
if (joined == null) return null;
if (joined == null)
return null;
for (String raw : joined.split("\n")) {
String line = raw.trim();
if (line.startsWith(key + "=")) {