Merge branch 'feat/client-chat' into chore/ui-checkstyle-fixes

# Conflicts:
#	src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/chat/ChatController.java
#	src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/chat/ChatModel.java
#	src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/chat/Message.java
#	src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/ChatClient.java
#	src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/ClientService.java
#	src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/CoreClient.java
#	src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/GameClient.java
#	src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/LobbyClient.java
#	src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/chatui/ChatViewController.java
#	src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/lobbyui/LobbyButtonGridManager.java
#	src/main/resources/ui-structure/components/Chatbox.fxml
This commit is contained in:
Julian Kropff
2026-04-11 12:56:01 +02:00
42 changed files with 1326 additions and 517 deletions
+1 -1
View File
@@ -32,7 +32,7 @@ dependencies {
// Source: https://mvnrepository.com/artifact/org.apache.logging.log4j // Source: https://mvnrepository.com/artifact/org.apache.logging.log4j
implementation("org.apache.logging.log4j:log4j-api:2.25.3") implementation("org.apache.logging.log4j:log4j-api:2.25.3")
runtimeOnly("org.apache.logging.log4j:log4j-core:2.25.3") runtimeOnly("org.apache.logging.log4j:log4j-core:2.25.3")
implementation("org.jspecify:jspecify:1.0.0")
// Source: https://mvnrepository.com/artifact/org.fusesource.jansi/jansi // Source: https://mvnrepository.com/artifact/org.fusesource.jansi/jansi
runtimeOnly("org.fusesource.jansi:jansi:2.4.2") runtimeOnly("org.fusesource.jansi:jansi:2.4.2")
@@ -1,63 +0,0 @@
This Document states the Network-Protocol as it is currently implemented
## GET_MESSAGE_COUNT
This Command gets the number of messages, currently stored in the queue for the specific client.
(Is used together with GET_NEXT_MESSAGE, to get all messages that are currently in the queue)
Example:
```
GET_MESSAGE_COUNT
1
+OK
```
## GET_NEXT_MESSAGE
This Command gets the next Message that is being stored in the queue for the client.
(Command is being sent the amount of times, the GET_MESSAGE_COUNT Command returns)
Example:
```
GET_NEXT_MESSAGE
TYPE=LOBBY GAME=1 USER=player1 TARGET=null TIME=9:30 TEXT="Guten Tag"
+OK
```
## SEND_MESSAGE
This Command gets sent if the user of that client writes a message to one of the three possible chats
(The Arguments / Parameters of the Command describe all the parameters of the Object "Message" being used internally by both the Client and the Server)
Example:
```
SEND_MESSAGE TYPE=LOBBY GAME=1 USER=player1 TARGET=null TIME=10:30 TEXT="Hallo Welt"
+OK
```
## LOG_IN
This Command is used to create a user on the server and associate that user with a username
Server returns the username, slightly changed if it is already used by someone else, and an ID to identify the client.
Example:
```
LOG_IN USERNAME="Peter"
USERNAME="Peter" ID=<random UUID>
+OK
```
## LOG_OUT
This Command is used to quit the connection between client and server.
Example:
```
LOG_OUT
+OK
```
@@ -342,3 +342,102 @@ LIST_USERS
END END
END END
``` ```
## SEND_MESSAGE command
The `SEND_MESSAGE` command is used to transfer the chat message sent by a user to the server.
### Required pre-execution checks
None.
### Request Parameters
| Field | Type | Description |
|:---------|:----------------|:-------------------------------------------------------------------|
| `TYPE` | `Enum<ChatType` | GLOBAL, LOBBY or WHISPER, specifying the Chat |
| `GAME` | `int` | ID for identifying the Lobby Chat |
| `USER` | `String` | Username of the player that sent the message |
| `TARGET` | `String` | Username of to which the message is sent to (only in Whisper Chat) |
| `TIME` | `String` | Timestamp, when the message was sent |
| `TEXT` | `String` | Content of the message |
### Success Response
No response fields.
### Error Response
None.
### Example Request
```
SEND_MESSAGE TYPE=GLOBAL GAME=1 USER=player1 TARGET=null TIME='10:30' TEXT='Hello World'
```
### Example Response
```
+OK
END
```
## GET_MESSAGE_COUNT command
The `GET_MESSAGE_COUNT` is used to get the current number of messages that are stored in the queue for a client.
### Required pre-execution checks
None.
### Request Parameters
No parameters.
### Success Response
| Field | Type | Description |
|:--------|:------|:-------------------------------|
| `COUNT` | `int` | The current number of messages |
### Error Response
| Code | Description |
| :------------------- |:---------------------------------------------------------------------------------|
| `NO_USER_ASSOCIATED` | The session has no user associated, there is no queue of messages for the client |
### Example Request
```
GET_MESSAGE_COUNT
```
### Example Response
```
+OK
COUNT=10
END
```
## GET_NEXT_MESSAGE command
The `GET_NEXT_MESSAGE` command is used to get the next message stored in a queue for the client.
### Required pre-execution checks
None.
### Request Parameters
No parameters.
### Success Response
| Field | Type | Description |
|:---------|:----------------|:-------------------------------------------------------------------|
| `TYPE` | `Enum<ChatType` | GLOBAL, LOBBY or WHISPER, specifying the Chat |
| `GAME` | `int` | ID for identifying the Lobby Chat |
| `USER` | `String` | Username of the player that sent the message |
| `TARGET` | `String` | Username of to which the message is sent to (only in Whisper Chat) |
| `TIME` | `String` | Timestamp, when the message was sent |
| `TEXT` | `String` | Content of the message |
### Error Response
| Code | Description |
| :------------------- |:---------------------------------------------------------------------------------|
| `NO_USER_ASSOCIATED` | The session has no user associated, there is no queue of messages for the client |
### Example Request
```
GET_NEXT_MESSAGE
```
### Example Response
```
+OK
TYPE=GLOBAL GAME=-1 USER=player1 TARGET=null TIME=9:30 TEXT="Guten Tag"
END
```
@@ -44,4 +44,8 @@ public class ClientApp {
// Forward the original address argument to the launcher as well. // Forward the original address argument to the launcher as well.
Launcher.main(new String[] { arg }); Launcher.main(new String[] { arg });
} }
public static void main(String[] args) {
start(args[0]);
}
} }
@@ -2,64 +2,128 @@ package ch.unibas.dmi.dbis.cs108.casono.client.chat;
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 java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;
import org.jspecify.annotations.Nullable;
/** /**
* responsible for the transferring of messages from the server to the ChatModel * responsible for the transferring of messages from the server to the ChatModel or from the
* or from the ChatViewController to the server * ChatViewController to the server
*/ */
public class ChatController { public class ChatController {
private final String username; private final String username;
private final ClientService clientService;
private final ChatClient chatClient; private final ChatClient chatClient;
private ChatModel chatModel; public ChatBoxController getChatBoxController() {
return chatBoxController;
}
private int game_id; private final ChatBoxController chatBoxController;
private int lobbyId = -1;
private final Timer timer;
public record ChatKey(ChatType type, @Nullable String targetUser) {
public ChatKey(ChatType type) {
this(type, null);
}
}
public Map<ChatKey, ChatModel> getChatModelMap() {
return chatModelMap;
}
private Map<ChatKey, ChatModel> chatModelMap;
private static final long REFRESH_TIME = 1000;
/**
* Constructor, adds TimerTask to be sent to the server regularly
*
* @param username
* @param clientService
*/
public ChatController(String username, ClientService clientService) { public ChatController(String username, ClientService clientService) {
this.username = username; this.username = username;
this.clientService = clientService; chatClient = new ChatClient(clientService);
this.chatClient = new ChatClient(this.clientService); chatModelMap = new LinkedHashMap<>();
this.chatBoxController = new ChatBoxController(username, this);
this.timer = new Timer();
timer.schedule(
new TimerTask() {
@Override
public void run() {
receiveMessage();
} }
},
public void createChat(int game_id) { 0,
chatModel = new ChatModel(ChatModel.ChatType.GLOBAL, username); REFRESH_TIME);
this.game_id = game_id;
}
public ChatModel getChatModel() {
return chatModel;
} }
/** /**
* method to send a message, the ChatViewController received to the server * Method to be activated, if a lobby has be chosen. It will update the UI and add a new
* ChatModel to hold the Data for the Lobby Chat
*
* @param lobbyId
*/ */
public void sendMessage(String msg, String username) { public void setLobbyChat(int lobbyId) {
Message message = new Message(Message.MessageType.GLOBAL, 0, username, null, msg); this.lobbyId = lobbyId;
onSendToNetwork(message); ChatModel lobbyChatModel = new ChatModel(ChatType.LOBBY, username, lobbyId, null);
chatModelMap.put(new ChatKey(ChatType.LOBBY), lobbyChatModel);
this.chatBoxController.addChatTab("Lobby", lobbyChatModel);
} }
/** /** method to get all messages from the server */
* method to get all messages from the server public void receiveMessage() {
*/
public Boolean receiveMessage() {
List<Message> newMessages = chatClient.getMessages(); List<Message> newMessages = chatClient.getMessages();
if (!newMessages.isEmpty()) { if (!newMessages.isEmpty()) {
for (Message msg : newMessages) { for (Message msg : newMessages) {
chatModel.addMessage(msg); switch (msg.getMessageType()) {
case ChatType.GLOBAL:
chatModelMap.get(new ChatKey(ChatType.GLOBAL)).addMessage(msg);
break;
case ChatType.LOBBY:
if (msg.lobbyId == lobbyId) {
chatModelMap
.computeIfAbsent(
new ChatKey(ChatType.LOBBY),
(_key) ->
new ChatModel(
ChatType.LOBBY,
username,
msg.lobbyId,
null))
.addMessage(msg);
}
break;
case ChatType.WHISPER:
if (msg.target.equals(username)) {
if (chatModelMap.containsKey(
new ChatKey(ChatType.WHISPER, msg.sender))) {
chatModelMap
.get(new ChatKey(ChatType.WHISPER, msg.sender))
.addMessage(msg);
} else {
chatBoxController.addWhisperChat(msg.sender);
}
}
break;
}
}
} }
return true;
} else { return false; }
} }
/** /**
* method to send a message to the server * method to send a message to the server
*
* @param message * @param message
*/ */
public void onSendToNetwork(Message message) { public void onSendToNetwork(Message message) {
chatClient.sendMessage(message); chatClient.sendMessage(message);
} }
} }
@@ -1,67 +1,85 @@
package ch.unibas.dmi.dbis.cs108.casono.client.chat; package ch.unibas.dmi.dbis.cs108.casono.client.chat;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.function.Consumer;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
/** /**
* ChatModel, stores the data for a specific chat * ChatModel, stores the data for a specific chat
* *
* Holds the current state of a chat * <p>Holds the current state of a chat
*/ */
public class ChatModel { public class ChatModel {
private ArrayList<Consumer<Message>> listeners = new ArrayList<>();
public ArrayList<Message> messages; public ArrayList<Message> messages;
public ChatType chattype; private final ChatType chattype;
public String username; /** The person currently using this client */
public final String username;
public int count; /** The person to send the message to If the chat is a whisper chat */
private final String target;
public enum ChatType { private final IntegerProperty count;
GLOBAL,
LOBBY, public int lobbyId;
WHISPER
}
/** /**
* Creates a new ChatModel, given a username of the client * Constructs a new ChatModel for a specific chat type.
* @param chattype *
* @param username * @param chattype The type of chat (e.g., GLOBAL, LOBBY, or WHISPER).
* @param username The username of the current user.
* @param lobbyId The ID of the lobby, or -1 if not applicable.
* @param target The username of the whisper recipient, or null for other chat types.
*/ */
public ChatModel(ChatType chattype, String username, int lobbyId, String target) {
public ChatModel(ChatType chattype, String username) {
this.messages = new ArrayList<Message>(); this.messages = new ArrayList<Message>();
this.chattype = chattype; this.chattype = chattype;
this.username = username; this.username = username;
this.count = 0; this.count = new SimpleIntegerProperty(0);
this.lobbyId = lobbyId;
this.target = target;
} }
/** /**
* method, used by the ChatViewController, to access all new messages, that are stored in the ChatModel * Returns the type of chat this model represents.
*
* @return The {@link ChatType}.
*/ */
public synchronized String viewNextMessage() { public ChatType getChattype() {
count--; return chattype;
Message msg = messages.getLast();
return String.format("[%s] %s: %s", msg.timestamp, msg.user, msg.getMessage());
} }
/** /**
* Adds a new message * Adds a new message to the history and notifies all registered listeners. This method is
* method used by the ChatController * synchronized to ensure thread safety when updating the message list.
* @param msg *
* @param msg The {@link Message} to be added.
*/ */
public synchronized void addMessage(Message msg) { public synchronized void addMessage(Message msg) {
messages.add(msg); messages.add(msg);
count++; listeners.stream().forEach((l) -> l.accept(messages.getLast()));
} }
/** /**
* method to send all current messages to the ChatViewController, if needed * Registers a listener to be notified whenever a new message is added to this model.
*
* @param listener A {@link Consumer} that processes the new {@link Message}.
*/ */
public void addCompleteChat() { public void addListener(Consumer<Message> listener) {
for (int i = 0; i < this.messages.size(); i++) { this.listeners.add(listener);
Message msg = this.messages.get(i);
} }
/**
* Returns the target user for this chat, primarily used for whispers.
*
* @return The target username or null.
*/
public String getTarget() {
return target;
} }
} }
@@ -0,0 +1,8 @@
package ch.unibas.dmi.dbis.cs108.casono.client.chat;
/** Describing the Type of the Chat */
public enum ChatType {
GLOBAL,
LOBBY,
WHISPER
}
@@ -1,57 +1,63 @@
package ch.unibas.dmi.dbis.cs108.casono.client.chat; package ch.unibas.dmi.dbis.cs108.casono.client.chat;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.RequestParameter;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder.ResponseBody;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder.ResponseBodyBuilder;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.regex.Matcher; import java.util.List;
import java.util.regex.Pattern; import java.util.Optional;
import org.jspecify.annotations.NonNull;
/**
* Message Object for internal handling of Chat-Messages
* TODO: Should be used on both sides of the network
*/
/** Message Object for internal handling of Chat-Messages */
public class Message { public class Message {
private final MessageType type; private final ChatType type;
private final String message; private final String message;
public String user; public String sender;
public String timestamp; public String timestamp;
public int game_id = 0; public int lobbyId = 0;
public String target = null; public String target = null;
public enum MessageType {
GLOBAL, LOBBY, WHISPER
};
/** /**
* Constructor for creating Messages with all information given * Constructs a Message with a provided timestamp. Typically used when reconstructing messages
* @param type - Either global, local or whisper * received from the server.
* @param game_id - lobby id, or null, if the type is global *
* @param user - username * @param type The chat category (e.g., GLOBAL, LOBBY, or WHISPER).
* @param target - username of the target user, for whisper chat * @param lobbyId The ID of the lobby, or -1 if not applicable.
* @param message * @param sender The username of the message creator.
* @param target The username of the recipient (required for whispers, otherwise null).
* @param timestamp The formatted time string (e.g., "HH:mm").
* @param message The actual text content of the message.
*/ */
public Message(
public Message(MessageType type, int game_id, String user, String target, String timestamp, String message) { ChatType type,
int lobbyId,
String sender,
String target,
String timestamp,
String message) {
this.type = type; this.type = type;
this.game_id = game_id; this.lobbyId = lobbyId;
this.user = user; this.sender = sender;
this.target = target; this.target = target;
this.timestamp = timestamp; this.timestamp = timestamp;
this.message = message; this.message = message;
} }
/** /**
* Constructor for creating the Messages of the current user, using this client -> time of writing is being recorded * Constructs a new Message for the current user. Automatically generates a timestamp based on
* @param type - Either global, local or whisper * the local system time ("HH:mm").
* @param game_id - lobby id, or null, if the type is global *
* @param user - username * @param type The chat category (e.g., GLOBAL, LOBBY, or WHISPER).
* @param target - username of the target user, for whisper chat * @param lobbyId The ID of the lobby, or -1 if not applicable.
* @param message * @param sender The username of the current user.
* @param target The username of the recipient (for whispers).
* @param message The actual text content to be sent.
*/ */
public Message(ChatType type, int lobbyId, String sender, String target, String message) {
public Message(MessageType type, int game_id, String user, String target, String message) {
this.type = type; this.type = type;
this.game_id = game_id; this.lobbyId = lobbyId;
this.user = user; this.sender = sender;
this.target = target; this.target = target;
this.message = message; this.message = message;
LocalDateTime now = LocalDateTime.now(); LocalDateTime now = LocalDateTime.now();
@@ -59,78 +65,139 @@ public class Message {
this.timestamp = now.format(formatter); this.timestamp = now.format(formatter);
} }
/**
* Returns the text content of the message.
*
* @return The message string.
*/
public String getMessage() { public String getMessage() {
return message; return message;
} }
public MessageType getMessageType() { /**
* Returns the type of chat this message belongs to.
*
* @return The {@link ChatType}.
*/
public ChatType getMessageType() {
return type; return type;
} }
/*
* Method to test the system
* @return - String representation of the Message instance, as for example "player1: Hello World"
public String toString() {
return String.format("%s: %s", this.user, this.message);
}
*/
/** /**
* Method to create the request representation of the message object, to be sent to the server * Formats the message object into a string representation compatible with the network protocol
* @return - request as specified in the network protocol, as String * arguments.
*
* @return A formatted string containing all message attributes for server transmission.
*/ */
public String toArgsString() { public String toArgsString() {
return String.format("TYPE=%s GAME=%d USER=%s TARGET=%s TIME=%s TEXT=%s", String gameIdString = "";
this.type.toString(), this.game_id, this.user, this.target, this.timestamp, this.message); if (lobbyId >= 0) {
gameIdString = " GAME=" + lobbyId;
} else {
gameIdString = " GAME='-1'";
}
return String.format(
"TYPE=%s%s USER='%s' TARGET='%s' TIME='%s' TEXT='%s'",
this.type.toString(),
gameIdString,
this.sender,
this.target,
this.timestamp,
this.message);
} }
/** /**
* Pattern, to analyze the response String with the given parameters * Parses a list of network request parameters to reconstruct a Message object. Handles
* different chat types (GLOBAL, LOBBY, WHISPER) and their specific requirements.
*
* @param parameters A list of {@link RequestParameter} received from the network.
* @return A new {@link Message} instance populated with the parsed data.
*/ */
public static Pattern msgRex = Pattern.compile( public static Message toMessageReqPars(List<RequestParameter> parameters) {
"TYPE=(?<type>\\w+) GAME=(?<game>\\w+) USER=(?<user>\\w+) TARGET=(?<target>\\w+) TIME=(?<time>[0-9:.]+) TEXT=(?<text>.*)$"); String typeString = getParString(parameters, "TYPE");
ChatType type = ChatType.valueOf(typeString);
return switch (type) {
case GLOBAL ->
new Message(
ChatType.GLOBAL,
-1,
getParString(parameters, "USER"),
null,
getParString(parameters, "TIME"),
getParString(parameters, "TEXT"));
case LOBBY ->
new Message(
ChatType.LOBBY,
Integer.parseInt(getParString(parameters, "GAME")),
getParString(parameters, "USER"),
null,
getParString(parameters, "TIME"),
getParString(parameters, "TEXT"));
case WHISPER ->
new Message(
ChatType.WHISPER,
Integer.parseInt(getParString(parameters, "GAME", "-1")),
getParString(parameters, "USER"),
getParString(parameters, "TARGET"),
getParString(parameters, "TIME"),
getParString(parameters, "TEXT"));
};
}
/** /**
* Method to create a Message Object, from the information given by the String * Helper method to extract a specific parameter value by its key.
* @param response - String that got sent as a response from the server *
* @return - New Message Object * @param parameters The list of parameters to search.
* @param keyString The key to look for.
* @return The value associated with the key.
* @throws RuntimeException if the key is not found.
*/ */
public static Message toMessage(String response) { private static @NonNull String getParString(
Matcher m = msgRex.matcher(response); List<RequestParameter> parameters, String keyString) {
if (! m.matches()) { return getParString(parameters, keyString, null);
throw new RuntimeException("Can not parse message: '" + response+"'");
} }
String typeString=m.group("type");
switch (typeString) { /**
case "GLOBAL": * Helper method to extract a specific parameter value by its key, with a fallback default
return new Message(MessageType.GLOBAL, * value.
0, *
m.group("user"), * @param parameters The list of parameters to search.
null, * @param keyString The key to look for.
m.group("time"), * @param defaultVal The value to return if the key is missing.
m.group("text")); * @return The found value or the default value.
*/
case "LOBBY": private static @NonNull String getParString(
return new Message(MessageType.LOBBY, List<RequestParameter> parameters, String keyString, String defaultVal) {
Integer.parseInt(m.group("game")), Optional<String> parOption =
m.group("user"), parameters.stream()
null, .filter((p) -> keyString.equals(p.key()))
m.group("time"), .findFirst()
m.group("text")); .map(RequestParameter::value);
if (parOption.isEmpty()) {
case "WHISPER": if (defaultVal == null) {
return new Message(MessageType.WHISPER, throw new RuntimeException("No " + keyString + " found");
Integer.parseInt(m.group("game")), } else {
m.group("user"), return defaultVal;
m.group("target"),
m.group("time"),
m.group("text"));
default:
throw new RuntimeException("Unknown message type " + typeString);
} }
} }
return parOption.get();
}
/**
* Converts the message object into a network response body using the provided builder.
*
* @param builder The {@link ResponseBodyBuilder} used to construct the response.
* @return The built {@link ResponseBody} containing the message data.
*/
public ResponseBody toResponse(ResponseBodyBuilder builder) {
builder.param("TYPE", type.name());
builder.param("GAME", lobbyId);
builder.param("USER", this.sender);
if (target != null) {
builder.param("TARGET", target);
}
builder.param("TIME", this.timestamp);
builder.param("TEXT", this.message);
return builder.build();
}
} }
@@ -1,60 +1,71 @@
package ch.unibas.dmi.dbis.cs108.casono.client.network; package ch.unibas.dmi.dbis.cs108.casono.client.network;
import ch.unibas.dmi.dbis.cs108.casono.client.chat.Message; import ch.unibas.dmi.dbis.cs108.casono.client.chat.Message;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.RequestParameter;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/** /**
* The ChatClient class is responsible for sending messages to the server and * The ChatClient class is responsible for sending messages to the server and retrieving messages
* retrieving messages from the server. It uses the ClientService to send * from the server. It uses the ClientService to send commands and receive responses from the
* commands and receive responses from the server. * server.
*/ */
public class ChatClient { public class ChatClient {
private ClientService clientService; private final ClientService clientService;
private final Logger logger;
/** /**
* Constructs a ChatClient with the given ClientService for communication. * Constructs a ChatClient with the given ClientService for communication.
* *
* @param clientService The ClientService instance used to send commands and * @param clientService The ClientService instance used to send commands and receive responses
* receive responses from the server. * from the server.
*/ */
public ChatClient(ClientService clientService) { public ChatClient(ClientService clientService) {
this.clientService = clientService; this.clientService = clientService;
this.logger = LogManager.getLogger(ChatClient.class);
} }
/** /**
* Send a Message to the server by converting it to a string format and * Send a Message to the server by converting it to a string format and sending a "SEND_MESSAGE"
* sending a "SEND_MESSAGE" command with the message content as arguments. * command with the message content as arguments.
* *
* @param message The Message object to be sent to the server. * @param message The Message object to be sent to the server.
*/ */
public void sendMessage(Message message) { public void sendMessage(Message message) {
String request = "SEND_MESSAGE " + message.toArgsString(); String request = "SEND_MESSAGE " + message.toArgsString();
logger.info("Writing to server: " + request);
clientService.processCommand(request); clientService.processCommand(request);
} }
/** /**
* Retrieve messages from the server by first sending a "GET_MESSAGE_COUNT" * Retrieve messages from the server by first sending a "GET_MESSAGE_COUNT" command to determine
* command to determine how many messages are available and then sending * how many messages are available and then sending "GET_NEXT_MESSAGE" commands in a loop to
* "GET_NEXT_MESSAGE" commands in a loop to retrieve each message. The * retrieve each message. The retrieved messages are parsed into Message objects and returned as
* retrieved messages are parsed into Message objects and returned as a list. * a list.
* *
* @return A list of Message objects representing the messages retrieved from * @return A list of Message objects representing the messages retrieved from the server.
* the server.
*/ */
public List<Message> getMessages() { public List<Message> getMessages() {
String countStr = clientService.processCommand("GET_MESSAGE_COUNT"); logger.info("Asking server for new messages");
int count = Integer.parseInt(countStr); List<RequestParameter> countStr =
System.out.println("Got " + count + " messages"); ClientService.convertToRequestParameters(
List<Message> messages = new ArrayList<>(); clientService.processCommand("GET_MESSAGE_COUNT"));
for (int i = 0; i < count; i++) { RequestParameter countRes = countStr.getFirst();
String message = clientService.processCommand("GET_NEXT_MESSAGE"); if (!countRes.key().equals("COUNT")) {
if (message != null) { logger.error("Not the right response from server");
Message message1 = Message.toMessage(message);
messages.add(message1);
} }
int count = Integer.parseInt(countRes.value());
logger.info("Got " + count + " messages");
ArrayList<Message> messages = new ArrayList<>();
for (int i = 0; i < count; i++) {
List<RequestParameter> msgRes =
ClientService.convertToRequestParameters(
clientService.processCommand("GET_NEXT_MESSAGE"));
Message msg = Message.toMessageReqPars(msgRes);
messages.add(msg);
} }
return messages; return messages;
} }
@@ -1,24 +1,26 @@
package ch.unibas.dmi.dbis.cs108.casono.client.network; package ch.unibas.dmi.dbis.cs108.casono.client.network;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.RequestParameter;
import ch.unibas.dmi.dbis.cs108.casono.server.network.transport.RawPacket; import ch.unibas.dmi.dbis.cs108.casono.server.network.transport.RawPacket;
import ch.unibas.dmi.dbis.cs108.casono.server.network.transport.TcpTransport; import ch.unibas.dmi.dbis.cs108.casono.server.network.transport.TcpTransport;
import java.io.IOException;
import java.net.Socket; import java.net.Socket;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.Future; import java.util.concurrent.Future;
import java.io.IOException;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference; import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/** /**
* The ClientService class is responsible for managing the connection to the * The ClientService class is responsible for managing the connection to the server, sending
* server, * commands, and receiving responses. It uses a TcpTransport to communicate with the server and an
* sending commands, and receiving responses. It uses a TcpTransport to * ExecutorService to handle asynchronous requests.
* communicate
* with the server and an ExecutorService to handle asynchronous requests.
*/ */
public class ClientService { public class ClientService {
@@ -26,15 +28,15 @@ 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;
private final Logger logger;
/** /**
* Constructs a ClientService with the given server IP and port. It establishes * Constructs a ClientService with the given server IP and port. It establishes a socket
* a socket connection to the server and initializes the TcpTransport and * connection to the server and initializes the TcpTransport and ExecutorService for
* ExecutorService for communication. * communication.
* *
* @param ip The IP address of the server to connect to. * @param ip The IP address of the server to connect to.
* @param port The port number of the server to connect to. * @param port The port number of the server to connect to.
@@ -43,10 +45,12 @@ public class ClientService {
this.idGenerator = new AtomicInteger(0); this.idGenerator = new AtomicInteger(0);
this.offlineMode = false; this.logger = LogManager.getLogger(ClientService.class);
try { try {
socket = new Socket(ip, port); socket = new Socket(ip, port);
clienttcptransport = new TcpTransport(socket); clienttcptransport = new TcpTransport(socket);
logger.info("Connected to server at " + ip);
} catch (IOException i) { } catch (IOException i) {
throw new RuntimeException(i); throw new RuntimeException(i);
} }
@@ -54,70 +58,104 @@ public class ClientService {
executor = Executors.newSingleThreadExecutor(); executor = Executors.newSingleThreadExecutor();
} }
static Pattern responseRex =
Pattern.compile(
"(?<key>\\w+)=(('(?<string>([^']|\\')+)')|(?<primVal>[+-]?[\\d\\w:]+))");
/** /**
* Constructs a ClientService in offline mode. No network connection will be * Removes escape characters from a string, specifically converting escaped single quotes (\')
* attempted and calls to processCommand will throw a RuntimeException. * back to regular single quotes (').
* *
* @param offline true to create an offline (no-network) client service * @param input The escaped string to process.
* @return The unescaped string.
*/ */
public ClientService(boolean offline) { private static String unescape(String input) {
this.idGenerator = new AtomicInteger(0); return input.replaceAll("\\\\'", "'");
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). * 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.
* @return A list of parsed {@link RequestParameter} objects.
* @throws RuntimeException if a parameter does not match the expected format.
*/ */
public boolean isOffline() { public static List<RequestParameter> convertToRequestParameters(List<String> input) {
return offlineMode; return input.stream()
.map((String parString) -> responseRex.matcher(parString))
.filter(Matcher::matches)
.map(
(m) -> {
if (!(m.group("string") == null)) {
return new RequestParameter(
m.group("key"), unescape(m.group("string")));
} else if (!(m.group("primVal") == null)) {
return new RequestParameter(m.group("key"), m.group("primVal"));
} else {
throw new RuntimeException();
}
})
.toList();
} }
/** /**
* Sends a command to the server and waits for the response. The command is * Sends a command to the server and processes the multi-line response. It handles the protocol
* sent using the TcpTransport, and the response is read in a loop until a * handshake (expecting +OK), strips leading tabs from response lines, and collects them until
* valid response is received. The method handles "+OK" and "-ERROR" responses * the "END" marker is reached.
* from the server and returns the actual response content.
* *
* @param message The command message to be sent to the server. * @param message The raw command string to be sent to the transport layer.
* @return The response from the server as a string. * @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 String processCommand(String message) { protected List<String> processCommand(String message) {
if (offlineMode) { List<String> response = new ArrayList<>();
throw new RuntimeException("ClientService is offline: cannot process command"); sendRequest(
} () -> {
AtomicReference<String> response = new AtomicReference<>();
sendRequest(() -> {
try { try {
writeToTransport(message); writeToTransport(message);
String responseLine = null; String responseText = null;
do {
responseLine = clienttcptransport.read().payload(); responseText = clienttcptransport.read().payload();
System.out.println("Raw message '" + responseLine + "'"); logger.info("Raw message '" + responseText + "'");
if ("+OK".equals(responseLine)) { Boolean success = null;
return; int count = 0;
} else if (("-ERROR").equals(responseLine)) { for (String line : responseText.split("\n")) {
throw new RuntimeException(responseLine); if (success == null) {
if ("+OK".equals(line)) {
success = true;
continue;
} else if (("-ERROR").equals(responseText)) {
success = false;
}
continue;
} else if ("END".equals(line)) {
break;
}
line = line.replaceFirst("^\t", "");
response.add(line);
}
if (success != null && success) {
return;
} else {
throw new RuntimeException("Error in " + message + ": " + response);
} }
response.set(responseLine);
} while (true);
} catch (Exception e) { } catch (Exception e) {
throw getRuntimeException(e); throw getRuntimeException(e);
} }
}); });
return response.get(); return response;
} }
/** /**
* Helper method to send a request to the server using the ExecutorService. It * Helper method to send a request to the server using the ExecutorService. It submits the
* submits the request as a Runnable task and waits for its completion. If * request as a Runnable task and waits for its completion. If the task is interrupted or
* the task is interrupted or encounters an execution exception, it throws a * encounters an execution exception, it throws a RuntimeException with the appropriate cause.
* RuntimeException with the appropriate cause.
* *
* @param request The Runnable task representing the request to be sent to the * @param request The Runnable task representing the request to be sent to the server.
* server.
*/ */
private void sendRequest(Runnable request) { private void sendRequest(Runnable request) {
Future<?> future = executor.submit(request); Future<?> future = executor.submit(request);
@@ -131,11 +169,10 @@ public class ClientService {
} }
/** /**
* Helper method to extract the cause of an exception and return it as a * Helper method to extract the cause of an exception and return it as a RuntimeException. If
* RuntimeException. If the cause is null, it returns the original exception as * the cause is null, it returns the original exception as a RuntimeException. If the cause is
* a RuntimeException. If the cause is already a RuntimeException, it returns * already a RuntimeException, it returns it directly. Otherwise, it wraps the cause in a new
* it directly. Otherwise, it wraps the cause in a new RuntimeException and * RuntimeException and returns it.
* returns it.
* *
* @param e The exception from which to extract the cause. * @param e The exception from which to extract the cause.
* @return A RuntimeException representing the cause of the original exception. * @return A RuntimeException representing the cause of the original exception.
@@ -153,38 +190,33 @@ public class ClientService {
} }
/** /**
* Closes the socket connection to the server and shuts down the * Closes the socket connection to the server and shuts down the ExecutorService. It also closes
* ExecutorService. * the TcpTransport used for communication. If any IOException occurs during this process, it
* It also closes the TcpTransport used for communication. If any IOException * prints the exception to the console.
* occurs during this process, it prints the exception to the console.
*/ */
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();
} logger.info("Socket closed");
} catch (IOException j) { } catch (IOException j) {
System.out.println(j); logger.debug(j);
} }
} }
/** /**
* Helper method to write a command string to the TcpTransport. It generates a * Method to write with the tcp transport to the server
* unique ID for the command using the idGenerator and sends a RawPacket
* containing the ID and the command string to the server. If an IOException
* occurs during this process, it throws a RuntimeException with the cause.
* *
* @param s The command string to be sent to the server. * @param s - Message to be sent
* @throws IOException If an I/O error occurs while writing to the transport. * @throws IOException
*/ */
private void writeToTransport(String s) throws IOException { private void writeToTransport(String s) throws IOException {
int id = this.idGenerator.incrementAndGet(); int id = this.idGenerator.incrementAndGet();
this.clienttcptransport.write(new RawPacket(id, s)); this.clienttcptransport.write(new RawPacket(id, s));
} }
public void ping() {
processCommand("PING");
}
} }
@@ -1,11 +1,9 @@
package ch.unibas.dmi.dbis.cs108.casono.client.network; package ch.unibas.dmi.dbis.cs108.casono.client.network;
/** /**
* The CoreClient class provides basic functionalities for communicating with * The CoreClient class provides basic functionalities for communicating with the server, such as
* the * sending a ping command to check connectivity and logging in with a username. It uses the
* server, such as sending a ping command to check connectivity and logging in * ClientService to send commands and receive responses from the server.
* with a username. It uses the ClientService to send commands and receive
* responses from the server.
*/ */
public class CoreClient { public class CoreClient {
private final ClientService clientService; private final ClientService clientService;
@@ -13,16 +11,16 @@ public class CoreClient {
/** /**
* Constructs a CoreClient with the given ClientService for communication. * Constructs a CoreClient with the given ClientService for communication.
* *
* @param clientservice The ClientService instance used to send commands and * @param clientservice The ClientService instance used to send commands and receive responses
* receive responses from the server. * from the server.
*/ */
public CoreClient(ClientService clientservice) { public CoreClient(ClientService clientservice) {
this.clientService = clientservice; this.clientService = clientservice;
} }
/** /**
* Sends a "PING" command to the server to check connectivity. The server * Sends a "PING" command to the server to check connectivity. The server should respond with a
* should respond with a "PONG" message if the connection is successful. * "PONG" message if the connection is successful.
*/ */
public void ping() { public void ping() {
clientService.processCommand("PING"); clientService.processCommand("PING");
@@ -0,0 +1,112 @@
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.ChatModel;
import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatType;
import java.io.IOException;
import java.net.URL;
import java.util.List;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Node;
import javafx.scene.control.MenuButton;
import javafx.scene.control.MenuItem;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
public class ChatBoxController {
private String username;
private ChatController chatController;
@FXML private VBox chatBox;
@FXML private TabPane chatTabPane;
@FXML private MenuButton addWhisperChatButton;
private FXMLLoader fxmlLoader;
@FXML private List<MenuItem> whisperUsers;
private String ressource = "/ui-structure/components/chatui/chattab.fxml";
public ChatBoxController(String username, ChatController chatController) {
this.username = username;
this.chatController = chatController;
}
/**
* Initializes the chat interface by creating the global chat model and adding the corresponding
* "GLOBAL" tab to the interface. It also registers the global chat in the {@link
* ChatController}'s model map.
*/
@FXML
public void initialize() {
ChatModel globalChatModel = new ChatModel(ChatType.GLOBAL, username, -1, null);
chatController
.getChatModelMap()
.put(new ChatController.ChatKey(ChatType.GLOBAL), globalChatModel);
addChatTab("GLOBAL", globalChatModel);
// TODO: Button to add new Whisper Chat
}
/**
* Adds a specific user to the list of available whisper targets. Creates a new menu item for
* the user and defines the action to open a private chat tab when selected.
*
* @param targetUserName The username of the person to be added to the whisper list.
*/
public void addWhisperUser(String targetUserName) {
MenuItem menuItem = new MenuItem(targetUserName);
whisperUsers.add(menuItem);
addWhisperChatButton.getItems().add(menuItem);
menuItem.setOnAction(event -> addWhisperChat(targetUserName));
}
/**
* Creates a new private (whisper) chat model for a specific target user, registers it within
* the chat system, and opens a new chat tab.
*
* @param target The username of the recipient for the private messages.
*/
public void addWhisperChat(String target) {
ChatModel chatModel = new ChatModel(ChatType.WHISPER, username, -1, target);
chatController
.getChatModelMap()
.put(new ChatController.ChatKey(ChatType.WHISPER, target), chatModel);
addChatTab(target, chatModel);
}
/**
* Dynamically loads a new chat tab from an FXML resource and attaches it to the TabPane. It
* initializes a {@link ChatViewController} for the tab and sets up a listener to display
* incoming messages in real-time.
*
* @param title The title to be displayed on the tab header.
* @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.
*/
public void addChatTab(String title, ChatModel chatModel) {
URL resource = getClass().getResource(ressource);
FXMLLoader fxmlLoader = new FXMLLoader(resource);
try {
ChatViewController chatViewController =
new ChatViewController(this.chatController, chatModel, this.username);
fxmlLoader.setController(chatViewController);
Node load = fxmlLoader.load();
VBox.setVgrow(load, Priority.ALWAYS);
VBox vbox = new VBox();
VBox.setVgrow(vbox, Priority.ALWAYS);
vbox.getChildren().add(load);
chatModel.addListener((msg) -> chatViewController.showMessage(msg));
Tab newChat = new Tab(title, vbox);
this.chatTabPane.getTabs().add(newChat);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
@@ -2,106 +2,101 @@ 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.Message;
import java.net.URL;
import java.util.ResourceBundle;
import javafx.application.Platform;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Button; import javafx.scene.control.Button;
import javafx.scene.control.Label; import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane; import javafx.scene.control.ScrollPane;
import javafx.scene.control.TextField; import javafx.scene.control.TextField;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
import java.util.Timer; /** Responsible for the presentation of the ChatModel to the Client */
import java.util.TimerTask; public class ChatViewController implements Initializable {
/** @FXML private Button sendButton;
* Responsible for the presentation of the ChatModel to the Client
*/
public class ChatViewController { @FXML private TextField inputField;
@FXML private VBox chatInterfaceVBox;
@FXML private HBox controlBar;
@FXML private ScrollPane scrollPane;
@FXML private VBox chat;
private final ChatModel chatModel;
private final ChatModel chatmodel;
private final String username; private final String username;
private final ChatController controller; private final ChatController controller;
private final Timer timer;
@FXML
private VBox chatVBox;
@FXML
private TextField inputField;
@FXML
private ScrollPane chatScrollPane;
@FXML
private Button sendButton;
@FXML
private Button refreshButton;
private static final int CHAT_PADDING = 20; private static final int CHAT_PADDING = 20;
public ChatViewController() { public ChatViewController(ChatController chatController, ChatModel chatModel, String username) {
this.username = null; this.controller = chatController;
this.chatmodel = null;
this.controller = null;
this.timer = new Timer();
}
public ChatViewController(String username, ChatModel chatmodel, ChatController controller) {
this.username = username; this.username = username;
this.chatmodel = chatmodel; this.chatModel = chatModel;
this.controller = controller; }
this.timer = new Timer();
if (this.controller != null) { /**
timer.schedule(new TimerTask() { * Initializes the controller after the FXML root element has been processed. Sets up event
* handlers for sending messages via the input field or button and ensures the ScrollPane
* automatically scrolls to the bottom when new messages are added.
*
* @param location The location used to resolve relative paths for the root object.
* @param resourceBundle The resources used to localize the root object.
*/
@Override @Override
public void run() { public void initialize(URL location, ResourceBundle resourceBundle) {
System.err.println("tick");
if (ChatViewController.this.controller.receiveMessage()) {
showMessage();
}
}
}, 0, 1000);
}
}
@FXML
public void initialize() {
if (inputField != null) {
inputField.setOnAction(event -> sendMessage()); inputField.setOnAction(event -> sendMessage());
}
if (sendButton != null) {
sendButton.setOnAction(event -> sendMessage()); sendButton.setOnAction(event -> sendMessage());
} scrollPane.vvalueProperty().bind(chat.heightProperty());
if (refreshButton != null) {
refreshButton.setOnAction(event -> showMessage());
}
if (chatScrollPane != null && chatVBox != null) {
chatScrollPane.vvalueProperty().bind(chatVBox.heightProperty());
}
}
public void showMessage() {
while (chatmodel.count > 0) {
String msg = chatmodel.viewNextMessage();
Label label = new Label(msg);
label.getStyleClass().add("info-text");
label.setWrapText(true);
label.maxWidthProperty().bind(chatVBox.widthProperty().subtract(CHAT_PADDING));
chatVBox.getChildren().add(label);
}
} }
/**
* Retrieves the text from the input field, creates a new {@link Message} object using the
* current model state, and passes it to the {@link ChatController} for network transmission.
* The input field is cleared after sending.
*/
public void sendMessage() { public void sendMessage() {
String message = inputField.getText().trim(); String message = inputField.getText().trim();
if (!message.isEmpty()) { if (!message.isEmpty()) {
inputField.clear(); inputField.clear();
this.controller.sendMessage(message, username); Message msg =
new Message(
chatModel.getChattype(),
chatModel.lobbyId,
username,
chatModel.getTarget(),
message);
controller.onSendToNetwork(msg);
} }
} }
public void endController() { /**
timer.cancel(); * Displays a message in the chat interface. This method creates a new Label for the message
* text and adds it to the message container. It uses {@link Platform#runLater(Runnable)} to
* ensure the UI update happens on the JavaFX Application Thread.
*
* @param msg The {@link Message} object containing the content and metadata to display.
*/
public void showMessage(Message msg) {
Platform.runLater(
() -> {
String msgText =
String.format(
"[%s] %s: %s", msg.timestamp, msg.sender, msg.getMessage());
Label label = new Label(msgText);
label.getStyleClass().add("info-text");
label.setWrapText(true);
label.maxWidthProperty().bind(chat.widthProperty().subtract(CHAT_PADDING));
chat.getChildren().add(label);
});
} }
} }
@@ -3,6 +3,12 @@ package ch.unibas.dmi.dbis.cs108.casono.server;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.check_nick.CheckUsernameHandler; import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.check_nick.CheckUsernameHandler;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.check_nick.CheckUsernameParser; import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.check_nick.CheckUsernameParser;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.check_nick.CheckUsernameRequest; import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.check_nick.CheckUsernameRequest;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_message_count.GetMessageCountHandler;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_message_count.GetMessageCountParser;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_message_count.GetMessageCountRequest;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_next_message.GetNextMessageHandler;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_next_message.GetNextMessageParser;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_next_message.GetNextMessageRequest;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.list_users.ListUsersHandler; import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.list_users.ListUsersHandler;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.list_users.ListUsersParser; import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.list_users.ListUsersParser;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.list_users.ListUsersRequest; import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.list_users.ListUsersRequest;
@@ -15,6 +21,9 @@ import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.logout.LogoutRequest;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.ping.PingHandler; import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.ping.PingHandler;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.ping.PingParser; import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.ping.PingParser;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.ping.PingRequest; 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.SendMessageParser;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.send_message.SendMessageRequest;
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;
import ch.unibas.dmi.dbis.cs108.casono.server.network.NetworkManager; import ch.unibas.dmi.dbis.cs108.casono.server.network.NetworkManager;
@@ -26,12 +35,13 @@ import ch.unibas.dmi.dbis.cs108.casono.server.network.events.EventBus;
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.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 org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.time.Duration; import java.time.Duration;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/** Application class for starting the server. */ /** Application class for starting the server. */
public class ServerApp { public class ServerApp {
@@ -110,6 +120,20 @@ public class ServerApp {
commandRouter.register( commandRouter.register(
LogoutRequest.class, new LogoutHandler(responseDispatcher, userRegistry)); LogoutRequest.class, new LogoutHandler(responseDispatcher, userRegistry));
parserDispatcher.register("SEND_MESSAGE", new SendMessageParser());
commandRouter.register(
SendMessageRequest.class, new SendMessageHandler(responseDispatcher, userRegistry));
parserDispatcher.register("GET_MESSAGE_COUNT", new GetMessageCountParser());
commandRouter.register(
GetMessageCountRequest.class,
new GetMessageCountHandler(responseDispatcher, userRegistry));
parserDispatcher.register("GET_NEXT_MESSAGE", new GetNextMessageParser());
commandRouter.register(
GetNextMessageRequest.class,
new GetNextMessageHandler(responseDispatcher, userRegistry));
parserDispatcher.register("LIST_USERS", new ListUsersParser()); parserDispatcher.register("LIST_USERS", new ListUsersParser());
commandRouter.register( commandRouter.register(
ListUsersRequest.class, new ListUsersHandler(responseDispatcher, userRegistry)); ListUsersRequest.class, new ListUsersHandler(responseDispatcher, userRegistry));
@@ -1,15 +1,12 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.check_nick; package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.check_nick;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.User; import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.User;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserRegistry; import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserRegistry;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler; import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler;
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 java.util.Optional; import java.util.Optional;
/** Handles {@link CheckUsernameRequest}s to check whether a username is available. */ /** Handles {@link CheckUsernameRequest}s to check whether a username is available. */
public class CheckUsernameHandler extends CommandHandler<CheckUsernameRequest> { public class CheckUsernameHandler extends CommandHandler<CheckUsernameRequest> {
private final UserRegistry userRegistry; private final UserRegistry userRegistry;
/** /**
* Creates a new handler for checking username availability. * Creates a new handler for checking username availability.
* *
@@ -20,7 +17,6 @@ public class CheckUsernameHandler extends CommandHandler<CheckUsernameRequest> {
super(responseDispatcher); super(responseDispatcher);
this.userRegistry = userRegistry; this.userRegistry = userRegistry;
} }
/** /**
* Executes the username availability check for the given request. * Executes the username availability check for the given request.
* *
@@ -1,9 +1,7 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.check_nick; package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.check_nick;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.CommandParser; import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.CommandParser;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.PrimitiveRequest; import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.PrimitiveRequest;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.accessor.RequestParameterAccessor; import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.accessor.RequestParameterAccessor;
/** Parses a primitive request into a {@link CheckUsernameRequest}. */ /** Parses a primitive request into a {@link CheckUsernameRequest}. */
public class CheckUsernameParser implements CommandParser<CheckUsernameRequest> { public class CheckUsernameParser implements CommandParser<CheckUsernameRequest> {
/** /**
@@ -1,12 +1,9 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.check_nick; package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.check_nick;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.Request; import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.Request;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext; import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
/** Request implementation used to check whether a username is available or already taken */ /** Request implementation used to check whether a username is available or already taken */
public class CheckUsernameRequest extends Request { public class CheckUsernameRequest extends Request {
private final String username; private final String username;
/** /**
* Constructs a new CheckUsernameRequest with the given context and username to check * Constructs a new CheckUsernameRequest with the given context and username to check
* *
@@ -18,7 +15,6 @@ public class CheckUsernameRequest extends Request {
super(context); super(context);
this.username = username; this.username = username;
} }
/** /**
* Returns the provided username in the request * Returns the provided username in the request
* *
@@ -3,7 +3,6 @@ package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.check_nick;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext; 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.SuccessResponse;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder.ResponseBodyBuilder; import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder.ResponseBodyBuilder;
/** Response indicating the availability status of a username check. */ /** Response indicating the availability status of a username check. */
public class CheckUsernameResponse extends SuccessResponse { public class CheckUsernameResponse extends SuccessResponse {
/** /**
@@ -1,7 +1,7 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.check_nick; package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.check_nick;
/** Represents the availability status of a username */ /** Represents the availability status of a username */
enum UsernameAvailability { public enum UsernameAvailability {
/** Username is available */ /** Username is available */
FREE, FREE,
@@ -0,0 +1,49 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_message_count;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.User;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserRegistry;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.ErrorResponse;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatcher;
import java.util.Optional;
public class GetMessageCountHandler extends CommandHandler<GetMessageCountRequest> {
private final UserRegistry userRegistry;
/**
* Constructs a new GetMessageCountHandler with the necessary response dispatcher and user
* registry.
*
* @param responseDispatcher The dispatcher used to send the count or error back to the client.
* @param userRegistry The registry used to identify the user and access their message queue.
*/
public GetMessageCountHandler(
ResponseDispatcher responseDispatcher, UserRegistry userRegistry) {
super(responseDispatcher);
this.userRegistry = userRegistry;
}
/**
* Processes a request to retrieve the number of pending messages for a user. It looks up the
* user by their session ID; if found, it dispatches a {@link GetMessageCountResponse}
* containing the current count. Otherwise, it dispatches an {@link ErrorResponse}.
*
* @param request The {@link GetMessageCountRequest} containing the session details.
*/
@Override
public void execute(GetMessageCountRequest request) {
Optional<User> user = userRegistry.getBySessionId(request.getSessionId());
if (user.isPresent()) {
int count = user.get().getMessageCount();
GetMessageCountResponse response =
new GetMessageCountResponse(request.getContext(), count);
responseDispatcher.dispatch(response);
} else {
ErrorResponse response =
new ErrorResponse(
request.getContext(), "", "user could not be identified by SessionId");
responseDispatcher.dispatch(response);
}
}
}
@@ -0,0 +1,22 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_message_count;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.CommandParser;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.PrimitiveRequest;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.accessor.RequestParameterAccessor;
public class GetMessageCountParser implements CommandParser<GetMessageCountRequest> {
/**
* Parses a raw {@link PrimitiveRequest} into a {@link GetMessageCountRequest}. This method
* wraps the request context from the network layer into a structured message count request
* object.
*
* @param primitiveRequest The raw request containing parameters and context from the network.
* @return A new {@link GetMessageCountRequest} instance.
*/
@Override
public GetMessageCountRequest parse(PrimitiveRequest primitiveRequest) {
RequestParameterAccessor accessor =
new RequestParameterAccessor(primitiveRequest.parameters());
return new GetMessageCountRequest(primitiveRequest.context());
}
}
@@ -0,0 +1,17 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_message_count;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.Request;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
public class GetMessageCountRequest extends Request {
/**
* Constructs a new GetMessageCountRequest with the specified request context. This request is
* used by a client to query the number of pending messages currently waiting in their
* server-side queue.
*
* @param context The {@link RequestContext} associated with this request.
*/
public GetMessageCountRequest(RequestContext context) {
super(context);
}
}
@@ -0,0 +1,18 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_message_count;
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.ResponseBodyBuilder;
public class GetMessageCountResponse extends SuccessResponse {
/**
* Constructs a new GetMessageCountResponse. It creates a response body containing the "COUNT"
* parameter and associates it with the original request context.
*
* @param context The {@link RequestContext} of the original request.
* @param count The number of pending messages to be returned to the client.
*/
public GetMessageCountResponse(RequestContext context, int count) {
super(context, new ResponseBodyBuilder().param("COUNT", count).build());
}
}
@@ -0,0 +1,49 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_next_message;
import ch.unibas.dmi.dbis.cs108.casono.client.chat.Message;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.User;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserRegistry;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.ErrorResponse;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatcher;
import java.util.Optional;
public class GetNextMessageHandler extends CommandHandler<GetNextMessageRequest> {
private final UserRegistry userRegistry;
/**
* Constructs a new GetNextMessageHandler with the required dispatcher and user registry.
*
* @param responseDispatcher The dispatcher used to send responses back to clients.
* @param userRegistry The registry used to look up users by their session information.
*/
public GetNextMessageHandler(ResponseDispatcher responseDispatcher, UserRegistry userRegistry) {
super(responseDispatcher);
this.userRegistry = userRegistry;
}
/**
* Executes the request to retrieve the next message for a specific user. It identifies the user
* via their session ID, dequeues the next available message, and dispatches a {@link
* GetNextMessageResponse}. If the user cannot be identified, an {@link ErrorResponse} is sent
* instead.
*
* @param request The {@link GetNextMessageRequest} containing the session and context.
*/
@Override
public void execute(GetNextMessageRequest request) {
Optional<User> user = userRegistry.getBySessionId(request.getSessionId());
if (user.isPresent()) {
Message msg = user.get().dequeMessage();
GetNextMessageResponse response = new GetNextMessageResponse(request.getContext(), msg);
responseDispatcher.dispatch(response);
} else {
ErrorResponse response =
new ErrorResponse(
request.getContext(),
"NO_USER_ASSOCIATED",
"user could not be identified by SessionId");
responseDispatcher.dispatch(response);
}
}
}
@@ -0,0 +1,22 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_next_message;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.CommandParser;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.PrimitiveRequest;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.accessor.RequestParameterAccessor;
public class GetNextMessageParser implements CommandParser<GetNextMessageRequest> {
/**
* Parses a raw {@link PrimitiveRequest} into a {@link GetNextMessageRequest}. This method
* initializes a parameter accessor (though not currently used for extraction) and returns a
* structured request object containing the original request context.
*
* @param primitiveRequest The raw request containing parameters and context from the network.
* @return A new {@link GetNextMessageRequest} instance.
*/
@Override
public GetNextMessageRequest parse(PrimitiveRequest primitiveRequest) {
RequestParameterAccessor accessor =
new RequestParameterAccessor(primitiveRequest.parameters());
return new GetNextMessageRequest(primitiveRequest.context());
}
}
@@ -0,0 +1,17 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_next_message;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.Request;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
public class GetNextMessageRequest extends Request {
/**
* Constructs a new GetNextMessageRequest with the specified request context. This request is
* typically used by a client to poll or retrieve the next available message from the server's
* queue.
*
* @param context The {@link RequestContext} associated with this request.
*/
public GetNextMessageRequest(RequestContext context) {
super(context);
}
}
@@ -0,0 +1,19 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_next_message;
import ch.unibas.dmi.dbis.cs108.casono.client.chat.Message;
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;
public class GetNextMessageResponse extends SuccessResponse {
/**
* Constructs a new GetNextMessageResponse. It converts the provided {@link Message} into a
* network-compatible response body and associates it with the original request context.
*
* @param context The {@link RequestContext} of the request being answered.
* @param msg The {@link Message} to be sent back to the client.
*/
public GetNextMessageResponse(RequestContext context, Message msg) {
super(context, msg.toResponse(ResponseBody.builder()));
}
}
@@ -0,0 +1,47 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.send_message;
import ch.unibas.dmi.dbis.cs108.casono.client.chat.Message;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserRegistry;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.OkResponse;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatcher;
public class SendMessageHandler extends CommandHandler<SendMessageRequest> {
private final UserRegistry userRegistry;
/**
* Constructs a new SendMessageHandler with the required dispatcher and user registry.
*
* @param responseDispatcher The dispatcher used to send responses back to clients.
* @param userRegistry The registry containing all currently connected users.
*/
public SendMessageHandler(ResponseDispatcher responseDispatcher, UserRegistry userRegistry) {
super(responseDispatcher);
this.userRegistry = userRegistry;
}
/**
* Processes a message send request. This method extracts the message from the request,
* broadcasts it to all connected users, and dispatches a success response (OK) back to the
* sender.
*
* @param request The {@link SendMessageRequest} containing the message and context.
*/
@Override
public void execute(SendMessageRequest request) {
Message message = request.getMessage();
broadcast(message);
OkResponse response = new OkResponse(request.getContext());
responseDispatcher.dispatch(response);
}
/**
* Distributes a message to every user currently registered in the system. Each user's message
* queue is updated with the new message.
*
* @param message The {@link Message} object to be broadcast.
*/
public void broadcast(Message message) {
userRegistry.getAllUsers().forEach(user -> user.enqueueMessage(message));
}
}
@@ -0,0 +1,22 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.send_message;
import ch.unibas.dmi.dbis.cs108.casono.client.chat.Message;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.CommandParser;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.PrimitiveRequest;
public class SendMessageParser implements CommandParser<SendMessageRequest> {
/**
* Parses a raw {@link PrimitiveRequest} into a specific {@link SendMessageRequest}. This method
* extracts the message details from the request parameters and wraps them along with the
* request context into a structured request object.
*
* @param primitiveRequest The raw request containing parameters and context from the network.
* @return A structured {@link SendMessageRequest} containing the parsed {@link Message}.
*/
@Override
public SendMessageRequest parse(PrimitiveRequest primitiveRequest) {
Message msg = Message.toMessageReqPars(primitiveRequest.parameters());
return new SendMessageRequest(primitiveRequest.context(), msg);
}
}
@@ -0,0 +1,30 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.send_message;
import ch.unibas.dmi.dbis.cs108.casono.client.chat.Message;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.Request;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
public class SendMessageRequest extends Request {
private final Message msg;
/**
* Constructs a new SendMessageRequest with the given context and message.
*
* @param context The {@link RequestContext} associated with this request.
* @param msg The {@link Message} object to be processed.
*/
public SendMessageRequest(RequestContext context, Message msg) {
super(context);
this.msg = msg;
}
/**
* Returns the message contained within this request.
*
* @return The {@link Message} instance.
*/
public Message getMessage() {
return msg;
}
}
@@ -1,15 +0,0 @@
package ch.unibas.dmi.dbis.cs108.casono.server.domain.message;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserRegistry;
public class MessageManager {
private final UserRegistry userRegistry;
public MessageManager(UserRegistry userRegistry) {
this.userRegistry = userRegistry;
}
public void broadcast(Message message) {
userRegistry.getAllUsers().forEach(user -> user.enqueueMessage(message));
}
}
@@ -1,10 +1,11 @@
package ch.unibas.dmi.dbis.cs108.casono.server.domain.user; package ch.unibas.dmi.dbis.cs108.casono.server.domain.user;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.message.Message; import ch.unibas.dmi.dbis.cs108.casono.client.chat.Message;
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionId; import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionId;
import java.time.Instant; import java.time.Instant;
import java.util.ArrayDeque; import java.util.ArrayDeque;
import java.util.List; import java.util.List;
import java.util.NoSuchElementException;
import java.util.Optional; import java.util.Optional;
import java.util.Queue; import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ConcurrentLinkedQueue;
@@ -88,6 +89,14 @@ public class User {
messages.add(message); messages.add(message);
} }
public synchronized int getMessageCount() {
return messages.size();
}
public synchronized Message dequeMessage() throws NoSuchElementException {
return messages.remove();
}
public synchronized List<Message> dequeueAllMessages(Message message) { public synchronized List<Message> dequeueAllMessages(Message message) {
List<Message> allMessages = new ArrayDeque<>(messages).stream().toList(); List<Message> allMessages = new ArrayDeque<>(messages).stream().toList();
messages.clear(); messages.clear();
@@ -9,14 +9,14 @@ import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder.
* <p>Implementations of this class use the {@code +OK} prefix. It provides a protected constructor * <p>Implementations of this class use the {@code +OK} prefix. It provides a protected constructor
* so subclasses can supply the response body content. * so subclasses can supply the response body content.
*/ */
public abstract class SuccessResponse extends Response { public class SuccessResponse extends Response {
/** /**
* Create a successful response with the provided body. * Create a successful response with the provided body.
* *
* @param context the RequestContext of the request * @param context the RequestContext of the request
* @param body the response body * @param body the response body
*/ */
protected SuccessResponse(RequestContext context, ResponseBody body) { public SuccessResponse(RequestContext context, ResponseBody body) {
super(context, body); super(context, body);
} }
@@ -5,37 +5,37 @@
<?import javafx.geometry.Insets?> <?import javafx.geometry.Insets?>
<?import javafx.scene.image.*?> <?import javafx.scene.image.*?>
<!-- Main container: Links the UI to the CasinoGameController and loads Casinogameui.css --> <!-- Hauptcontainer: Verknüpft die UI mit dem CasinoGameController und lädt casinogameui.css -->
<AnchorPane xmlns="http://javafx.com/javafx/21" <AnchorPane xmlns="http://javafx.com/javafx/21"
xmlns:fx="http://javafx.com/fxml/1" xmlns:fx="http://javafx.com/fxml/1"
fx:controller="ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.CasinoGameController" fx:controller="ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.CasinoGameController"
styleClass="background" styleClass="background"
stylesheets="@Casinogameui.css"> stylesheets="@casinogameui.css">
<GridPane prefWidth="1200" prefHeight="800" AnchorPane.topAnchor="0" AnchorPane.bottomAnchor="0" AnchorPane.leftAnchor="0" AnchorPane.rightAnchor="0"> <GridPane prefWidth="1200" prefHeight="800" AnchorPane.topAnchor="0" AnchorPane.bottomAnchor="0" AnchorPane.leftAnchor="0" AnchorPane.rightAnchor="0">
<columnConstraints> <columnConstraints>
<!-- Table-Box: 70% --> <!-- Tisch-Box: 70% -->
<ColumnConstraints percentWidth="70.0" hgrow="ALWAYS" /> <ColumnConstraints percentWidth="70.0" hgrow="ALWAYS" />
<!-- Table-Box: 5% --> <!-- Leere-Box: 5% -->
<ColumnConstraints percentWidth="5.0" hgrow="ALWAYS" /> <ColumnConstraints percentWidth="5.0" hgrow="ALWAYS" />
<!-- Table-Box: 20% --> <!-- Chat-Box: 20% -->
<ColumnConstraints percentWidth="25.0" hgrow="ALWAYS" /> <ColumnConstraints percentWidth="25.0" hgrow="ALWAYS" />
</columnConstraints> </columnConstraints>
<!-- Ensures that this line uses all available vertical space --> <!-- Sorgt dafür, dass diese Zeile den gesamten verfügbaren vertikalen Platz nutzt -->
<rowConstraints> <rowConstraints>
<RowConstraints vgrow="ALWAYS" /> <RowConstraints vgrow="ALWAYS" />
</rowConstraints> </rowConstraints>
<children> <children>
<!-- Table box in column 0 --> <!-- Tisch-Box in Spalte 0 -->
<StackPane GridPane.columnIndex="0"> <StackPane GridPane.columnIndex="0">
<!-- Inner spacing: Keeps the content 10 pixels away from the top/bottom and 20 pixels from the sides --> <!-- Innenabstand: Hält den Inhalt 10 Pixel von oben/unten und 20 Pixel von den Seiten fern -->
<padding> <padding>
<Insets top="20" right="10" bottom="120" left="20"/> <Insets top="20" right="10" bottom="120" left="20"/>
</padding> </padding>
<!-- Central Casino Table: Centers the content and utilizes the entire window due to its maximum size (1.7976931348623157E308) --> <!-- Zentraler Casinotisch: Zentriert den Inhalt und nutzt durch die maximale Größe (1.7976931348623157E308) das gesamte Fenster -->
<VBox fx:id="casinoTable" <VBox fx:id="casinoTable"
alignment="CENTER" alignment="CENTER"
styleClass="casino-table" styleClass="casino-table"
@@ -44,7 +44,7 @@
maxWidth="Infinity" maxWidth="Infinity"
maxHeight="Infinity"> maxHeight="Infinity">
<!-- TODO: Placeholder for the poker playing area: Cards, chips and opponents' bets will be dynamically displayed here later --> <!-- TODO: Platzhalter für die Poker-Spielfläche: Hier werden später dynamisch Karten, Chips und Einsätze der Gegner angezeigt -->
<HBox alignment="CENTER" spacing="15"> <HBox alignment="CENTER" spacing="15">
<VBox styleClass="dealer-box" prefWidth="80" prefHeight="120" /> <VBox styleClass="dealer-box" prefWidth="80" prefHeight="120" />
<VBox styleClass="dealer-box" prefWidth="80" prefHeight="120" /> <VBox styleClass="dealer-box" prefWidth="80" prefHeight="120" />
@@ -52,7 +52,7 @@
</HBox> </HBox>
<VBox alignment="CENTER" spacing="10"> <VBox alignment="CENTER" spacing="10">
<!-- Label with logo --> <!-- Label mit Logo -->
<Label text="CAS0NO" styleClass="table-title"> <Label text="CAS0NO" styleClass="table-title">
<graphic> <graphic>
<ImageView fitHeight="30.0" preserveRatio="true"> <ImageView fitHeight="30.0" preserveRatio="true">
@@ -63,32 +63,32 @@
</graphic> </graphic>
</Label> </Label>
<!-- TODO: Placeholder: Will be replaced once the game engine is finished and can process real-world scenarios --> <!-- TODO: Platzhalter: Wird ersetzt, sobald die GameEngine fertig ist und echte Einsätze verarbeiten kann -->
<Label fx:id="welcomeText" text="Setzen Sie Ihren Einsatz" styleClass="table-title" /> <Label fx:id="welcomeText" text="Setzen Sie Ihren Einsatz" styleClass="table-title" />
</VBox> </VBox>
</VBox> </VBox>
</StackPane> </StackPane>
<!-- Empty box in column 1: Creates space between the table box and the chat box --> <!-- Leere-Box in Spalte 1: Schafft Platz zwischen dem Tisch-Box und der Chat-Box -->
<VBox GridPane.columnIndex="1" <VBox GridPane.columnIndex="1"
alignment="CENTER" alignment="CENTER"
minWidth="100" minWidth="100"
minHeight="100"> minHeight="100">
<!-- Leave blank --> <!-- Bleibt leer -->
</VBox> </VBox>
<!-- TODO: Placeholder: Chat box in column 2: --> <!-- TODO: Platzhalter: Chat-Box in Spalte 2: -->
<fx:include source="components/Chatbox.fxml" GridPane.columnIndex="2"/> <fx:include source="components/Chatbox.fxml" GridPane.columnIndex="2"/>
</children> </children>
</GridPane> </GridPane>
<!-- movable taskbar --> <!-- verschiebbare Taskbar -->
<AnchorPane> <AnchorPane>
<fx:include fx:id="taskbarInclude" source="gameuicomponents/Taskbar.fxml"/> <fx:include fx:id="taskbarInclude" source="gameuicomponents/taskbar.fxml"/>
</AnchorPane> </AnchorPane>
<!-- Opponent Status 1: One of three panels displaying the opponent's icon, name, and account balance --> <!-- Gegner-Status 1: Eines von drei Panels, das Icon, Name und Kontostand des Mitspielers anzeigt -->
<GridPane AnchorPane.leftAnchor="0.0" <GridPane AnchorPane.leftAnchor="0.0"
AnchorPane.rightAnchor="0.0" AnchorPane.rightAnchor="0.0"
AnchorPane.topAnchor="50"> AnchorPane.topAnchor="50">
@@ -99,11 +99,11 @@
</columnConstraints> </columnConstraints>
<children> <children>
<fx:include source="gameuicomponents/Playerstatus.fxml" GridPane.columnIndex="1"/> <fx:include source="gameuicomponents/playerstatus.fxml" GridPane.columnIndex="1"/>
</children> </children>
</GridPane> </GridPane>
<!-- Opponent Status 2: One of three panels displaying the opponent's icon, name, and account balance --> <!-- Gegner-Status 2: Eines von drei Panels, das Icon, Name und Kontostand des Mitspielers anzeigt -->
<GridPane AnchorPane.leftAnchor="0.0" <GridPane AnchorPane.leftAnchor="0.0"
AnchorPane.rightAnchor="0.0" AnchorPane.rightAnchor="0.0"
AnchorPane.topAnchor="20"> AnchorPane.topAnchor="20">
@@ -114,11 +114,11 @@
</columnConstraints> </columnConstraints>
<children> <children>
<fx:include source="gameuicomponents/Playerstatus.fxml" GridPane.columnIndex="1"/> <fx:include source="gameuicomponents/playerstatus.fxml" GridPane.columnIndex="1"/>
</children> </children>
</GridPane> </GridPane>
<!-- Opponent Status 3: One of three panels that displays the opponent's icon, name, and account balance --> <!-- Gegner-Status 3: Eines von drei Panels, das Icon, Name und Kontostand des Mitspielers anzeigt -->
<GridPane AnchorPane.leftAnchor="0.0" <GridPane AnchorPane.leftAnchor="0.0"
AnchorPane.rightAnchor="0.0" AnchorPane.rightAnchor="0.0"
AnchorPane.topAnchor="50"> AnchorPane.topAnchor="50">
@@ -129,7 +129,7 @@
</columnConstraints> </columnConstraints>
<children> <children>
<fx:include source="gameuicomponents/Playerstatus.fxml" GridPane.columnIndex="1"/> <fx:include source="gameuicomponents/playerstatus.fxml" GridPane.columnIndex="1"/>
</children> </children>
</GridPane> </GridPane>
</AnchorPane> </AnchorPane>
@@ -123,7 +123,7 @@
</VBox> </VBox>
<!-- RECHTER BEREICH (Info-Box) --> <!-- RECHTER BEREICH (Info-Box) -->
<fx:include source="components/Chatbox.fxml" GridPane.columnIndex="2"/> <fx:include source="components/chatui/chatbox.fxml" GridPane.columnIndex="2"/>
</children> </children>
</GridPane> </GridPane>
</AnchorPane> </AnchorPane>
@@ -1,57 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.geometry.Insets?>
<!--
This window will later be linked to the server requests,
to display messages from other players and system messages in real time.
The chat internally distinguishes between:
- Player-to-Player (Private 2-person chat)
- Lobby Chat (Current room)
- Global Chat (Entire server)
Features planned for later implementation:
- Stylish chat bubbles with names and timestamps.
-->
<VBox xmlns="http://javafx.com/javafx/21"
xmlns:fx="http://javafx.com/fxml/1"
fx:controller="ch.unibas.dmi.dbis.cs108.casono.client.ui.chatui.ChatViewController"
alignment="CENTER"
stylesheets="@Chatui.css">
<!-- Inner spacing: Creates 30 pixels of space at the top, right and bottom, only 10 pixels on the left (asymmetrical) -->
<padding>
<Insets top="30" right="30" bottom="30" left="10"/>
</padding>
<children>
<VBox VBox.vgrow="ALWAYS"
styleClass="chat-box"
spacing="10"
maxWidth="Infinity">
<children>
<Label text="== CHAT ==" styleClass="chat-header" alignment="CENTER" maxWidth="Infinity"/>
<Region minHeight="4" styleClass="chat-separator"/>
<!-- Chat messages will be added here -->
<ScrollPane fx:id="chatScrollPane" fitToWidth="true" vbarPolicy="AS_NEEDED" hbarPolicy="NEVER" VBox.vgrow="ALWAYS" styleClass="chat-scroll-pane">
<content>
<VBox fx:id="chatVBox" spacing="10" styleClass="chat-VBox"/>
</content>
</ScrollPane>
<!-- Input area -->
<HBox spacing="10">
<!-- TODO: Dynamically adjust the size of the text field -->
<TextField fx:id="inputField" HBox.hgrow="ALWAYS" promptText="Nachricht eingeben..." styleClass="gray-input-field" onAction="#sendMessage"/>
<Button fx:id="sendButton" text="SENDEN" onAction="#sendMessage" styleClass="yellow-button"/>
</HBox>
</children>
</VBox>
</children>
</VBox>
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<VBox xmlns="http://javafx.com/javafx/21"
xmlns:fx="http://javafx.com/fxml/1"
fx:id="chatBox"
VBox.vgrow="ALWAYS"
stylesheets="@chatui.css"
styleClass="chat-box"
spacing="10"
maxWidth="Infinity">
<HBox spacing="10"
fx:id="menuBox">
<Label text="== CHAT ==" styleClass="chat-header" alignment="CENTER" maxWidth="Infinity"/>
<Region HBox.hgrow="ALWAYS"/>
<MenuButton fx:id="addWhisperChatButton" styleClass="yellow-button" text="WHISPER CHAT"
alignment="CENTER_RIGHT">
</MenuButton>
</HBox>
<TabPane fx:id="chatTabPane"
VBox.vgrow="ALWAYS"
styleClass="tab-header-area">
</TabPane>
</VBox>
@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<!-- VBox for the global chat, should only be visible when activated via changeGlobalChatButton-->
<VBox fx:id="chatInterfaceVBox"
maxWidth="Infinity"
spacing="10"
styleClass="chat-box"
stylesheets="@chatui.css"
VBox.vgrow="ALWAYS"
xmlns="http://javafx.com/javafx/17.0.12"
xmlns:fx="http://javafx.com/fxml/1">
<children>
<!-- Chatnachrichten werden hier hinzugefügt -->
<ScrollPane fx:id="scrollPane" fitToWidth="true" vbarPolicy="AS_NEEDED" hbarPolicy="NEVER" styleClass="chat-scroll-pane" VBox.vgrow="ALWAYS">
<content>
<VBox fx:id="chat" spacing="10" styleClass="chat-VBox" />
</content>
</ScrollPane>
<!-- Eingabebereich -->
<HBox fx:id="controlBar" spacing="10">
<!-- TODO: Größe des TextFields dynamisch anpassen -->
<TextField fx:id="inputField" promptText="Nachricht eingeben..." styleClass="gray-input-field" HBox.hgrow="ALWAYS" />
<Button fx:id="sendButton" styleClass="yellow-button" text="SENDEN" />
</HBox>
</children>
</VBox>
@@ -5,7 +5,7 @@
-fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10; -fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10;
-fx-border-width: 12; -fx-border-width: 12;
-fx-padding: 20; -fx-padding: 20;
-fx-effect: dropshadow(one-pass-box, rgba(0,0,0,1), 0, 0, 15, 15); /*-fx-effect: dropshadow(one-pass-box, rgba(0,0,0,1), 0, 0, 15, 15);*/
} }
.chat-header { .chat-header {
@@ -83,7 +83,7 @@
.gray-button { .gray-button {
-fx-background-color: #333333; -fx-background-color: #333333;
-fx-border-color: #666666; -fx-border-color: #666666;
-fx-text-fill: #cccccc; -fx-text-fill: #ffffff;
} }
.gray-button, .yellow-button, .red-button { .gray-button, .yellow-button, .red-button {
@@ -153,3 +153,54 @@
.scroll-pane .scroll-bar:horizontal .thumb:hover { .scroll-pane .scroll-bar:horizontal .thumb:hover {
-fx-pref-height: 10; -fx-pref-height: 10;
} }
.tab-pane .tab-header-area .tab-header-background {
-fx-opacity: 0;
-fx-background-color: #0d9e3b;
-fx-background-radius: 18;
-fx-border-radius: 10;
-fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10;
-fx-border-width: 5;
-fx-padding: 10;
-fx-effect: dropshadow(one-pass-box, rgba(0,0,0,1), 0, 0, 15, 15);
-fx-max-height: 10;
}
.tab-pane .tab {
-fx-background-color: #0d9e3b;
-fx-background-radius: 18;
-fx-border-radius: 10;
-fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10;
-fx-border-width: 5;
-fx-padding: 10;
}
.tab-pane .tab:selected {
-fx-background-color: #0d763b;
-fx-background-radius: 18;
-fx-border-radius: 10;
-fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10;
-fx-border-width: 5;
-fx-padding: 10;
}
.tab-pane .tab .tab-close-button {
-fx-text-fill: #0d9e3b;
}
.tab-pane .tab-header-area .headers-region {
-fx-background-color: #0d9e3b;
}
.tab .tab-label {
-fx-alignment: CENTER;
-fx-text-fill: #ffffff;
-fx-font-size: 12px;
-fx-font-weight: bold;
}
.tab:selected .tab-label {
-fx-alignment: CENTER;
-fx-text-fill: #ffffff;
}
@@ -0,0 +1,9 @@
package ch.unibas.dmi.dbis.cs108.casono.client.chat;
import javafx.application.Application;
public class Chat {
public static void main(String[] args) {
Application.launch(ChatApplication.class);
}
}
@@ -0,0 +1,38 @@
package ch.unibas.dmi.dbis.cs108.casono.client.chat;
import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService;
import ch.unibas.dmi.dbis.cs108.casono.client.network.CoreClient;
import ch.unibas.dmi.dbis.cs108.casono.client.ui.chatui.ChatBoxController;
import java.io.IOException;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.stage.Stage;
public class ChatApplication extends Application {
private static final int SCENE_WIDTH = 1200;
private static final int SCENE_HEIGHT = 800;
String ip = "localhost";
String username = "mathis";
int port = 5000;
public ChatApplication() {}
public void start(Stage stage) throws IOException {
FXMLLoader fxmlLoader =
new FXMLLoader(
getClass().getResource("/ui-structure/components/chatui/chatbox.fxml"));
ClientService clientService = new ClientService(ip, port);
CoreClient coreClient = new CoreClient(clientService);
coreClient.login(username);
ChatController chatController = new ChatController(username, clientService);
ChatBoxController chatBoxController = new ChatBoxController(username, chatController);
fxmlLoader.setController(chatBoxController);
Scene scene = new Scene(fxmlLoader.load(), SCENE_WIDTH, SCENE_HEIGHT);
stage.setTitle("Chat");
stage.setScene(scene);
stage.show();
}
}
@@ -0,0 +1,39 @@
package ch.unibas.dmi.dbis.cs108.casono.client.chat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.RequestParameter;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.Test;
public class ChatTest {
@Test
public void chatTest() {
List<String> list = new ArrayList<String>();
list.add("COUNT=19199");
list.add("TEXT='test \\' test'");
list.add("TEXT='%/§§%&&/%=%$/%))==/?``*\\'\\'**\\'\\'§?'");
list.add("TEXT='Hello World'");
list.add("NUMBER=-56887387394898392849");
List<RequestParameter> newList = ClientService.convertToRequestParameters(list);
RequestParameter par0 = newList.get(0);
assertEquals("COUNT", par0.key());
assertEquals("19199", par0.value());
RequestParameter par1 = newList.get(1);
assertEquals("TEXT", par1.key());
assertEquals("test ' test", par1.value());
RequestParameter par2 = newList.get(2);
assertEquals("TEXT", par2.key());
assertEquals("%/§§%&&/%=%$/%))==/?``*''**''§?", par2.value());
RequestParameter par4 = newList.get(4);
assertEquals("NUMBER", par4.key());
assertEquals("-56887387394898392849", par4.value());
}
}