Feat/chat tests #317
@@ -4,8 +4,10 @@ import ch.unibas.dmi.dbis.cs108.casono.client.ClientApp;
|
|||||||
import ch.unibas.dmi.dbis.cs108.casono.client.network.ChatClient;
|
import ch.unibas.dmi.dbis.cs108.casono.client.network.ChatClient;
|
||||||
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.ui.chatui.ChatBoxController;
|
import ch.unibas.dmi.dbis.cs108.casono.client.ui.chatui.ChatBoxController;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.client.ui.chatui.ChatViewController;
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.RequestParameter;
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.RequestParameter;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -39,7 +41,11 @@ public class ChatController {
|
|||||||
return chatBoxController;
|
return chatBoxController;
|
||||||
}
|
}
|
||||||
|
|
||||||
private final ChatBoxController chatBoxController;
|
public void setChatBoxController(ChatBoxController chatBoxController) {
|
||||||
|
this.chatBoxController = chatBoxController;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ChatBoxController chatBoxController;
|
||||||
private int lobbyId = -1;
|
private int lobbyId = -1;
|
||||||
private final Timer timer;
|
private final Timer timer;
|
||||||
private final Consumer<List<String>> serverEventListener;
|
private final Consumer<List<String>> serverEventListener;
|
||||||
@@ -61,6 +67,12 @@ public class ChatController {
|
|||||||
/** List of all users connected to the server, to safe them locally on the client */
|
/** List of all users connected to the server, to safe them locally on the client */
|
||||||
private final List<String> localUserList;
|
private final List<String> localUserList;
|
||||||
|
|
||||||
|
public List<String> getLocalUserList() {
|
||||||
|
return this.localUserList;
|
||||||
|
}
|
||||||
|
|
||||||
|
public final Map<ChatController.ChatKey, ChatViewController> activeChatControllers;
|
||||||
|
|
||||||
private final Logger logger;
|
private final Logger logger;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -78,6 +90,7 @@ public class ChatController {
|
|||||||
this.chatBoxController = new ChatBoxController(username, this);
|
this.chatBoxController = new ChatBoxController(username, this);
|
||||||
this.logger = LogManager.getLogger(ChatController.class);
|
this.logger = LogManager.getLogger(ChatController.class);
|
||||||
this.serverEventListener = this::handleServerEvent;
|
this.serverEventListener = this::handleServerEvent;
|
||||||
|
this.activeChatControllers = new HashMap<>();
|
||||||
|
|
||||||
registerAsActiveController(clientService);
|
registerAsActiveController(clientService);
|
||||||
clientService.addEventListener(serverEventListener);
|
clientService.addEventListener(serverEventListener);
|
||||||
@@ -126,7 +139,7 @@ public class ChatController {
|
|||||||
}
|
}
|
||||||
ChatModel lobbyChatModel = new ChatModel(ChatType.LOBBY, username, lobbyId, null);
|
ChatModel lobbyChatModel = new ChatModel(ChatType.LOBBY, username, lobbyId, null);
|
||||||
chatModelMap.put(key, lobbyChatModel);
|
chatModelMap.put(key, lobbyChatModel);
|
||||||
this.chatBoxController.addChatTab("Lobby", lobbyChatModel);
|
this.chatBoxController.addChatTab("LOBBY", lobbyChatModel, ChatType.LOBBY);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+39
-11
@@ -3,6 +3,7 @@ package ch.unibas.dmi.dbis.cs108.casono.client.ui.chatui;
|
|||||||
import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatController;
|
import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatController;
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatModel;
|
import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatModel;
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatType;
|
import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatType;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.client.chat.Message;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -27,7 +28,7 @@ public class ChatBoxController {
|
|||||||
|
|
||||||
private String username;
|
private String username;
|
||||||
|
|
||||||
private ChatController chatController;
|
private final ChatController chatController;
|
||||||
|
|
||||||
@FXML private VBox chatBox;
|
@FXML private VBox chatBox;
|
||||||
|
|
||||||
@@ -43,8 +44,6 @@ public class ChatBoxController {
|
|||||||
|
|
||||||
@FXML private final Map<String, Tab> usernameTabMap;
|
@FXML private final Map<String, Tab> usernameTabMap;
|
||||||
|
|
||||||
private String ressource = "/ui-structure/components/chatui/chattab.fxml";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor for the ChatBoxController, initializes the necessary fields and data structures
|
* Constructor for the ChatBoxController, initializes the necessary fields and data structures
|
||||||
* for managing chat tabs and whisper chats.
|
* for managing chat tabs and whisper chats.
|
||||||
@@ -73,11 +72,10 @@ public class ChatBoxController {
|
|||||||
*/
|
*/
|
||||||
@FXML
|
@FXML
|
||||||
public void initialize() {
|
public void initialize() {
|
||||||
ChatModel globalChatModel = new ChatModel(ChatType.GLOBAL, username, -1, null);
|
ChatType global = ChatType.GLOBAL;
|
||||||
chatController
|
ChatModel globalChatModel = new ChatModel(global, username, -1, null);
|
||||||
.getChatModelMap()
|
chatController.getChatModelMap().put(new ChatController.ChatKey(global), globalChatModel);
|
||||||
.put(new ChatController.ChatKey(ChatType.GLOBAL), globalChatModel);
|
addChatTab("GLOBAL", globalChatModel, global);
|
||||||
addChatTab("GLOBAL", globalChatModel);
|
|
||||||
addWhisperChatButton.setOnAction(event -> addWhisperChatButton.show());
|
addWhisperChatButton.setOnAction(event -> addWhisperChatButton.show());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,10 +164,11 @@ public class ChatBoxController {
|
|||||||
public void addWhisperChat(String target, ChatModel chatModel) {
|
public void addWhisperChat(String target, ChatModel chatModel) {
|
||||||
if (!activeWhisperChats.contains(target)) {
|
if (!activeWhisperChats.contains(target)) {
|
||||||
activeWhisperChats.add(target);
|
activeWhisperChats.add(target);
|
||||||
|
ChatType whisper = ChatType.WHISPER;
|
||||||
chatController
|
chatController
|
||||||
.getChatModelMap()
|
.getChatModelMap()
|
||||||
.put(new ChatController.ChatKey(ChatType.WHISPER, target), chatModel);
|
.put(new ChatController.ChatKey(whisper, target), chatModel);
|
||||||
addChatTab(target, chatModel);
|
addChatTab(target, chatModel, whisper);
|
||||||
} else {
|
} else {
|
||||||
chatTabPane.getSelectionModel().select(usernameTabMap.get(target));
|
chatTabPane.getSelectionModel().select(usernameTabMap.get(target));
|
||||||
}
|
}
|
||||||
@@ -184,7 +183,8 @@ public class ChatBoxController {
|
|||||||
* @param chatModel The {@link ChatModel} containing the data and logic for this specific chat.
|
* @param chatModel The {@link ChatModel} containing the data and logic for this specific chat.
|
||||||
* @throws RuntimeException If the FXML resource for the chat tab cannot be loaded.
|
* @throws RuntimeException If the FXML resource for the chat tab cannot be loaded.
|
||||||
*/
|
*/
|
||||||
public void addChatTab(String title, ChatModel chatModel) {
|
public void addChatTab(String title, ChatModel chatModel, ChatType chatType) {
|
||||||
|
String ressource = "/ui-structure/components/chatui/chattab.fxml";
|
||||||
URL resource = getClass().getResource(ressource);
|
URL resource = getClass().getResource(ressource);
|
||||||
FXMLLoader fxmlLoader = new FXMLLoader(resource);
|
FXMLLoader fxmlLoader = new FXMLLoader(resource);
|
||||||
runOnPlatformSynchronized(
|
runOnPlatformSynchronized(
|
||||||
@@ -193,6 +193,8 @@ public class ChatBoxController {
|
|||||||
ChatViewController chatViewController =
|
ChatViewController chatViewController =
|
||||||
new ChatViewController(
|
new ChatViewController(
|
||||||
this.chatController, chatModel, this.username);
|
this.chatController, chatModel, this.username);
|
||||||
|
chatController.activeChatControllers.put(
|
||||||
|
new ChatController.ChatKey(chatType), chatViewController);
|
||||||
fxmlLoader.setController(chatViewController);
|
fxmlLoader.setController(chatViewController);
|
||||||
Node load = fxmlLoader.load();
|
Node load = fxmlLoader.load();
|
||||||
VBox.setVgrow(load, Priority.ALWAYS);
|
VBox.setVgrow(load, Priority.ALWAYS);
|
||||||
@@ -239,4 +241,30 @@ public class ChatBoxController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void loadChats() {
|
||||||
|
Map<ChatController.ChatKey, ChatModel> chatModelMap = chatController.getChatModelMap();
|
||||||
|
ChatController.ChatKey key = new ChatController.ChatKey(ChatType.GLOBAL);
|
||||||
|
ChatModel global = chatModelMap.get(key);
|
||||||
|
ChatViewController globalController = chatController.activeChatControllers.get(key);
|
||||||
|
|
||||||
|
for (String user : chatController.getLocalUserList()) {
|
||||||
|
addWhisperUser(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Message msg : global.messages) {
|
||||||
|
globalController.showMessage(msg);
|
||||||
|
}
|
||||||
|
for (String user : chatController.getLocalUserList()) {
|
||||||
|
ChatController.ChatKey whisperUserKey =
|
||||||
|
new ChatController.ChatKey(ChatType.WHISPER, user);
|
||||||
|
if (chatModelMap.containsKey(whisperUserKey)) {
|
||||||
|
ChatModel whisperChatModel = chatModelMap.get(whisperUserKey);
|
||||||
|
addWhisperChat(user, whisperChatModel);
|
||||||
|
for (Message msg : whisperChatModel.messages) {
|
||||||
|
chatController.activeChatControllers.get(whisperUserKey).showMessage(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+23
-29
@@ -18,15 +18,13 @@ import java.util.concurrent.CompletableFuture;
|
|||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
import javafx.fxml.FXML;
|
import javafx.fxml.FXML;
|
||||||
import javafx.fxml.FXMLLoader;
|
import javafx.fxml.FXMLLoader;
|
||||||
import javafx.scene.Parent;
|
import javafx.scene.Node;
|
||||||
import javafx.scene.Scene;
|
|
||||||
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;
|
||||||
import javafx.scene.layout.AnchorPane;
|
import javafx.scene.layout.AnchorPane;
|
||||||
import javafx.scene.layout.HBox;
|
import javafx.scene.layout.HBox;
|
||||||
import javafx.scene.layout.VBox;
|
import javafx.scene.layout.VBox;
|
||||||
import javafx.stage.Stage;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Controller for the casino gaming area.
|
* Controller for the casino gaming area.
|
||||||
@@ -67,7 +65,9 @@ public class CasinoGameController {
|
|||||||
private java.util.List<String> lastCommunityKeys = java.util.List.of();
|
private java.util.List<String> lastCommunityKeys = java.util.List.of();
|
||||||
private java.util.List<String> lastMyCardKeys = java.util.List.of();
|
private java.util.List<String> lastMyCardKeys = java.util.List.of();
|
||||||
private int lastPot = Integer.MIN_VALUE;
|
private int lastPot = Integer.MIN_VALUE;
|
||||||
|
private ChatController chatController;
|
||||||
|
|
||||||
|
public static final double CHAT_CONTAINER_CONSTANT = 6.0;
|
||||||
private static final int TOTAL_SLOTS = 5;
|
private static final int TOTAL_SLOTS = 5;
|
||||||
private static final int PLAYER_SLOTS = 2;
|
private static final int PLAYER_SLOTS = 2;
|
||||||
private int pot = 0;
|
private int pot = 0;
|
||||||
@@ -139,7 +139,6 @@ public class CasinoGameController {
|
|||||||
private static final double CHAT_WIDTH = 400;
|
private static final double CHAT_WIDTH = 400;
|
||||||
private static final double CHAT_HEIGHT = 600;
|
private static final double CHAT_HEIGHT = 600;
|
||||||
|
|
||||||
private ChatController chatController;
|
|
||||||
private String chatUsername;
|
private String chatUsername;
|
||||||
private ClientService chatClientService;
|
private ClientService chatClientService;
|
||||||
private int chatLobbyId = -1;
|
private int chatLobbyId = -1;
|
||||||
@@ -197,6 +196,11 @@ public class CasinoGameController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void startChat(ChatController chatController) {
|
||||||
|
this.chatController = chatController;
|
||||||
|
initializeChatIfPossible();
|
||||||
|
}
|
||||||
|
|
||||||
/** Set the PlayerId of the current player. */
|
/** Set the PlayerId of the current player. */
|
||||||
@FXML
|
@FXML
|
||||||
public void initialize() {
|
public void initialize() {
|
||||||
@@ -239,7 +243,8 @@ public class CasinoGameController {
|
|||||||
// empty display only (optional)
|
// empty display only (optional)
|
||||||
renderCommunityCards(List.of());
|
renderCommunityCards(List.of());
|
||||||
renderPlayerCards(List.of());
|
renderPlayerCards(List.of());
|
||||||
initializeChatIfPossible();
|
|
||||||
|
chatContainer.toFront();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -257,7 +262,6 @@ public class CasinoGameController {
|
|||||||
this.chatUsername = username;
|
this.chatUsername = username;
|
||||||
this.chatClientService = clientService;
|
this.chatClientService = clientService;
|
||||||
this.chatLobbyId = lobbyId;
|
this.chatLobbyId = lobbyId;
|
||||||
initializeChatIfPossible();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -265,42 +269,32 @@ public class CasinoGameController {
|
|||||||
* is available and the chat has not already been initialized.
|
* is available and the chat has not already been initialized.
|
||||||
*/
|
*/
|
||||||
private void initializeChatIfPossible() {
|
private void initializeChatIfPossible() {
|
||||||
if (chatInitialized || chatClientService == null || chatUsername == null) {
|
if (chatInitialized
|
||||||
|
|| chatClientService == null
|
||||||
|
|| chatUsername == null
|
||||||
|
|| chatController == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
chatController = new ChatController(chatUsername, chatClientService);
|
chatController.updateUsername(chatUsername);
|
||||||
|
|
||||||
URL resource = getClass().getResource("/ui-structure/components/chatui/chatbox.fxml");
|
URL resource = getClass().getResource("/ui-structure/components/chatui/chatbox.fxml");
|
||||||
FXMLLoader loader = new FXMLLoader(resource);
|
FXMLLoader loader = new FXMLLoader(resource);
|
||||||
loader.setController(chatController.getChatBoxController());
|
loader.setController(chatController.getChatBoxController());
|
||||||
|
Node chatNode = loader.load();
|
||||||
|
chatContainer.getChildren().setAll(chatNode);
|
||||||
|
AnchorPane.setTopAnchor(chatNode, CHAT_CONTAINER_CONSTANT);
|
||||||
|
AnchorPane.setBottomAnchor(chatNode, CHAT_CONTAINER_CONSTANT);
|
||||||
|
AnchorPane.setLeftAnchor(chatNode, 0.0);
|
||||||
|
AnchorPane.setRightAnchor(chatNode, CHAT_CONTAINER_CONSTANT);
|
||||||
|
|
||||||
Parent root = loader.load();
|
chatController.getChatBoxController().loadChats();
|
||||||
|
chatInitialized = true;
|
||||||
Stage chatStage = new Stage();
|
|
||||||
|
|
||||||
chatStage.setTitle("Casono");
|
|
||||||
|
|
||||||
String iconPath = getClass().getResource("/images/logoinverted.png").toExternalForm();
|
|
||||||
chatStage.getIcons().add(new Image(iconPath));
|
|
||||||
|
|
||||||
Scene scene = new Scene(root);
|
|
||||||
chatStage.setScene(scene);
|
|
||||||
|
|
||||||
chatStage.setWidth(CHAT_WIDTH);
|
|
||||||
chatStage.setHeight(CHAT_HEIGHT);
|
|
||||||
|
|
||||||
chatStage.setOnCloseRequest(event -> chatInitialized = false);
|
|
||||||
|
|
||||||
chatStage.show();
|
|
||||||
|
|
||||||
if (chatLobbyId >= 0) {
|
if (chatLobbyId >= 0) {
|
||||||
chatController.setLobbyChat(chatLobbyId);
|
chatController.setLobbyChat(chatLobbyId);
|
||||||
}
|
}
|
||||||
|
|
||||||
chatInitialized = true;
|
|
||||||
|
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
LOGGER.warning("Could not initialize game chat UI: " + e.getMessage());
|
LOGGER.warning("Could not initialize game chat UI: " + e.getMessage());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui;
|
package ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui;
|
||||||
|
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.client.ClientApp;
|
import ch.unibas.dmi.dbis.cs108.casono.client.ClientApp;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatController;
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.client.game.GameService;
|
import ch.unibas.dmi.dbis.cs108.casono.client.game.GameService;
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.client.game.PlayerId;
|
import ch.unibas.dmi.dbis.cs108.casono.client.game.PlayerId;
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService;
|
import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService;
|
||||||
@@ -25,6 +26,7 @@ public class CasinoGameUI extends Application {
|
|||||||
private static final Logger LOG = Logger.getLogger(CasinoGameUI.class.getName());
|
private static final Logger LOG = Logger.getLogger(CasinoGameUI.class.getName());
|
||||||
|
|
||||||
private static ClientService clientService;
|
private static ClientService clientService;
|
||||||
|
private static ChatController chatController;
|
||||||
|
|
||||||
private static String username;
|
private static String username;
|
||||||
private static int lobbyId = -1;
|
private static int lobbyId = -1;
|
||||||
@@ -56,6 +58,10 @@ public class CasinoGameUI extends Application {
|
|||||||
CasinoGameUI.username = username;
|
CasinoGameUI.username = username;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void setChatController(ChatController chatController) {
|
||||||
|
CasinoGameUI.chatController = chatController;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the lobby ID to be used by the application.
|
* Sets the lobby ID to be used by the application.
|
||||||
*
|
*
|
||||||
@@ -124,6 +130,8 @@ public class CasinoGameUI extends Application {
|
|||||||
controller.setMyPlayerId(PlayerId.of(effectiveUsername));
|
controller.setMyPlayerId(PlayerId.of(effectiveUsername));
|
||||||
controller.setChatContext(effectiveUsername, clientService, lobbyId);
|
controller.setChatContext(effectiveUsername, clientService, lobbyId);
|
||||||
|
|
||||||
|
controller.startChat(chatController);
|
||||||
|
|
||||||
Scene scene = new Scene(root, DEFAULT_WIDTH, DEFAULT_HEIGHT);
|
Scene scene = new Scene(root, DEFAULT_WIDTH, DEFAULT_HEIGHT);
|
||||||
stage.setTitle("Casono");
|
stage.setTitle("Casono");
|
||||||
|
|
||||||
|
|||||||
+1
@@ -141,6 +141,7 @@ public class CasinomainuiController {
|
|||||||
AnchorPane.setBottomAnchor(chatNode, 0.0);
|
AnchorPane.setBottomAnchor(chatNode, 0.0);
|
||||||
AnchorPane.setLeftAnchor(chatNode, 0.0);
|
AnchorPane.setLeftAnchor(chatNode, 0.0);
|
||||||
AnchorPane.setRightAnchor(chatNode, 0.0);
|
AnchorPane.setRightAnchor(chatNode, 0.0);
|
||||||
|
gridManager.setChatController(chatController);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
LOGGER.warn("Could not initialize lobby chat UI: {}", e.getMessage());
|
LOGGER.warn("Could not initialize lobby chat UI: {}", e.getMessage());
|
||||||
}
|
}
|
||||||
|
|||||||
+8
@@ -1,6 +1,7 @@
|
|||||||
package ch.unibas.dmi.dbis.cs108.casono.client.ui.lobbyui;
|
package ch.unibas.dmi.dbis.cs108.casono.client.ui.lobbyui;
|
||||||
|
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.client.ClientApp;
|
import ch.unibas.dmi.dbis.cs108.casono.client.ClientApp;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatController;
|
||||||
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;
|
import ch.unibas.dmi.dbis.cs108.casono.client.network.LobbyClient;
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.CasinoGameUI;
|
import ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.CasinoGameUI;
|
||||||
@@ -43,6 +44,8 @@ public class LobbyButtonGridManager {
|
|||||||
private final LobbyButtonTranslationManager translationManager;
|
private final LobbyButtonTranslationManager translationManager;
|
||||||
private final LobbyClient lobbyClient;
|
private final LobbyClient lobbyClient;
|
||||||
|
|
||||||
|
private ChatController chatController;
|
||||||
|
|
||||||
private final ConcurrentHashMap<String, Image> imageCache = new ConcurrentHashMap<>();
|
private final ConcurrentHashMap<String, Image> imageCache = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
private final ExecutorService executor = Executors.newCachedThreadPool();
|
private final ExecutorService executor = Executors.newCachedThreadPool();
|
||||||
@@ -557,6 +560,7 @@ public class LobbyButtonGridManager {
|
|||||||
|
|
||||||
CasinoGameUI.setClientService(cs);
|
CasinoGameUI.setClientService(cs);
|
||||||
CasinoGameUI.setLobbyId(lobbyId);
|
CasinoGameUI.setLobbyId(lobbyId);
|
||||||
|
CasinoGameUI.setChatController(chatController);
|
||||||
|
|
||||||
String username = ClientApp.getSharedUsername();
|
String username = ClientApp.getSharedUsername();
|
||||||
if (username == null || username.isBlank()) {
|
if (username == null || username.isBlank()) {
|
||||||
@@ -586,4 +590,8 @@ public class LobbyButtonGridManager {
|
|||||||
public LobbyClient getLobbyClient() {
|
public LobbyClient getLobbyClient() {
|
||||||
return lobbyClient;
|
return lobbyClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setChatController(ChatController chatController) {
|
||||||
|
this.chatController = chatController;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.ping.PingRequest;
|
|||||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.send_message.SendMessageHandler;
|
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.send_message.SendMessageHandler;
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.send_message.SendMessageParser;
|
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.send_message.SendMessageParser;
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.send_message.SendMessageRequest;
|
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.send_message.SendMessageRequest;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.LobbyCleanupJob;
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.LobbyManager;
|
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.LobbyManager;
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserCleanupJob;
|
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.domain.user.UserRegistry;
|
||||||
@@ -73,11 +74,7 @@ import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandR
|
|||||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.CommandParserDispatcher;
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.CommandParserDispatcher;
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.events.DisconnectEvent;
|
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.events.EventBus;
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
|
|
||||||
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.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.SessionDisconnectJob;
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionDisconnectJob;
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionManager;
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionManager;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
@@ -141,37 +138,12 @@ public class ServerApp {
|
|||||||
userRegistry,
|
userRegistry,
|
||||||
new CommandContext(lobbyManager, sessionManager));
|
new CommandContext(lobbyManager, sessionManager));
|
||||||
|
|
||||||
// Periodic cleanup: remove empty lobbies older than 30s and notify affected
|
|
||||||
// users
|
|
||||||
scheduler.scheduleAtFixedRate(
|
scheduler.scheduleAtFixedRate(
|
||||||
() -> {
|
new LobbyCleanupJob(
|
||||||
try {
|
lobbyManager,
|
||||||
var expired =
|
sessionManager,
|
||||||
lobbyManager.findEmptyLobbiesOlderThan(
|
responseDispatcher,
|
||||||
Duration.ofSeconds(LOBBY_EXPIRY_SECONDS));
|
Duration.ofSeconds(LOBBY_EXPIRY_SECONDS)),
|
||||||
for (var lid : expired) {
|
|
||||||
// remove lobby from manager first
|
|
||||||
lobbyManager.removeLobby(lid);
|
|
||||||
|
|
||||||
// broadcast LOBBY_CLOSED event to all connected sessions
|
|
||||||
// (requestId=0)
|
|
||||||
for (Session s : sessionManager.getAllSessions()) {
|
|
||||||
RequestContext ctx = new RequestContext(s.getId(), 0);
|
|
||||||
SuccessResponse ev =
|
|
||||||
new SuccessResponse(
|
|
||||||
ctx,
|
|
||||||
ResponseBody.builder()
|
|
||||||
.param("EVENT", "LOBBY_CLOSED")
|
|
||||||
.param("LOBBY_ID", lid.value())
|
|
||||||
.build()) {};
|
|
||||||
|
|
||||||
responseDispatcher.dispatch(ev);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
logger.warn("Lobby expiry job failed", e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
LOBBY_CLEANUP_INITIAL_DELAY_SECONDS,
|
LOBBY_CLEANUP_INITIAL_DELAY_SECONDS,
|
||||||
LOBBY_CLEANUP_PERIOD_SECONDS,
|
LOBBY_CLEANUP_PERIOD_SECONDS,
|
||||||
TimeUnit.SECONDS);
|
TimeUnit.SECONDS);
|
||||||
|
|||||||
+100
@@ -0,0 +1,100 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby;
|
||||||
|
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
|
||||||
|
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 java.time.Duration;
|
||||||
|
import java.util.List;
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
|
||||||
|
/** Periodically removes expired empty lobbies and notifies connected sessions. */
|
||||||
|
public class LobbyCleanupJob implements Runnable {
|
||||||
|
|
||||||
|
private static final Logger LOGGER = LogManager.getLogger(LobbyCleanupJob.class);
|
||||||
|
|
||||||
|
private final LobbyManager lobbyManager;
|
||||||
|
private final SessionManager sessionManager;
|
||||||
|
private final ResponseDispatcher responseDispatcher;
|
||||||
|
private final Duration expiryThreshold;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new cleanup job for expired empty lobbies.
|
||||||
|
*
|
||||||
|
* @param lobbyManager manager used to find and remove expired lobbies
|
||||||
|
* @param sessionManager manager used to resolve connected sessions
|
||||||
|
* @param responseDispatcher dispatcher used to notify clients about closed lobbies
|
||||||
|
* @param expiryThreshold age threshold that marks an empty lobby as expired
|
||||||
|
*/
|
||||||
|
public LobbyCleanupJob(
|
||||||
|
LobbyManager lobbyManager,
|
||||||
|
SessionManager sessionManager,
|
||||||
|
ResponseDispatcher responseDispatcher,
|
||||||
|
Duration expiryThreshold) {
|
||||||
|
this.lobbyManager = lobbyManager;
|
||||||
|
this.sessionManager = sessionManager;
|
||||||
|
this.responseDispatcher = responseDispatcher;
|
||||||
|
this.expiryThreshold = expiryThreshold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs one cleanup cycle.
|
||||||
|
*
|
||||||
|
* <p>The method first fetches all expired empty lobbies. Each lobby is then processed
|
||||||
|
* independently so that a failure for one lobby does not stop the remaining cleanups.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
LOGGER.debug("Job started.");
|
||||||
|
try {
|
||||||
|
List<LobbyId> expired;
|
||||||
|
try {
|
||||||
|
expired = lobbyManager.findEmptyLobbiesOlderThan(expiryThreshold);
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOGGER.error("Lobby expiry job failed: could not fetch expired lobbies", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (LobbyId lobbyId : expired) {
|
||||||
|
try {
|
||||||
|
lobbyManager.removeLobby(lobbyId);
|
||||||
|
broadcastLobbyClosed(lobbyId);
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
LOGGER.warn("Failed to process expired lobby {}", lobbyId.value(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
LOGGER.debug("Job finished.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Broadcasts a lobby-closed event to all currently connected sessions.
|
||||||
|
*
|
||||||
|
* @param lobbyId id of the lobby that was closed
|
||||||
|
*/
|
||||||
|
private void broadcastLobbyClosed(LobbyId lobbyId) {
|
||||||
|
for (Session session : sessionManager.getAllSessions()) {
|
||||||
|
try {
|
||||||
|
RequestContext ctx = new RequestContext(session.getId(), 0);
|
||||||
|
SuccessResponse event =
|
||||||
|
new SuccessResponse(
|
||||||
|
ctx,
|
||||||
|
ResponseBody.builder()
|
||||||
|
.param("EVENT", "LOBBY_CLOSED")
|
||||||
|
.param("LOBBY_ID", lobbyId.value())
|
||||||
|
.build()) {};
|
||||||
|
responseDispatcher.dispatch(event);
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
LOGGER.warn(
|
||||||
|
"Failed to notify session {} about closed lobby {}",
|
||||||
|
session.getId().value(),
|
||||||
|
lobbyId.value(),
|
||||||
|
e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -48,7 +48,7 @@ public class TokenClassifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static void validateSeparator(List<RawToken> rawTokens, int index) {
|
private static void validateSeparator(List<RawToken> rawTokens, int index) {
|
||||||
boolean missingKey = index == 0 || rawTokens.get(index - 1).type() != RawTokenType.WORD;
|
boolean missingKey = index <= 1 || rawTokens.get(index - 1).type() != RawTokenType.WORD;
|
||||||
|
|
||||||
boolean missingValue =
|
boolean missingValue =
|
||||||
index + 1 >= rawTokens.size()
|
index + 1 >= rawTokens.size()
|
||||||
|
|||||||
@@ -76,12 +76,14 @@
|
|||||||
<HBox fx:id="communityCardsBox"
|
<HBox fx:id="communityCardsBox"
|
||||||
styleClass="community-cards-box"
|
styleClass="community-cards-box"
|
||||||
alignment="CENTER"
|
alignment="CENTER"
|
||||||
|
GridPane.columnIndex="2"
|
||||||
spacing="10"/>
|
spacing="10"/>
|
||||||
|
|
||||||
<!-- Pot Box -->
|
<!-- Pot Box -->
|
||||||
<HBox fx:id="potBox"
|
<HBox fx:id="potBox"
|
||||||
alignment="CENTER"
|
alignment="CENTER"
|
||||||
spacing="4"
|
spacing="4"
|
||||||
|
GridPane.columnIndex="2"
|
||||||
maxWidth="Infinity"
|
maxWidth="Infinity"
|
||||||
maxHeight="Infinity"/>
|
maxHeight="Infinity"/>
|
||||||
|
|
||||||
@@ -99,6 +101,7 @@
|
|||||||
styleClass="dealer-box"
|
styleClass="dealer-box"
|
||||||
fitWidth="100"
|
fitWidth="100"
|
||||||
fitHeight="100"
|
fitHeight="100"
|
||||||
|
GridPane.columnIndex="2"
|
||||||
preserveRatio="true"
|
preserveRatio="true"
|
||||||
visible="false"/>
|
visible="false"/>
|
||||||
|
|
||||||
@@ -117,56 +120,48 @@
|
|||||||
<!-- RIGHT SIDE (Chat Box) -->
|
<!-- RIGHT SIDE (Chat Box) -->
|
||||||
<AnchorPane fx:id="chatContainer" GridPane.columnIndex="2" />
|
<AnchorPane fx:id="chatContainer" GridPane.columnIndex="2" />
|
||||||
|
|
||||||
|
<HBox AnchorPane.topAnchor="45"
|
||||||
|
AnchorPane.leftAnchor="0"
|
||||||
|
AnchorPane.rightAnchor="0"
|
||||||
|
alignment="CENTER"
|
||||||
|
spacing="300">
|
||||||
|
|
||||||
|
<VBox HBox.hgrow="ALWAYS"
|
||||||
|
alignment="TOP_LEFT"
|
||||||
|
translateX="50"
|
||||||
|
translateY="50">
|
||||||
|
|
||||||
|
<fx:include fx:id="player1"
|
||||||
|
source="gameuicomponents/Playerstatus.fxml"/>
|
||||||
|
|
||||||
|
</VBox>
|
||||||
|
|
||||||
|
<VBox HBox.hgrow="ALWAYS"
|
||||||
|
alignment="TOP_CENTER"
|
||||||
|
translateY="20">
|
||||||
|
|
||||||
|
<fx:include fx:id="player2"
|
||||||
|
source="gameuicomponents/Playerstatus.fxml"/>
|
||||||
|
|
||||||
|
</VBox>
|
||||||
|
|
||||||
|
<VBox HBox.hgrow="ALWAYS"
|
||||||
|
alignment="TOP_RIGHT"
|
||||||
|
translateX="-50"
|
||||||
|
translateY="50">
|
||||||
|
|
||||||
|
<fx:include fx:id="player3"
|
||||||
|
source="gameuicomponents/Playerstatus.fxml"/>
|
||||||
|
|
||||||
|
</VBox>
|
||||||
|
|
||||||
|
</HBox>
|
||||||
</children>
|
</children>
|
||||||
</GridPane>
|
</GridPane>
|
||||||
|
|
||||||
<!-- movable taskbar -->
|
<!-- movable taskbar -->
|
||||||
<AnchorPane>
|
<AnchorPane>
|
||||||
<fx:include fx:id="taskbarInclude" source="gameuicomponents/Taskbar.fxml"/>
|
<fx:include fx:id="taskbarInclude" source="gameuicomponents/Taskbar.fxml" GridPane.columnIndex="1"/>
|
||||||
</AnchorPane>
|
</AnchorPane>
|
||||||
|
|
||||||
<!-- Opponent Status 1: One of three panels displaying the opponent's icon, name, and account balance -->
|
|
||||||
<GridPane AnchorPane.leftAnchor="0.0"
|
|
||||||
AnchorPane.rightAnchor="0.0"
|
|
||||||
AnchorPane.topAnchor="50">
|
|
||||||
|
|
||||||
<columnConstraints>
|
|
||||||
<ColumnConstraints percentWidth="5"/>
|
|
||||||
<ColumnConstraints percentWidth="16"/>
|
|
||||||
</columnConstraints>
|
|
||||||
|
|
||||||
<children>
|
|
||||||
<fx:include fx:id="player1" source="gameuicomponents/Playerstatus.fxml" GridPane.columnIndex="1"/>
|
|
||||||
</children>
|
|
||||||
</GridPane>
|
|
||||||
|
|
||||||
<!-- Opponent Status 2: One of three panels displaying the opponent's icon, name, and account balance -->
|
|
||||||
<GridPane AnchorPane.leftAnchor="0.0"
|
|
||||||
AnchorPane.rightAnchor="0.0"
|
|
||||||
AnchorPane.topAnchor="20">
|
|
||||||
|
|
||||||
<columnConstraints>
|
|
||||||
<ColumnConstraints percentWidth="30"/>
|
|
||||||
<ColumnConstraints percentWidth="16"/>
|
|
||||||
</columnConstraints>
|
|
||||||
|
|
||||||
<children>
|
|
||||||
<fx:include fx:id="player2" source="gameuicomponents/Playerstatus.fxml" GridPane.columnIndex="1"/>
|
|
||||||
</children>
|
|
||||||
</GridPane>
|
|
||||||
|
|
||||||
<!-- Opponent Status 3: One of three panels that displays the opponent's icon, name, and account balance -->
|
|
||||||
<GridPane AnchorPane.leftAnchor="0.0"
|
|
||||||
AnchorPane.rightAnchor="0.0"
|
|
||||||
AnchorPane.topAnchor="50">
|
|
||||||
|
|
||||||
<columnConstraints>
|
|
||||||
<ColumnConstraints percentWidth="55"/>
|
|
||||||
<ColumnConstraints percentWidth="16"/>
|
|
||||||
</columnConstraints>
|
|
||||||
|
|
||||||
<children>
|
|
||||||
<fx:include fx:id="player3" source="gameuicomponents/Playerstatus.fxml" GridPane.columnIndex="1"/>
|
|
||||||
</children>
|
|
||||||
</GridPane>
|
|
||||||
</AnchorPane>
|
</AnchorPane>
|
||||||
|
|||||||
+94
@@ -0,0 +1,94 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.parser.tokenizer;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
public class TokenClassifierTest {
|
||||||
|
@Test
|
||||||
|
void testClassifySimpleCommand() {
|
||||||
|
List<RawToken> raw = Tokenizer.tokenize("PING");
|
||||||
|
List<Token> tokens = TokenClassifier.classify(raw);
|
||||||
|
|
||||||
|
assertEquals(2, tokens.size());
|
||||||
|
assertEquals(TokenType.COMMAND, tokens.get(0).type());
|
||||||
|
assertEquals("PING", tokens.get(0).value());
|
||||||
|
assertEquals(TokenType.EOF, tokens.get(1).type());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCommandWithParameter() {
|
||||||
|
List<RawToken> raw = Tokenizer.tokenize("ANSWER VALUE=42");
|
||||||
|
List<Token> tokens = TokenClassifier.classify(raw);
|
||||||
|
|
||||||
|
assertEquals(5, tokens.size());
|
||||||
|
assertEquals(TokenType.COMMAND, tokens.get(0).type());
|
||||||
|
assertEquals(TokenType.KEY, tokens.get(1).type());
|
||||||
|
assertEquals(TokenType.SEPARATOR, tokens.get(2).type());
|
||||||
|
assertEquals(TokenType.VALUE, tokens.get(3).type());
|
||||||
|
assertEquals("42", tokens.get(3).value());
|
||||||
|
assertEquals(TokenType.EOF, tokens.get(4).type());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testStringValue() {
|
||||||
|
List<RawToken> raw = Tokenizer.tokenize("GREET MSG='Hello World'");
|
||||||
|
List<Token> tokens = TokenClassifier.classify(raw);
|
||||||
|
|
||||||
|
assertEquals(5, tokens.size());
|
||||||
|
assertEquals(TokenType.COMMAND, tokens.get(0).type());
|
||||||
|
assertEquals(TokenType.KEY, tokens.get(1).type());
|
||||||
|
assertEquals(TokenType.SEPARATOR, tokens.get(2).type());
|
||||||
|
assertEquals(TokenType.VALUE, tokens.get(3).type());
|
||||||
|
assertEquals("Hello World", tokens.get(3).value());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testUnexpectedStringLiteralThrows() {
|
||||||
|
List<RawToken> raw = Tokenizer.tokenize("CMD 'oops'");
|
||||||
|
|
||||||
|
TokenizerException ex =
|
||||||
|
assertThrows(TokenizerException.class, () -> TokenClassifier.classify(raw));
|
||||||
|
assertTrue(ex.getMessage().contains("Unexpected string literal"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testMissingValueAfterSeparatorThrows() {
|
||||||
|
List<RawToken> raw = Tokenizer.tokenize("CMD KEY=");
|
||||||
|
|
||||||
|
TokenizerException ex =
|
||||||
|
assertThrows(TokenizerException.class, () -> TokenClassifier.classify(raw));
|
||||||
|
assertEquals("Expected VALUE after '='", ex.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testMissingKeyBeforeSeparatorThrows() {
|
||||||
|
List<RawToken> raw = Tokenizer.tokenize("CMD =VALUE");
|
||||||
|
|
||||||
|
TokenizerException ex =
|
||||||
|
assertThrows(TokenizerException.class, () -> TokenClassifier.classify(raw));
|
||||||
|
assertEquals("Expected KEY before '='", ex.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testNextWordIsKeyThrows() {
|
||||||
|
List<RawToken> raw = Tokenizer.tokenize("CMD KEY1=KEY2=42");
|
||||||
|
|
||||||
|
TokenizerException ex =
|
||||||
|
assertThrows(TokenizerException.class, () -> TokenClassifier.classify(raw));
|
||||||
|
assertEquals("Expected VALUE after '='", ex.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testEmptyRawTokensThrows() {
|
||||||
|
List<RawToken> raw = new ArrayList<>();
|
||||||
|
|
||||||
|
TokenizerException ex =
|
||||||
|
assertThrows(TokenizerException.class, () -> TokenClassifier.classify(raw));
|
||||||
|
assertEquals("Expected COMMAND as first token", ex.getMessage());
|
||||||
|
assertEquals(1, ex.getLine());
|
||||||
|
assertEquals(1, ex.getColumn());
|
||||||
|
}
|
||||||
|
}
|
||||||
+211
@@ -0,0 +1,211 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.parser.tokenizer;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
class TokenizerTest {
|
||||||
|
@Test
|
||||||
|
void testEofAlwaysPresent() {
|
||||||
|
List<RawToken> tokens = Tokenizer.tokenize("");
|
||||||
|
|
||||||
|
assertEquals(1, tokens.size());
|
||||||
|
assertEquals(RawTokenType.EOF, tokens.get(0).type());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSimpleCommand() {
|
||||||
|
List<RawToken> tokens = Tokenizer.tokenize("PING");
|
||||||
|
assertEquals(2, tokens.size());
|
||||||
|
assertEquals(RawTokenType.WORD, tokens.get(0).type());
|
||||||
|
assertEquals("PING", tokens.get(0).value());
|
||||||
|
assertEquals(RawTokenType.EOF, tokens.get(1).type());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSimpleCommandWithUnderscore() {
|
||||||
|
List<RawToken> tokens = Tokenizer.tokenize("JOIN_GAME");
|
||||||
|
assertEquals(2, tokens.size());
|
||||||
|
assertEquals(RawTokenType.WORD, tokens.get(0).type());
|
||||||
|
assertEquals("JOIN_GAME", tokens.get(0).value());
|
||||||
|
assertEquals(RawTokenType.EOF, tokens.get(1).type());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCommandWithOneParameter() {
|
||||||
|
List<RawToken> tokens = Tokenizer.tokenize("GET VALUE=42");
|
||||||
|
assertEquals(5, tokens.size());
|
||||||
|
|
||||||
|
assertEquals(RawTokenType.WORD, tokens.get(0).type());
|
||||||
|
assertEquals("GET", tokens.get(0).value());
|
||||||
|
|
||||||
|
// Parameter
|
||||||
|
assertEquals(RawTokenType.WORD, tokens.get(1).type());
|
||||||
|
assertEquals("VALUE", tokens.get(1).value());
|
||||||
|
|
||||||
|
assertEquals(RawTokenType.SEPARATOR, tokens.get(2).type());
|
||||||
|
assertEquals("=", tokens.get(2).value());
|
||||||
|
|
||||||
|
assertEquals(RawTokenType.WORD, tokens.get(3).type());
|
||||||
|
assertEquals("42", tokens.get(3).value());
|
||||||
|
|
||||||
|
assertEquals(RawTokenType.EOF, tokens.get(4).type());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCommandWithOneParameterWhitespacesBetweenSeperator() {
|
||||||
|
List<RawToken> tokens = Tokenizer.tokenize("GET VALUE = 42");
|
||||||
|
assertEquals(5, tokens.size());
|
||||||
|
|
||||||
|
assertEquals(RawTokenType.WORD, tokens.get(0).type());
|
||||||
|
assertEquals("GET", tokens.get(0).value());
|
||||||
|
|
||||||
|
// Parameter
|
||||||
|
assertEquals(RawTokenType.WORD, tokens.get(1).type());
|
||||||
|
assertEquals("VALUE", tokens.get(1).value());
|
||||||
|
|
||||||
|
assertEquals(RawTokenType.SEPARATOR, tokens.get(2).type());
|
||||||
|
assertEquals("=", tokens.get(2).value());
|
||||||
|
|
||||||
|
assertEquals(RawTokenType.WORD, tokens.get(3).type());
|
||||||
|
assertEquals("42", tokens.get(3).value());
|
||||||
|
|
||||||
|
assertEquals(RawTokenType.EOF, tokens.get(4).type());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCommandWithOneParameterAndNewline() {
|
||||||
|
List<RawToken> tokens = Tokenizer.tokenize("GET\nVALUE=42");
|
||||||
|
assertEquals(6, tokens.size());
|
||||||
|
|
||||||
|
assertEquals(RawTokenType.WORD, tokens.get(0).type());
|
||||||
|
assertEquals("GET", tokens.get(0).value());
|
||||||
|
|
||||||
|
// Parameter
|
||||||
|
assertEquals(RawTokenType.WORD, tokens.get(2).type());
|
||||||
|
assertEquals("VALUE", tokens.get(2).value());
|
||||||
|
|
||||||
|
assertEquals(RawTokenType.SEPARATOR, tokens.get(3).type());
|
||||||
|
assertEquals("=", tokens.get(3).value());
|
||||||
|
|
||||||
|
assertEquals(RawTokenType.WORD, tokens.get(4).type());
|
||||||
|
assertEquals("42", tokens.get(4).value());
|
||||||
|
|
||||||
|
assertEquals(RawTokenType.EOF, tokens.get(5).type());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCommandWithStringParameter() {
|
||||||
|
List<RawToken> tokens = Tokenizer.tokenize("GREET MSG='Hello World'");
|
||||||
|
assertEquals(5, tokens.size());
|
||||||
|
|
||||||
|
assertEquals(RawTokenType.WORD, tokens.get(0).type());
|
||||||
|
assertEquals("GREET", tokens.get(0).value());
|
||||||
|
|
||||||
|
// First parameter
|
||||||
|
assertEquals(RawTokenType.WORD, tokens.get(1).type());
|
||||||
|
assertEquals("MSG", tokens.get(1).value());
|
||||||
|
|
||||||
|
assertEquals(RawTokenType.SEPARATOR, tokens.get(2).type());
|
||||||
|
assertEquals("=", tokens.get(2).value());
|
||||||
|
|
||||||
|
assertEquals(RawTokenType.STRING, tokens.get(3).type());
|
||||||
|
assertEquals("Hello World", tokens.get(3).value());
|
||||||
|
|
||||||
|
assertEquals(RawTokenType.EOF, tokens.get(4).type());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCommandWithMultilineStringParameter() {
|
||||||
|
List<RawToken> tokens = Tokenizer.tokenize("GREET MSG='Hello\nWorld'");
|
||||||
|
assertEquals(5, tokens.size());
|
||||||
|
|
||||||
|
assertEquals(RawTokenType.WORD, tokens.get(0).type());
|
||||||
|
assertEquals("GREET", tokens.get(0).value());
|
||||||
|
|
||||||
|
// First parameter
|
||||||
|
assertEquals(RawTokenType.WORD, tokens.get(1).type());
|
||||||
|
assertEquals("MSG", tokens.get(1).value());
|
||||||
|
|
||||||
|
assertEquals(RawTokenType.SEPARATOR, tokens.get(2).type());
|
||||||
|
assertEquals("=", tokens.get(2).value());
|
||||||
|
|
||||||
|
assertEquals(RawTokenType.STRING, tokens.get(3).type());
|
||||||
|
assertEquals("Hello\nWorld", tokens.get(3).value());
|
||||||
|
|
||||||
|
assertEquals(RawTokenType.EOF, tokens.get(4).type());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testWhitespace() {
|
||||||
|
List<RawToken> tokens = Tokenizer.tokenize(" \t PING");
|
||||||
|
assertEquals(2, tokens.size());
|
||||||
|
assertEquals(RawTokenType.WORD, tokens.get(0).type());
|
||||||
|
assertEquals("PING", tokens.get(0).value());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testStringWithEscapedQuote() {
|
||||||
|
List<RawToken> tokens =
|
||||||
|
Tokenizer.tokenize("WONDERFUL_GREETING MSG='it\\'s a wonderful day'");
|
||||||
|
assertEquals(5, tokens.size());
|
||||||
|
assertEquals(RawTokenType.STRING, tokens.get(3).type());
|
||||||
|
assertEquals("it's a wonderful day", tokens.get(3).value());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testStringWithWronglyEscapedQuote() {
|
||||||
|
TokenizerException ex =
|
||||||
|
assertThrows(
|
||||||
|
TokenizerException.class,
|
||||||
|
() -> Tokenizer.tokenize("WONDERFUL_GREETING MSG='it\'s a wonderful day'"));
|
||||||
|
assertEquals("Unterminated string literal", ex.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testStringWithEscapedBackslash() {
|
||||||
|
List<RawToken> tokens = Tokenizer.tokenize("EXECUTE_COMMAND CMD='\\whoami'");
|
||||||
|
assertEquals(5, tokens.size());
|
||||||
|
assertEquals(RawTokenType.STRING, tokens.get(3).type());
|
||||||
|
assertEquals("\\whoami", tokens.get(3).value());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testColumnTracking() {
|
||||||
|
List<RawToken> tokens = Tokenizer.tokenize("GET VAL=1");
|
||||||
|
assertEquals(5, tokens.size());
|
||||||
|
assertEquals(1, tokens.get(0).column());
|
||||||
|
assertEquals(5, tokens.get(1).column());
|
||||||
|
assertEquals(8, tokens.get(2).column());
|
||||||
|
assertEquals(9, tokens.get(3).column());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testUnterminatedStringThrows() {
|
||||||
|
TokenizerException ex =
|
||||||
|
assertThrows(
|
||||||
|
TokenizerException.class, () -> Tokenizer.tokenize("GREETING = 'unclosed"));
|
||||||
|
assertEquals(1, ex.getLine());
|
||||||
|
assertEquals(12, ex.getColumn());
|
||||||
|
assertTrue(ex.getMessage().contains("Unterminated string literal"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testUnexpectedCharacterThrows() {
|
||||||
|
TokenizerException ex =
|
||||||
|
assertThrows(TokenizerException.class, () -> Tokenizer.tokenize("CMD @ KEY=VALUE"));
|
||||||
|
assertTrue(ex.getMessage().contains("Unexpected character '@'"));
|
||||||
|
assertEquals(1, ex.getLine());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testEmptyStringValue() {
|
||||||
|
List<RawToken> tokens = Tokenizer.tokenize("cmd = ''");
|
||||||
|
assertEquals(4, tokens.size());
|
||||||
|
assertEquals(RawTokenType.STRING, tokens.get(2).type());
|
||||||
|
assertEquals("", tokens.get(2).value());
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user