Merge remote-tracking branch 'origin/main'

This commit is contained in:
Mathis Ginkel
2026-03-20 13:23:01 +01:00
24 changed files with 240 additions and 689 deletions
+1
View File
@@ -122,3 +122,4 @@ $RECYCLE.BIN/
## bin ## bin
bin/ bin/
gradle.properties
@@ -0,0 +1,31 @@
## Bug Report
### Summary
<!-- Short, precise description of the bug -->
### Affected Component
<!-- e.g. Lobby Menu, Global Chat, Loading screen, ... -->
### Steps to Reproduce
1. Do this first
2. Then do that next
3. Lastly, see the unexprected error message ...
### Expected Behavior
<!-- What should happen? -->
### Actual Behavior
<!-- What happens instead? -->
### Stack Trace / Error Message
```
<!-- Paste stack trace or log output here -->
```
### Possible Cause
<!-- Optional: your own hypothesis about the root cause -->
/label ~bug
/label ~client
@@ -0,0 +1,30 @@
## Bug Report
### Summary
<!-- Short, precise description of the bug -->
### Affected Component
<!-- e.g. TransportLayer, CommandRouter, UserRegistry, Response, ... -->
### Steps to Reproduce
1. Do this first
2. Then do that next
3. Lastly, see the unexprected error message ...
### Expected Behavior
<!-- What should happen? -->
### Actual Behavior
<!-- What happens instead? -->
### Stack Trace / Error Message
```
<!-- Paste stack trace or log output here -->
```
### Possible Cause
<!-- Optional: your own hypothesis about the root cause -->
/label ~bug
/label ~server
@@ -0,0 +1,27 @@
## Feature Request
### Summary
<!-- Brief description of the desired functionality -->
### Motivation / Problem
<!-- What problem does this feature solve? Why is it needed? -->
### Description
<!-- Detailed description of the desired behavior -->
### Affected Components
<!-- Which existing components are affected or need to be extended? -->
<!-- e.g. Lobby Menu, Global Chat, Loading screen, … -->
### Acceptance Criteria
<!-- What must be fulfilled for this feature to be considered done? -->
- [ ]
- [ ]
- [ ]
### Open Questions / Trade-offs
<!-- Are there design decisions that still need to be discussed? -->
/label ~enhancement
/label ~client
@@ -0,0 +1,27 @@
## Feature Request
### Summary
<!-- Brief description of the desired functionality -->
### Motivation / Problem
<!-- What problem does this feature solve? Why is it needed? -->
### Description
<!-- Detailed description of the desired behavior -->
### Affected Components
<!-- Which existing components are affected or need to be extended? -->
<!-- e.g. CommandRouter, SessionManager, UserRegistry, … -->
### Acceptance Criteria
<!-- What must be fulfilled for this feature to be considered done? -->
- [ ]
- [ ]
- [ ]
### Open Questions / Trade-offs
<!-- Are there design decisions that still need to be discussed? -->
/label ~enhancement
/label ~server
@@ -0,0 +1,37 @@
## Protocol / API Change
### Summary
<!-- Brief description of the protocol change -->
### Affected Commands / Responses
<!-- Which commands or response types are being added, changed, or removed? -->
| Command | Response | Type of Change | Description |
|---------|----------|----------------|-------------|
| | | New / Changed / Removed | |
### New / Changed Syntax
```
<!-- Before / after or new command syntax here -->
<!-- Example:
BEFORE: LOGIN UNAME=<username> PASS=<password>
AFTER: LOGIN USERNAME=<username> PASSWORD=<password>
-->
```
### Affected Implementation Layers
<!-- Which components / classes need to be updated? -->
- [ ] `Session`
- [ ] `User` or `UserRegistry`
- [ ] `Tokenizer` / `RequestTokenClassifier`
- [ ] `ProtocolParser`
- [ ] `CommandParserDispatcher` + corresponding `CommandParser`
- [ ] `CommandHandler`
- [ ] `Response` hierarchy
- [ ] `GameEngine` or `GameState`
- [ ] Client-side (create your own issue and create link)
### Motivation
<!-- Why is this protocol change necessary? -->
/label ~protocol
+25
View File
@@ -0,0 +1,25 @@
## Task
### Summary
<!-- What needs to be implemented? -->
### Context
<!-- Which part of the system does it belong to? -->
### Affected Components
<!-- e.g. Controller, View, Model, Network, ... -->
### Definition of Done
- [ ]
- [ ]
- [ ]
### Notes
<!-- Optional: implementation hints, links to prior design discussions, etc. -->
### Checklist
- [ ] Linked to other issues or branches that must be completed first?
/label ~task
/label ~client
+25
View File
@@ -0,0 +1,25 @@
## Task
### Summary
<!-- What needs to be implemented? -->
### Context
<!-- Which part of the system does it belong to? -->
### Affected Components
<!-- e.g. Response hierarchy, CommandRouter, UserRegistry, ... -->
### Definition of Done
- [ ]
- [ ]
- [ ]
### Notes
<!-- Optional: implementation hints, links to prior design discussions, etc. -->
### Checklist
- [ ] Linked to other issues or branches that must be completed first?
/label ~task
/label ~server
+8 -12
View File
@@ -20,7 +20,7 @@
<!-- https://checkstyle.sourceforge.io/checks/whitespace/filetabcharacter.html --> <!-- https://checkstyle.sourceforge.io/checks/whitespace/filetabcharacter.html -->
<module name="FileTabCharacter"/> <module name="FileTabCharacter"/>
<!-- Maximale Zeilenlänge --> <!-- Maximale Zeilenlänge (AOSP enforces 100 lines, the Google Java Style Guide 80)-->
<!-- https://checkstyle.sourceforge.io/checks/sizes/linelength.html --> <!-- https://checkstyle.sourceforge.io/checks/sizes/linelength.html -->
<module name="LineLength"> <module name="LineLength">
<property name="max" value="100"/> <property name="max" value="100"/>
@@ -42,16 +42,6 @@
"/> "/>
</module> </module>
<!-- Enforce indentation of four whitespaces -->
<!-- https://checkstyle.sourceforge.io/checks/misc/indentation.html -->
<module name="Indentation">
<property name="basicOffset" value="4"/>
<property name="caseIndent" value="4"/>
<property name="throwsIndent" value="4"/>
<property name="arrayInitIndent" value="4"/>
<property name="lineWrappingIndentation" value="8"/>
</module>
<!-- No wildcard imports (import x.*) --> <!-- No wildcard imports (import x.*) -->
<!-- https://checkstyle.sourceforge.io/checks/imports/avoidstarimport.html --> <!-- https://checkstyle.sourceforge.io/checks/imports/avoidstarimport.html -->
<module name="AvoidStarImport"/> <module name="AvoidStarImport"/>
@@ -62,7 +52,13 @@
<!-- No unordered / ungrouped imports --> <!-- No unordered / ungrouped imports -->
<!-- https://checkstyle.sourceforge.io/checks/imports/importorder.html --> <!-- https://checkstyle.sourceforge.io/checks/imports/importorder.html -->
<module name="ImportOrder"/> <module name="ImportOrder">
<property name="option" value="top"/>
<property name="groups" value="/^import static\..+/,*"/>
<property name="separated" value="true"/>
<property name="separatedStaticGroups" value="true"/>
<property name="sortStaticImportsAlphabetically" value="true"/>
</module>
<!-- Classes, enums, records, ... as PascalCase --> <!-- Classes, enums, records, ... as PascalCase -->
<!-- https://checkstyle.sourceforge.io/checks/naming/typename.html --> <!-- https://checkstyle.sourceforge.io/checks/naming/typename.html -->
@@ -1,136 +0,0 @@
package ch.unibas.dmi.dbis.cs108.casono.client.chat;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Message Object for internal handling of Chat-Messages
* TODO: Should be used on both sides of the network
*/
public class Message {
private final MessageType type;
private final String message;
public String user;
public String timestamp;
public int game_id = 0;
public String target = null;
public enum MessageType {
GLOBAL, LOBBY, WHISPER
};
/**
* Constructor for creating Messages with all information given
* @param type - Either global, local or whisper
* @param game_id - lobby id, or null, if the type is global
* @param user - username
* @param target - username of the target user, for whisper chat
* @param message
*/
public Message(MessageType type, int game_id, String user, String target, String timestamp, String message) {
this.type = type;
this.game_id = game_id;
this.user = user;
this.target = target;
this.timestamp = timestamp;
this.message = message;
}
/**
* Constructor for creating the Messages of the current user, using this client -> time of writing is being recorded
* @param type - Either global, local or whisper
* @param game_id - lobby id, or null, if the type is global
* @param user - username
* @param target - username of the target user, for whisper chat
* @param message
*/
public Message(MessageType type, int game_id, String user, String target, String message) {
this.type = type;
this.game_id = game_id;
this.user = user;
this.target = target;
this.message = message;
LocalDateTime now = LocalDateTime.now();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm");
this.timestamp = now.format(formatter);
}
public String getMessage() {
return message;
}
public MessageType getMessageType() {
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
* @return - request as specified in the network protocol, as String
*/
public String toArgsString() {
return String.format("TYPE=%s GAME=%d USER=%s TARGET=%s TIME=%s TEXT=%s",
this.type.toString(), this.game_id, this.user, this.target, this.timestamp, this.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+) 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()) {
throw new RuntimeException("Can not parse message: '" + response+"'");
}
String typeString=m.group("type");
switch (typeString) {
case "GLOBAL":
return new Message(MessageType.GLOBAL,
0,
m.group("user"),
null,
m.group("time"),
m.group("text"));
case "LOBBY":
return new Message(MessageType.LOBBY,
Integer.parseInt(m.group("game")),
m.group("user"),
null,
m.group("time"),
m.group("text"));
case "WHISPER":
return new Message(MessageType.WHISPER,
Integer.parseInt(m.group("game")),
m.group("user"),
m.group("target"),
m.group("time"),
m.group("text"));
default:
throw new RuntimeException("Unknown message type " + typeString);
}
}
}
@@ -1,204 +0,0 @@
package ch.unibas.dmi.dbis.cs108.casono.client.network;
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.io.IOException;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import ch.unibas.dmi.dbis.cs108.casono.client.chat.Message;
import ch.unibas.dmi.dbis.cs108.casono.server.network.transport.RawPacket;
import ch.unibas.dmi.dbis.cs108.casono.server.network.transport.TcpTransport;
/**
* Responsible for the transferring of the data from the Client to the Server and the other way around
*/
public class ClientService {
private final TcpTransport clienttcptransport;
private final Socket socket;
private final String ip;
private final int port;
private final ExecutorService executor;
public static ArrayList<String> response;
private final AtomicInteger idGenerator;
/**
* Creates a new ClientSession with a Socket, a Reader and Writer of the Input- and the Outputstream and a pool of threads to send requests and receive responses.
* @param ip : ip-adress of the server
* @param port : port of the server
*/
public ClientService(String ip, int port) {
this.ip = ip;
this.port = port;
this.idGenerator = new AtomicInteger(0);
try {
socket = new Socket(this.ip, this.port);
System.out.println("Connected");
clienttcptransport = new TcpTransport(socket);
}
catch (IOException i) {
throw new RuntimeException(i);
}
executor = Executors.newSingleThreadExecutor();
}
/**
* Sends a ping to the server
* Returns nothing, the method processMessage() already handles the cases "+OK" or "-ERROR"
*/
public void ping() {
processMessage("PING");
}
/**
* Sends a login request to the server to create a new user on the server
* @param username
* @return - a new username, if the same username is already used by someone else
*/
public String login(String username) {
String msg = "LOGIN USERNAME=" + username;
return processMessage(msg);
}
/**
* Send a Request to get the number of Messages currently in the Queue for the client.
* Then proceeds, if needed, to get the messages by sending
*/
public List<Message> getMessages() {
String countStr = processMessage("GET_MESSAGE_COUNT");
int count = Integer.parseInt(countStr);
System.out.println("Got " + count + " messages");
List<Message> messages = new ArrayList<>();
for (int i = 0; i < count; i++) {
String message = processMessage("GET_NEXT_MESSAGE");
if (message != null) {
Message message1 = Message.toMessage(message);
messages.add(message1);
}
}
return messages;
}
/**
* Sends a Request to the Server containing all relevant information of the message the client wrote.
* @param message
* @return
*/
public void sendMessage(Message message) {
String request = "SEND_MESSAGE " + message.toArgsString();
processMessage(request);
}
/**
* 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
* @param message
* @return - The response as a string, if it has to be returned
* (+OK will not be returned)
*/
private String processMessage(String message) {
AtomicReference<String> response = new AtomicReference<>();
sendRequest(() -> {
try {
writeToTransport(message);
String responseLine=null;
do {
responseLine = clienttcptransport.read().payload();
System.out.println("Raw message '" + responseLine + "'");
if ("+OK".equals(responseLine)) {
return;
} else if (("-ERROR").equals(responseLine)) {
throw new RuntimeException(responseLine);
}
response.set(responseLine);
} while(true);
} catch (Exception e) {
throw getRuntimeException(e);
}
});
return response.get();
}
/**
*
* @param request
*/
private void sendRequest(Runnable request) {
Future<?> future = executor.submit(request);
try {
future.get();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (ExecutionException e) {
throw getRuntimeException(e);
}
}
/**
* Returns a Runtime Exceptions thrown by the sendRequest method
* @param e - an Exception
* @return - a Runtime Exception
*/
private static RuntimeException getRuntimeException(Exception e) {
Throwable reason = e.getCause();
RuntimeException re;
if (reason == null) {
reason = e;
} else if (reason instanceof RuntimeException rte) {
re = rte;
}
re = new RuntimeException(reason);
return re;
}
/**
* Closes the Socket and shuts down the Threadpool associated with that Socket-Connection.
*/
public void closeSocket() {
try {
executor.shutdown();
clienttcptransport.close();
socket.close();
} catch (IOException j) {
System.out.println(j);
}
}
/**
* Method to write with the tcp transport to the server
* @param s - Message to be sent
* @throws IOException
*/
private void writeToTransport(String s) throws IOException {
int id = this.idGenerator.incrementAndGet();
this.clienttcptransport.write(new RawPacket(id, s));
}
}
@@ -1,59 +0,0 @@
package ch.unibas.dmi.dbis.cs108.casono.server;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
public class Server {
private ServerSocket ss = null;
private BufferedReader input = null;
private BufferedWriter output = null;
public Server(int port) {
try {
ss = new ServerSocket(port);
System.out.println("Server started");
}
catch (IOException e) {
System.out.println(e.getMessage());
}
}
public void simpleListenLoop() {
System.out.println("Waiting for connection...");
Socket s = null;
try {
while (true) {
s = ss.accept();
System.out.println("Accepted connection");
input = new BufferedReader(new InputStreamReader(s.getInputStream()));
output = new BufferedWriter(new OutputStreamWriter(s.getOutputStream()));
String line = null;
while((line = input.readLine())!=null) {
System.out.println("Server got "+ line);
// do something with this line
output.write("OK"+"\n");
output.flush();
if(Thread.currentThread().isInterrupted()) {
return;
}
}
if(Thread.currentThread().isInterrupted()) {
return;
}
}
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
try {
s.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
@@ -25,10 +25,6 @@ public class ServerApp {
Logger logger = LogManager.getLogger(ServerApp.class); Logger logger = LogManager.getLogger(ServerApp.class);
logger.info("Starting server at port {}", port); logger.info("Starting server at port {}", port);
startServerCore(port);
}
public static void startServerCore(int port) {
EventBus eventBus = new EventBus(); EventBus eventBus = new EventBus();
SessionManager sessionManager = new SessionManager(); SessionManager sessionManager = new SessionManager();
eventBus.subscribe( eventBus.subscribe(
@@ -1,15 +0,0 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.events;
import ch.unibas.dmi.dbis.cs108.casono.client.chat.Message;
public class ChatMessageEvent implements Event {
private final Message message;
public ChatMessageEvent(Message message) {
this.message = message;
}
public Message getMessage() {
return message;
}
}
@@ -1,56 +0,0 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.handlers;
import ch.unibas.dmi.dbis.cs108.casono.client.chat.Message;
import ch.unibas.dmi.dbis.cs108.casono.server.network.events.ChatMessageEvent;
import ch.unibas.dmi.dbis.cs108.casono.server.network.events.EventBus;
import java.util.ArrayDeque;
public class ChatHandler {
private final EventBus eventBus;
private final ArrayDeque<Message> receivedMessages = new ArrayDeque<>();
/**
* Creates a ChatHandler for a Session
* @param eventBus
*/
public ChatHandler(EventBus eventBus) {
eventBus.subscribe(ChatMessageEvent.class, (event) -> {
processMessageEvent(event.getMessage());
});
this.eventBus = eventBus;
}
/**
* Sends the Chat Message that the server received to the queue
* @param message
*/
private synchronized void processMessageEvent(Message message) {
// TODO: filter for Messages that were sent by the Client
this.receivedMessages.offer(message);
}
/**
* Returns the current size of the queue (Used for Command GET_MESSAGE_COUNT)
*/
public synchronized int getMessageCount() {
return this.receivedMessages.size();
}
/**
* Takes the next message out of the queue, to be sent to the Client
*/
public synchronized Message getNextMessage() {
return this.receivedMessages.poll();
}
/**
* Message is converted into a String and sent to the Client
*/
public void sendMessage(String messageString) {
Message msg = Message.toMessage(messageString);
this.eventBus.publish(new ChatMessageEvent(msg));
}
}
@@ -1,57 +0,0 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.handlers;
import jdk.jshell.spi.ExecutionControl;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class UserHandler {
private final List<String> usernames;
public UserHandler() {
this.usernames = new ArrayList<String>();
}
public Boolean checkUsername(String username) {
for (int i = 0; i < this.usernames.size(); i++) {
if (this.usernames.get(i).equals(username)) {
return false;
}
}
return true;
}
public Pattern usrRex = Pattern.compile("USERNAME=(?<user>\\w+)");
public String addUsername(String username) {
Matcher matcher = usrRex.matcher(username);
if (! matcher.find()) {
throw new RuntimeException("Cannot parse " + username);
}
String usr = matcher.group("user");
if (!this.checkUsername(usr)) {
this.usernames.add(usr);
} else {
this.usernames.add(getAltUsername(usr));
}
return String.format("USERNAME=%s", usr);
}
public void removeUsername(String username) {
this.usernames.remove(username);
}
public String getAltUsername(String username) {
int count = 0;
while (true) {
username = String.format("%s%d", username, ++count);
if (this.checkUsername(username)) {
return username;
}
}
}
}
@@ -1,32 +1,24 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.sessions; package ch.unibas.dmi.dbis.cs108.casono.server.network.sessions;
import java.io.EOFException;
import java.io.IOException;
import java.util.concurrent.atomic.AtomicInteger;
import ch.unibas.dmi.dbis.cs108.casono.server.network.handlers.ChatHandler;
import ch.unibas.dmi.dbis.cs108.casono.server.network.handlers.UserHandler;
import ch.unibas.dmi.dbis.cs108.casono.server.network.transport.RawPacket;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import ch.unibas.dmi.dbis.cs108.casono.server.network.events.DisconnectEvent; import ch.unibas.dmi.dbis.cs108.casono.server.network.events.DisconnectEvent;
import ch.unibas.dmi.dbis.cs108.casono.server.network.events.EventBus; import ch.unibas.dmi.dbis.cs108.casono.server.network.events.EventBus;
import ch.unibas.dmi.dbis.cs108.casono.server.network.parser.PrimitiveRequest;
import ch.unibas.dmi.dbis.cs108.casono.server.network.parser.ProtocolParser;
import ch.unibas.dmi.dbis.cs108.casono.server.network.transport.RawPacket;
import ch.unibas.dmi.dbis.cs108.casono.server.network.transport.TransportLayer; import ch.unibas.dmi.dbis.cs108.casono.server.network.transport.TransportLayer;
import java.io.EOFException;
import java.io.IOException;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/** /** Represents a client session in the network server. */
* Represents a client session in the network server.
*/
public class Session implements Runnable { public class Session implements Runnable {
private final ChatHandler chatHandler;
private final UserHandler userHandler;
private SessionId id; private SessionId id;
private Thread thread; private Thread thread;
private TransportLayer transport; private TransportLayer transport;
private Logger logger; private Logger logger;
private Boolean running; private Boolean running;
private EventBus eventBus; private EventBus eventBus;
private AtomicInteger idGenerator;
/** /**
* Creates a new Session with the given transport and event bus. * Creates a new Session with the given transport and event bus.
@@ -44,9 +36,6 @@ public class Session implements Runnable {
this.logger = LogManager.getLogger(Session.class.toString() + id.value()); this.logger = LogManager.getLogger(Session.class.toString() + id.value());
this.logger.info("Created new session"); this.logger.info("Created new session");
this.idGenerator = new AtomicInteger();
this.chatHandler = new ChatHandler(eventBus);
this.userHandler = new UserHandler();
} }
/** /**
@@ -58,9 +47,7 @@ public class Session implements Runnable {
return this.id; return this.id;
} }
/** /** Starts the session thread. */
* Starts the session thread.
*/
public void start() { public void start() {
thread.start(); thread.start();
} }
@@ -75,53 +62,16 @@ public class Session implements Runnable {
this.running = false; this.running = false;
} }
/** /** Runs the session loop, reading from the transport. */
* Runs the session loop, reading from the transport.
*/
@Override @Override
public void run() { public void run() {
while (running) { while (running) {
try { try {
String clientCommand = transport.read().payload(); RawPacket rawPacket = transport.read();
String[] commandAndArgs = clientCommand.split("\\s+", 2); logger.debug("Recieved: {}", rawPacket);
String commandWord = commandAndArgs[0];
System.out.println("Session "+id.value()+" Received: " + commandAndArgs[0]);
switch (commandWord) {
case "SEND_MESSAGE":
this.chatHandler.sendMessage(commandAndArgs[1]);
writeToTransport("+OK");
writeToTransport("+OK");
break;
case "GET_MESSAGE_COUNT":
int count = chatHandler.getMessageCount();
writeToTransport(""+count);
writeToTransport("+OK");
break;
case "GET_NEXT_MESSAGE":
if (chatHandler.getMessageCount() == 0) {
logger.warn("FAIL No more messages!");
writeToTransport("-ERROR");
} else {
String msgString = chatHandler.getNextMessage().toArgsString();
logger.debug("Session "+id.value()+" Write next message "+msgString);
writeToTransport(msgString);
writeToTransport("+OK");
}
break;
case "LOGIN":
String usr = userHandler.addUsername(commandAndArgs[1]);
writeToTransport(usr);
writeToTransport("+OK");
case "LOGOUT": PrimitiveRequest primitiveRequest = ProtocolParser.parse(rawPacket);
userHandler.removeUsername(commandAndArgs[1]); logger.debug("Parsed request to {}", primitiveRequest);
writeToTransport("+OK");
case "PING":
default:
this.logger.warn("Unknown command: " + commandWord);
writeToTransport("-ERROR");
}
} catch (EOFException e) { } catch (EOFException e) {
logger.info("Client disconnected"); logger.info("Client disconnected");
eventBus.publish(new DisconnectEvent(id)); eventBus.publish(new DisconnectEvent(id));
@@ -132,9 +82,4 @@ public class Session implements Runnable {
} }
} }
} }
private void writeToTransport(String s) throws IOException {
int id = this.idGenerator.incrementAndGet();
this.transport.write(new RawPacket(id, s));
}
} }
@@ -1,7 +1,7 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.transport; package ch.unibas.dmi.dbis.cs108.casono.server.network.transport;
/** /**
* Transport object, created by the transport layer to store raw data about the request * Transport object, created by the transport layer to store raw data about the request prior to any
* prior to any processing * processing
*/ */
public record RawPacket(int requestId, String payload) {} public record RawPacket(int requestId, String payload) {}
@@ -8,8 +8,8 @@ import java.nio.charset.StandardCharsets;
/** Implements TCP-based transport layer for network communication. */ /** Implements TCP-based transport layer for network communication. */
public class TcpTransport implements TransportLayer { public class TcpTransport implements TransportLayer {
private final Socket socket; private Socket socket;
private final DataInputStream in; private DataInputStream in;
private DataOutputStream out; private DataOutputStream out;
/** /**
@@ -49,8 +49,9 @@ public class TcpTransport implements TransportLayer {
public void write(RawPacket data) throws IOException { public void write(RawPacket data) throws IOException {
int requestId = data.requestId(); int requestId = data.requestId();
byte[] rawPayload = data.payload().getBytes(StandardCharsets.UTF_8); byte[] rawPayload = data.payload().getBytes(StandardCharsets.UTF_8);
out.writeInt(rawPayload.length);
out.writeInt(requestId); out.writeInt(requestId);
out.writeInt(rawPayload.length);
out.write(rawPayload); out.write(rawPayload);
out.flush(); out.flush();
} }
@@ -1,65 +0,0 @@
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.Server;
import ch.unibas.dmi.dbis.cs108.casono.server.ServerApp;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.util.List;
import java.util.concurrent.ExecutionException;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class ClientServiceTest {
Server server = null;
private Thread serverThread;
@BeforeEach
public void setUp() {
//server = new Server(5000);
//serverThread = new Thread(()-> server.simpleListenLoop());
//serverThread.start();
ServerApp.startServerCore(5000);
}
@AfterEach
public void tearDown() {
//serverThread.interrupt();
}
@Test
public void testClientService() throws ExecutionException, InterruptedException {
ClientService client1 = new ClientService("localhost", 5000);
/*
ClientService client2 = new ClientService("localhost", 5000);
Thread.sleep(2000);
client1.sendMessage(new Message(Message.MessageType.GLOBAL, 0, "Mathis", null, "blahh"));
Thread.sleep(5000);
List<Message> received = client2.getMessages();
assertEquals(1, received.size());
Message s = received.get(0);
assertEquals(Message.MessageType.GLOBAL, s.getMessageType());
*/
String username = client1.login("Mathis");
assertEquals("USERNAME=Mathis", username);
}
@Test
public void testMessage() {
Message msg = new Message(Message.MessageType.GLOBAL, 0, "Mathis", null, "blah");
Message msg2 = new Message(Message.MessageType.WHISPER, 1, "Mathis", "Julian", "blah");
String serial = msg.toArgsString();
Message roundTripped = Message.toMessage(serial);
String serial2 = msg2.toArgsString();
Message roundTripped2 = Message.toMessage(serial2);
assert(roundTripped.game_id == msg.game_id);
assertEquals(0, roundTripped.game_id);
assert(roundTripped2.game_id == msg2.game_id);
}
}
@@ -1,9 +1,9 @@
package ch.unibas.dmi.dbis.cs108.casono.client.ui.lobbyui; package ch.unibas.dmi.dbis.cs108.casono.client.ui.lobbyui;
import static org.junit.jupiter.api.Assertions.*;
import javafx.scene.layout.GridPane; import javafx.scene.layout.GridPane;
import org.junit.jupiter.api.*; import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
class LobbyButtonGridManagerTest { class LobbyButtonGridManagerTest {
LobbyButtonGridManager gridManager; LobbyButtonGridManager gridManager;
@@ -1,9 +1,9 @@
package ch.unibas.dmi.dbis.cs108.casono.client.ui.lobbyui; package ch.unibas.dmi.dbis.cs108.casono.client.ui.lobbyui;
import static org.junit.jupiter.api.Assertions.*;
import java.util.Map; import java.util.Map;
import org.junit.jupiter.api.*; import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
class LobbyButtonTranslationManagerTest { class LobbyButtonTranslationManagerTest {
LobbyButtonTranslationManager manager; LobbyButtonTranslationManager manager;
@@ -1,10 +1,11 @@
package ch.unibas.dmi.dbis.cs108.casono.server.tokenizer; package ch.unibas.dmi.dbis.cs108.casono.server.tokenizer;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.Test;
class StateTest { class StateTest {
@Test @Test
void testConstructor() { void testConstructor() {
@@ -1,8 +1,9 @@
package ch.unibas.dmi.dbis.cs108.casono.server.tokenizer; package ch.unibas.dmi.dbis.cs108.casono.server.tokenizer;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;
class TokenizerExceptionTest { class TokenizerExceptionTest {
@Test @Test
void testConstructorAndGetters() { void testConstructorAndGetters() {