Feat: add offline mode to prevent errors when trying to send requests to not connected server

This commit is contained in:
Jona Walpert
2026-04-08 13:29:39 +02:00
parent ca782b389e
commit d855f2d568
2 changed files with 116 additions and 10 deletions
@@ -26,6 +26,7 @@ public class ClientService {
private final Socket socket; private final Socket socket;
private final ExecutorService executor; private final ExecutorService executor;
private final boolean offlineMode;
public static ArrayList<String> response; public static ArrayList<String> response;
private final AtomicInteger idGenerator; private final AtomicInteger idGenerator;
@@ -42,6 +43,7 @@ public class ClientService {
this.idGenerator = new AtomicInteger(0); this.idGenerator = new AtomicInteger(0);
this.offlineMode = false;
try { try {
socket = new Socket(ip, port); socket = new Socket(ip, port);
clienttcptransport = new TcpTransport(socket); clienttcptransport = new TcpTransport(socket);
@@ -52,6 +54,27 @@ public class ClientService {
executor = Executors.newSingleThreadExecutor(); executor = Executors.newSingleThreadExecutor();
} }
/**
* 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
*/
public ClientService(boolean offline) {
this.idGenerator = new AtomicInteger(0);
this.offlineMode = offline;
this.socket = null;
this.clienttcptransport = null;
this.executor = Executors.newSingleThreadExecutor();
}
/**
* Returns true if this ClientService is running in offline mode (no network).
*/
public boolean isOffline() {
return offlineMode;
}
/** /**
* Sends a command to the server and waits for the response. The command is * Sends a command to the server and waits for the response. The command is
* sent using the TcpTransport, and the response is read in a loop until a * sent using the TcpTransport, and the response is read in a loop until a
@@ -62,6 +85,9 @@ public class ClientService {
* @return The response from the server as a string. * @return The response from the server as a string.
*/ */
protected String processCommand(String message) { protected String processCommand(String message) {
if (offlineMode) {
throw new RuntimeException("ClientService is offline: cannot process command");
}
AtomicReference<String> response = new AtomicReference<>(); AtomicReference<String> response = new AtomicReference<>();
sendRequest(() -> { sendRequest(() -> {
try { try {
@@ -135,8 +161,12 @@ public class ClientService {
public void closeSocket() { public void closeSocket() {
try { try {
executor.shutdown(); executor.shutdown();
if (clienttcptransport != null) {
clienttcptransport.close(); clienttcptransport.close();
}
if (socket != null) {
socket.close(); socket.close();
}
} catch (IOException j) { } catch (IOException j) {
System.out.println(j); System.out.println(j);
} }
@@ -3,6 +3,9 @@ package ch.unibas.dmi.dbis.cs108.casono.client.ui.lobbyui;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.scene.control.Button; import javafx.scene.control.Button;
import javafx.scene.control.TextField;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.control.Label; import javafx.scene.control.Label;
import javafx.scene.image.Image; import javafx.scene.image.Image;
import javafx.scene.image.ImageView; import javafx.scene.image.ImageView;
@@ -12,6 +15,7 @@ import javafx.scene.shape.Rectangle;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService; import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService;
import ch.unibas.dmi.dbis.cs108.casono.client.network.LobbyClient;
/** /**
* Controller for the Casono main UI lobby. Handles UI initialization and user * Controller for the Casono main UI lobby. Handles UI initialization and user
@@ -20,17 +24,29 @@ import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService;
public class CasinomainuiController { public class CasinomainuiController {
private static final Logger LOGGER = LogManager.getLogger(CasinomainuiController.class); private static final Logger LOGGER = LogManager.getLogger(CasinomainuiController.class);
@FXML private AnchorPane rootPane; @FXML
@FXML private Label titleLabel; private AnchorPane rootPane;
@FXML private Label subtitleLabel; @FXML
@FXML private ImageView logoView; private Label titleLabel;
@FXML private Rectangle greenBox; @FXML
@FXML private Button exitbutton; private Label subtitleLabel;
@FXML private VBox casinoTable; @FXML
private ImageView logoView;
@FXML
private Rectangle greenBox;
@FXML
private Button exitbutton;
@FXML
private VBox casinoTable;
@FXML
private TextField usernameField;
@FXML
private Button loginButton;
private LobbyButtonTranslationManager translationManager; private LobbyButtonTranslationManager translationManager;
private LobbyButtonGridManager gridManager; private LobbyButtonGridManager gridManager;
private int nextButtonId = 1; private int nextButtonId = 1;
private LobbyClient lobbyClient;
/** Default constructor for dependency injection by FXMLLoader. */ /** Default constructor for dependency injection by FXMLLoader. */
public CasinomainuiController() { public CasinomainuiController() {
@@ -47,8 +63,60 @@ public class CasinomainuiController {
translationManager = LobbyButtonTranslationManager.getInstance(); translationManager = LobbyButtonTranslationManager.getInstance();
String host = System.getProperty("casono.server.host"); String host = System.getProperty("casono.server.host");
int port = Integer.parseInt(System.getProperty("casono.server.port")); int port = Integer.parseInt(System.getProperty("casono.server.port"));
ClientService clientService = new ClientService(host, port); ClientService clientService;
try {
clientService = new ClientService(host, port);
} catch (RuntimeException e) {
LOGGER.warn("Could not connect to server {}:{} — starting in offline mode: {}", host, port, e.getMessage());
clientService = new ClientService(true); // offline mode
}
gridManager = new LobbyButtonGridManager(new javafx.scene.layout.GridPane(), translationManager, clientService); gridManager = new LobbyButtonGridManager(new javafx.scene.layout.GridPane(), translationManager, clientService);
// LobbyClient will use the provided ClientService; in offline mode calls will
// fail with RuntimeException
lobbyClient = new LobbyClient(clientService);
casinoTable.getChildren().clear();
casinoTable.getChildren().add(gridManager.getGridPane());
gridManager.renderLobbyButtons();
}
/**
* Handles the login button action. Validates input and calls
* LobbyClient.login().
*/
@FXML
public void handleLoginButton() {
String username = usernameField.getText();
if (username == null || username.isBlank()) {
showAlert("Please enter a username.");
return;
}
// Only allow alphanumeric, _ and -
if (!username.matches("[a-zA-Z0-9_-]+")) {
showAlert("Only letters, numbers, '_' and '-' are allowed!");
return;
}
if (lobbyClient.getClientService().isOffline()) {
showAlert("Offline mode: cannot send login to server.");
return;
}
try {
lobbyClient.login(username);
showAlert("Login sent: " + username);
} catch (RuntimeException e) {
LOGGER.error("Login failed: {}", e.getMessage());
showAlert("Login failed: " + e.getMessage());
}
}
/**
* Shows an alert dialog with the given message.
*/
private void showAlert(String message) {
Alert alert = new Alert(AlertType.INFORMATION);
alert.setTitle("Info");
alert.setHeaderText(null);
alert.setContentText(message);
alert.showAndWait();
casinoTable.getChildren().clear(); casinoTable.getChildren().clear();
casinoTable.getChildren().add(gridManager.getGridPane()); casinoTable.getChildren().add(gridManager.getGridPane());
gridManager.renderLobbyButtons(); gridManager.renderLobbyButtons();
@@ -69,6 +137,14 @@ public class CasinomainuiController {
} }
int buttonId = nextButtonId++; int buttonId = nextButtonId++;
try { try {
String username = usernameField != null ? usernameField.getText() : "<unknown>";
LOGGER.info("Creating lobby for user: {}", username);
// avoid attempting to create a lobby when offline
if (lobbyClient.getClientService().isOffline()) {
LOGGER.warn("Cannot create lobby while offline");
showAlert("Offline mode: cannot create lobby.");
return;
}
int lobbyId = gridManager.createLobby(); int lobbyId = gridManager.createLobby();
translationManager.addLobbyButton(buttonId, lobbyId); translationManager.addLobbyButton(buttonId, lobbyId);
LOGGER.info("ButtonID: {}, LobbyID: {}", buttonId, lobbyId); LOGGER.info("ButtonID: {}, LobbyID: {}", buttonId, lobbyId);