Fix: Parsing of Server Responses for Client Chat

This commit is contained in:
Mathis Ginkel
2026-04-08 23:06:25 +02:00
parent 4e7b0ab67a
commit c0c9225d42
17 changed files with 196 additions and 93 deletions
@@ -3,6 +3,7 @@ package ch.unibas.dmi.dbis.cs108.casono.client;
/**
* Entry point for the Casono client application. Handles client startup and connection parameters.
*/
import ch.unibas.dmi.dbis.cs108.casono.client.ui.Launcher;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@@ -77,13 +77,14 @@ public class ChatController {
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:
// TODO: Check, if target person is user and if yes, iterate through all
if (msg.target.equals(username)) {
if (chatModelMap.containsKey(new ChatKey(ChatType.WHISPER, msg.sender))) {
chatModelMap.get(new ChatKey(ChatType.WHISPER, msg.sender)).addMessage(msg);
@@ -93,6 +94,7 @@ public class ChatController {
chatBoxController.addWhisperChat(msg.sender, value);
}
}
break;
}
@@ -1,10 +1,10 @@
package ch.unibas.dmi.dbis.cs108.casono.client.chat;
import ch.unibas.dmi.dbis.cs108.casono.client.ui.chatui.ChatViewController;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import java.util.ArrayList;
import java.util.function.Consumer;
/**
* ChatModel, stores the data for a specific chat
@@ -13,6 +13,8 @@ import java.util.ArrayList;
*/
public class ChatModel {
private ArrayList<Consumer<Message>> listeners = new ArrayList<>();
public ArrayList<Message> messages;
private final ChatType chattype;
@@ -62,18 +64,12 @@ public class ChatModel {
*/
public synchronized void addMessage(Message msg) {
messages.add(msg);
count.add(1);
listeners.stream().forEach((l)-> l.accept(messages.getLast()));
}
public void addListener(ChatViewController chatViewController) {
count.addListener(
(_count, _p, n) ->
{
if(n.intValue() > 0) {
chatViewController.showMessage();
}
});
public void addListener(Consumer<Message> listener) {
this.listeners.add(listener);
}
public String getTarget() {
@@ -1,12 +1,14 @@
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 org.jspecify.annotations.NonNull;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.regex.Matcher;
import java.util.Optional;
import java.util.regex.Pattern;
/**
@@ -80,10 +82,16 @@ public class Message {
* @return - request as specified in the network protocol, as String
*/
public String toArgsString() {
String gameIdString="";
if(lobbyId >= 0) {
gameIdString=" GAME="+lobbyId;
} else {
gameIdString=" GAME='-1'";
}
return String.format(
"TYPE=%s GAME=%d USER=%s TARGET=%s TIME=%s TEXT='%s'",
"TYPE=%s%s USER='%s' TARGET='%s' TIME='%s' TEXT='%s'",
this.type.toString(),
this.lobbyId,
gameIdString,
this.sender,
this.target,
this.timestamp,
@@ -93,16 +101,18 @@ public class Message {
/** Pattern, to analyze the response String with the given parameters */
public static Pattern msgRex =
Pattern.compile(
"TYPE=(?<type>\\w+) " + "GAME=(?<game>\\w+) " +
"USER=(?<user>\\w+) " + "TARGET=(?<target>\\w+) " +
"TYPE=(?<type>\\w+) " + "(GAME=(?<game>\\w+) )?" +
"USER=(?<user>\\w+) " + "(TARGET=(?<target>\\w+) )?" +
"TIME=(?<time>[0-9:.]+) " + "TEXT='(?<text>([^']|\\')+)'");
/**
* Method to create a Message Object, from the information given by the String
*
* @param response - String that got sent as a response from the server
* @return - New Message Object
*/
/*
public static Message toMessage(String response) {
Matcher m = msgRex.matcher(response);
if (!m.matches()) {
@@ -114,16 +124,18 @@ public class Message {
case "GLOBAL":
return new Message(
ChatType.GLOBAL,
0,
-1,
m.group("user"),
null,
m.group("time"),
m.group("text"));
case "LOBBY":
int gameId = getGameId(m.group("game"));
return new Message(
ChatType.LOBBY,
Integer.parseInt(m.group("game")),
gameId,
m.group("user"),
null,
m.group("time"),
@@ -132,7 +144,7 @@ public class Message {
case "WHISPER":
return new Message(
ChatType.WHISPER,
Integer.parseInt(m.group("game")),
-1,
m.group("user"),
m.group("target"),
m.group("time"),
@@ -142,13 +154,22 @@ public class Message {
throw new RuntimeException("Unknown message type " + typeString);
}
}
*/
public static Message toMessage(List<RequestParameter> parameters) {
private static int getGameId(String gameIdStr) {
int gameId = -1;
if (gameIdStr != null) {
gameId = Integer.parseInt(gameIdStr);
}
return gameId;
}
public static Message toMessageReqPars(List<RequestParameter> parameters) {
String typeString = getParString(parameters, "TYPE");
ChatType type = ChatType.valueOf(typeString);
return switch (type) {
case GLOBAL -> new Message(ChatType.GLOBAL,
0,
-1,
getParString(parameters, "USER"),
null,
getParString(parameters, "TIME"),
@@ -162,7 +183,7 @@ public class Message {
getParString(parameters, "TEXT")
);
case WHISPER -> new Message(ChatType.WHISPER,
Integer.parseInt(getParString(parameters, "GAME")),
Integer.parseInt(getParString(parameters, "GAME", "-1")),
getParString(parameters, "USER"),
getParString(parameters, "TARGET"),
getParString(parameters, "TIME"),
@@ -171,11 +192,32 @@ public class Message {
};
}
private static @NonNull String getParString(List<RequestParameter> parameters, String keyString) {
return parameters.stream().filter((p) -> "TYPE".equals(p.key()))
return getParString(parameters, keyString, null);
}
private static @NonNull String getParString(List<RequestParameter> parameters, String keyString, String defaultVal) {
Optional<String> parOption = parameters.stream().filter((p) -> keyString.equals(p.key()))
.findFirst()
.map(RequestParameter::value)
.orElseThrow(() -> new RuntimeException("No " + keyString + " found"));
.map(RequestParameter::value);
if(parOption.isEmpty()) {
if (defaultVal == null) {
throw new RuntimeException("No " + keyString + " found");
} else {
return defaultVal;
}
}
return parOption.get();
}
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,11 +1,12 @@
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.server.network.command.parsing.RequestParameter;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* The ChatClient class is responsible for sending messages to the server and
@@ -15,6 +16,7 @@ import java.util.regex.Pattern;
public class ChatClient {
private final ClientService clientService;
private final Logger logger;
/**
* Constructs a ChatClient with the given ClientService for communication.
@@ -24,6 +26,7 @@ public class ChatClient {
*/
public ChatClient(ClientService clientService) {
this.clientService = clientService;
this.logger = LogManager.getLogger(ChatClient.class);
}
/**
@@ -34,6 +37,7 @@ public class ChatClient {
*/
public void sendMessage(Message message) {
String request = "SEND_MESSAGE " + message.toArgsString();
logger.info("Writing to server: " + request);
clientService.processCommand(request);
}
@@ -47,24 +51,20 @@ public class ChatClient {
* the server.
*/
public List<Message> getMessages() {
String countStr = clientService.processCommand("GET_MESSAGE_COUNT");
Matcher m = countRex.matcher(countStr);
if (!m.matches()) {
throw new RuntimeException("Can not parse response: " + countStr);
logger.info("Asking server for new messages");
List<RequestParameter> countStr = ClientService.convertToRequestParameters(clientService.processCommand("GET_MESSAGE_COUNT"));
RequestParameter countRes = countStr.getFirst();
if (!countRes.key().equals("COUNT")) {
logger.error("Not the right response from server");
}
int count = Integer.parseInt(m.group("count"));
System.out.println("Got " + count + " messages");
List<Message> messages = new ArrayList<>();
int count = Integer.parseInt(countRes.value());
logger.info("Got " + count + " messages");
ArrayList<Message> messages = new ArrayList<>();
for (int i = 0; i < count; i++) {
String message = clientService.processCommand("GET_NEXT_MESSAGE");
if (message != null) {
Message message1 = Message.toMessage(message);
messages.add(message1);
}
List<RequestParameter> msgRes = ClientService.convertToRequestParameters(clientService.processCommand("GET_NEXT_MESSAGE"));
Message msg = Message.toMessageReqPars(msgRes);
messages.add(msg);
}
return messages;
}
public static Pattern countRex = Pattern.compile("COUNT=(?<count>[0-9]+)");
}
// (?<key>\w+)='(?<string>([^']|\')+)'|(?<primVal>[\d\w]+)
@@ -1,5 +1,6 @@
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.TcpTransport;
import org.apache.logging.log4j.LogManager;
@@ -8,12 +9,14 @@ import org.apache.logging.log4j.Logger;
import java.io.IOException;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* The ClientService class is responsible for managing the connection to the
@@ -58,6 +61,8 @@ public class ClientService {
}
static Pattern responseRex = Pattern.compile("(?<key>\\w+)=(('(?<string>([^']|\\')+)')|(?<primVal>[+-]?[\\d\\w:]+))");
/**
* Sends the Requests to the server and waits for the response If the response is "+OK" it
* proceeds normal If the response "-ERROR" it throws a runtime exception
@@ -65,36 +70,61 @@ public class ClientService {
* @param message
* @return - The response as a string, if it has to be returned (+OK will not be returned)
*/
protected String processCommand(String message) {
AtomicReference<String> response = new AtomicReference<>();
private static String unescape(String input) {
return input.replaceAll("\\\\'", "'");
}
public static List<RequestParameter> convertToRequestParameters(List<String> input) {
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();
}
protected List<String> processCommand(String message) {
List<String> response = new ArrayList<>();
sendRequest(
() -> {
try {
writeToTransport(message);
String responseText = null;
do {
responseText = clienttcptransport.read().payload();
logger.info("Raw message '" + responseText + "'");
for(String line: responseText.split("\n")) {
responseText = clienttcptransport.read().payload();
logger.info("Raw message '" + responseText + "'");
Boolean success = null;
int count = 0;
for(String line: responseText.split("\n")) {
if (success == null) {
if ("+OK".equals(line)) {
return;
success = true;
continue;
} else if (("-ERROR").equals(responseText)) {
throw new RuntimeException(responseText);
} else {
String start = response.get();
if(start == null) {
response.set(line);
} else {
response.set(start + "\n" + line);
}
success = false;
}
continue;
} else if ("END".equals(line)) {
break;
}
} while (true);
line = line.replaceFirst("^\t", "");
response.add(line);
}
if (success!= null && success) {
return;
} else {
throw new RuntimeException("Error in "+message+": "+response);
}
} catch (Exception e) {
throw getRuntimeException(e);
}
});
return response.get();
return response;
}
/**
@@ -4,7 +4,6 @@ import ch.unibas.dmi.dbis.cs108.casono.client.game.Card;
import ch.unibas.dmi.dbis.cs108.casono.client.game.GameState;
import ch.unibas.dmi.dbis.cs108.casono.client.game.Player;
import java.util.ArrayList;
import java.util.List;
/**
@@ -33,7 +32,7 @@ public class GameClient {
* @return A GameState object representing the current state of the game.
*/
public GameState getGameState() {
String response = client.processCommand("GET_GAME_STATE");
List<String> response = client.processCommand("GET_GAME_STATE");
return parseGameState(response);
}
@@ -43,16 +42,14 @@ public class GameClient {
* @param input The raw response string from the server.
* @return A GameState object representing the current state of the game.
*/
private GameState parseGameState(String input) {
private GameState parseGameState(List<String> input) {
GameState state = new GameState();
String[] lines = input.split("\n");
Player currentPlayer = null;
Card currentCard = null;
for (String rawLine : lines) {
for (String rawLine : input) {
String line = rawLine.trim();
@@ -27,7 +27,7 @@ public class LobbyClient {
* by the server.
*/
public String fetchLobbyStatusString(int lobbyId) {
return client.processCommand("GET_LOBBY_STATUS ID=" + lobbyId);
return client.processCommand("GET_LOBBY_STATUS ID=" + lobbyId).getFirst();
}
/**
@@ -37,7 +37,7 @@ public class LobbyClient {
* @return The id of the newly created lobby, as returned by the server.
*/
public int createLobby() {
String response = client.processCommand("CREATE_LOBBY");
String response = client.processCommand("CREATE_LOBBY").getFirst();
return Integer.parseInt(response);
}
@@ -49,7 +49,7 @@ public class LobbyClient {
* the server.
*/
public int getLobbyId() {
String response = client.processCommand("GET_LOBBY_ID");
String response = client.processCommand("GET_LOBBY_ID").getFirst();
return Integer.parseInt(response);
}
@@ -51,7 +51,7 @@ public class ChatBoxController {
URL resource = getClass().getResource(ressource);
FXMLLoader fxmlLoader = new FXMLLoader(resource);
ChatViewController chatViewController = new ChatViewController(username, chatModel, chatController);
chatModel.addListener(chatViewController);
chatModel.addListener((msg)-> chatViewController.showMessage(msg));
try {
fxmlLoader.setController(chatViewController);
Tab newChat = new Tab(title, fxmlLoader.load());
@@ -22,13 +22,13 @@ public class ChatViewController implements Initializable {
@FXML private TextField inputField;
@FXML private VBox chatVBox;
@FXML private VBox chatInterfaceVBox;
@FXML private HBox controlBar;
@FXML private ScrollPane scrollPane;
@FXML private VBox Chat;
@FXML private VBox chat;
private final ChatModel chatModel;
@@ -54,7 +54,7 @@ public class ChatViewController implements Initializable {
public void initialize(URL location, ResourceBundle resourceBundle) {
inputField.setOnAction(event -> sendMessage());
sendButton.setOnAction(event -> sendMessage());
scrollPane.vvalueProperty().bind(chatVBox.heightProperty());
scrollPane.vvalueProperty().bind(chatInterfaceVBox.heightProperty());
}
public void sendMessage() {
@@ -66,13 +66,13 @@ public class ChatViewController implements Initializable {
}
}
public void showMessage() {
String msg = chatModel.viewNextMessage();
Label label = new Label(msg);
public void showMessage(Message msg) {
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(chatVBox.widthProperty().subtract(CHAT_PADDING));
chatVBox.getChildren().add(label);
label.maxWidthProperty().bind(chat.widthProperty().subtract(CHAT_PADDING));
chat.getChildren().add(label);
}
@@ -31,7 +31,7 @@ public class GetNextMessageHandler implements CommandHandler<GetNextMessageReque
} else {
ErrorResponse response = new ErrorResponse(
request.getContext(),
"",
"NO_USER_ASSOCIATED",
"user could not be identified by SessionId"
);
@@ -7,8 +7,7 @@ import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder.
public class GetNextMessageResponse extends SuccessResponse {
public GetNextMessageResponse(RequestContext context, Message msg) {
super(context, ResponseBody.builder()
.param("MESSAGE", msg.toArgsString())
.build());
super(context, msg.toResponse(ResponseBody.builder())
);
}
}
@@ -7,7 +7,7 @@ import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.Primitive
public class SendMessageParser implements CommandParser<SendMessageRequest> {
@Override
public SendMessageRequest parse(PrimitiveRequest primitiveRequest) {
Message msg = Message.toMessage(primitiveRequest.parameters());
Message msg = Message.toMessageReqPars(primitiveRequest.parameters());
return new SendMessageRequest(primitiveRequest.context(), msg);
}
}