Merge remote-tracking branch 'origin/main'

This commit is contained in:
Mathis Ginkel
2026-04-02 10:14:23 +02:00
206 changed files with 683 additions and 251 deletions
+18 -7
View File
@@ -6,11 +6,13 @@ stages:
# Reusable definitions # Reusable definitions
.gradle-cache: &gradle-cache .gradle-cache: &gradle-cache
variables:
GRADLE_USER_HOME: '$CI_PROJECT_DIR/.gradle-home'
cache: cache:
key: '$CI_PROJECT_ID-gradle' key: '$CI_PROJECT_ID-gradle'
paths: paths:
- .gradle/ - .gradle/
- ~/.gradle/caches/ - .gradle-home/
.on-commit: &on-commit .on-commit: &on-commit
rules: rules:
@@ -22,7 +24,7 @@ stages:
.on-mr: &on-mr .on-mr: &on-mr
rules: rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"' - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
allow_failure: true # deactivate after all issues (shown by checkstyle) have been resolved allow_failure: false
# Jobs # Jobs
checkstyle: checkstyle:
@@ -30,7 +32,7 @@ checkstyle:
stage: lint stage: lint
image: gradle:9.3.1-jdk25 image: gradle:9.3.1-jdk25
script: script:
- gradle checkstyleMain checkstyleTest - gradle checkstyleMain checkstyleTest --configuration-cache --configuration-cache-problems=warn
allow_failure: true allow_failure: true
artifacts: artifacts:
when: always when: always
@@ -47,8 +49,8 @@ checkstyle-mr:
stage: lint stage: lint
image: gradle:9.3.1-jdk25 image: gradle:9.3.1-jdk25
script: script:
- gradle checkstyleMain checkstyleTest - gradle checkstyleMain checkstyleTest --configuration-cache --configuration-cache-problems=warn
allow_failure: true allow_failure: false
artifacts: artifacts:
when: always when: always
paths: paths:
@@ -66,6 +68,7 @@ checkstyle-report:
- job: checkstyle-mr - job: checkstyle-mr
artifacts: true artifacts: true
optional: true optional: true
when: always
rules: rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"' - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
script: script:
@@ -81,7 +84,7 @@ compile-check:
stage: build stage: build
image: gradle:9.3.1-jdk25 image: gradle:9.3.1-jdk25
script: script:
- gradle compileTestJava - gradle compileTestJava --configuration-cache --configuration-cache-problems=warn
needs: [] needs: []
rules: rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"' - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
@@ -89,12 +92,20 @@ compile-check:
- if: '$CI_COMMIT_BRANCH' - if: '$CI_COMMIT_BRANCH'
allow_failure: false allow_failure: false
javadoc-check:
<<: [*gradle-cache, *on-mr]
stage: build
image: gradle:9.3.1-jdk25
script:
- gradle javaDoc --configuration-cache --configuration-cache-problems=warn
needs: []
test: test:
<<: *gradle-cache <<: *gradle-cache
stage: test stage: test
image: gradle:9.3.1-jdk25 image: gradle:9.3.1-jdk25
script: script:
- gradle test - gradle test --configuration-cache --configuration-cache-problems=warn
artifacts: artifacts:
when: always when: always
reports: reports:
+1 -1
View File
@@ -34,7 +34,7 @@
### Checklist ### Checklist
- [ ] I reproduced the problem using the steps above - [ ] I reproduced the problem using the steps above
- [ ] I searched documentation docs for relevant information - [ ] I searched documentation for relevant information
- [ ] I added relevant labels - [ ] I added relevant labels
/label ~bug /label ~bug
+2 -2
View File
@@ -11,8 +11,8 @@
<!-- Detailed description of the desired behavior --> <!-- Detailed description of the desired behavior -->
### Checklist ### Checklist
- [ ] I reproduced the problem using the steps above - [ ] I have described the function in detail
- [ ] I searched documentation docs for relevant information - [ ] I searched docs for alternative implementations matching my needs
- [ ] I added relevant labels - [ ] I added relevant labels
/label ~enhancement /label ~enhancement
+14
View File
@@ -108,3 +108,17 @@ tasks.register('fatJar', Jar) {
configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) }
}) })
} }
tasks.register('javadocJar', Jar) {
group = 'build'
description = 'Assembles a Javadoc JAR.'
dependsOn tasks.named('javadoc')
archiveClassifier = 'javadoc'
from(tasks.javadoc.destinationDir)
}
tasks.register('build-cs108') {
group = 'build'
description = 'Produces executable JAR and Javadoc JAR for CS108.'
dependsOn tasks.named('fatJar'), tasks.named('javadocJar')
}
@@ -10,15 +10,15 @@ import org.apache.logging.log4j.Logger;
/** /**
* Entry point for the Casono client application. Handles client startup and connection parameters. * Entry point for the Casono client application. Handles client startup and connection parameters.
* *
* <p>Standardkonstruktor für die Anwendung. * <p>Default constructor for the application.
*/ */
public class ClientApp { public class ClientApp {
private static final Logger LOGGER = LogManager.getLogger(ClientApp.class); private static final Logger LOGGER = LogManager.getLogger(ClientApp.class);
/** Standardkonstruktor. */ /** Default constructor. */
public ClientApp() { public ClientApp() {
// Standardkonstruktor // Default constructor
} }
/** /**
@@ -6,13 +6,13 @@ import javafx.application.Application;
/** /**
* Launcher for the Casono main UI. * Launcher for the Casono main UI.
* *
* <p>Standardkonstruktor für die Anwendung. * <p>Default constructor for the application.
*/ */
public class Launcher { public class Launcher {
/** Standardkonstruktor. */ /** Default constructor. */
public Launcher() { public Launcher() {
// Standardkonstruktor // Default constructor
} }
/** /**
@@ -41,6 +41,11 @@ public class ChatController {
chatScrollPane.vvalueProperty().bind(chatVBox.heightProperty()); chatScrollPane.vvalueProperty().bind(chatVBox.heightProperty());
} }
/** Standardkonstruktor. Wird von FXML verwendet. */
public ChatController() {
// default constructor for FXML
}
/** /**
* Diese Methode wird vom Senden-Button oder Enter ausgelöst. Sie gibt die eigene Nachricht an * Diese Methode wird vom Senden-Button oder Enter ausgelöst. Sie gibt die eigene Nachricht an
* das Netzwerkprotokoll weiter. * das Netzwerkprotokoll weiter.
@@ -15,11 +15,22 @@ import javafx.scene.layout.VBox;
*/ */
public class CasinoGameController { public class CasinoGameController {
/** Standardkonstruktor. Wird von FXML verwendet. */
public CasinoGameController() {
// default constructor for FXML
}
@FXML private Label welcomeText; @FXML private Label welcomeText;
@FXML private VBox casinoTable; @FXML private VBox casinoTable;
// TODO: Test-Logik: wird durch echte Spielinteraktionen ersetzt, // TODO: Test-Logik: wird durch echte Spielinteraktionen ersetzt,
// sobald die GameEngine fertig ist // sobald die GameEngine fertig ist
/**
* Temporäre Test-Methode, die bei Klick auf den Tisch eine Platzhalteraktion ausführt.
*
* <p>Wird in der finalen Implementierung durch die Spiel-Logik ersetzt.
*/
@FXML @FXML
public void onTableClick() { public void onTableClick() {
welcomeText.setText("Einsatz akzeptiert!"); welcomeText.setText("Einsatz akzeptiert!");
@@ -17,6 +17,11 @@ import javafx.stage.Stage;
*/ */
public class CasinoGameUI extends Application { public class CasinoGameUI extends Application {
/** Standardkonstruktor. */
public CasinoGameUI() {
// default no-arg constructor
}
private static final int DEFAULT_WIDTH = 1200; private static final int DEFAULT_WIDTH = 1200;
private static final int DEFAULT_HEIGHT = 800; private static final int DEFAULT_HEIGHT = 800;
@@ -29,7 +34,7 @@ public class CasinoGameUI extends Application {
@Override @Override
public void start(Stage stage) throws IOException { public void start(Stage stage) throws IOException {
FXMLLoader fxmlLoader = FXMLLoader fxmlLoader =
new FXMLLoader(CasinoGameUI.class.getResource("/ui-structure/casinogameui.fxml")); new FXMLLoader(CasinoGameUI.class.getResource("/ui-structure/Casinogameui.fxml"));
Scene scene = new Scene(fxmlLoader.load(), DEFAULT_WIDTH, DEFAULT_HEIGHT); Scene scene = new Scene(fxmlLoader.load(), DEFAULT_WIDTH, DEFAULT_HEIGHT);
stage.setTitle("Casono (GAME)"); stage.setTitle("Casono (GAME)");
@@ -27,6 +27,8 @@ import javafx.scene.shape.Rectangle;
import javafx.scene.web.WebEngine; import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView; import javafx.scene.web.WebView;
import javafx.stage.Stage; import javafx.stage.Stage;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/** /**
* Experimenteller integrierter Browser für Casono. * Experimenteller integrierter Browser für Casono.
@@ -50,13 +52,17 @@ import javafx.stage.Stage;
*/ */
public class CasinoBrowserController { public class CasinoBrowserController {
/** Standardkonstruktor. Initialisiert den CasinoBrowserController. */
public CasinoBrowserController() {
// Intentionally left blank; controller initialization is FXML-driven.
}
private static final Set<String> TRUSTED_DOMAINS = new HashSet<>(); private static final Set<String> TRUSTED_DOMAINS = new HashSet<>();
private static final CookieManager COOKIE_MANAGER = private static final CookieManager COOKIE_MANAGER =
new CookieManager(null, CookiePolicy.ACCEPT_ORIGINAL_SERVER); new CookieManager(null, CookiePolicy.ACCEPT_ORIGINAL_SERVER);
private static final org.apache.logging.log4j.Logger LOGGER = private static final Logger LOGGER = LogManager.getLogger(CasinoBrowserController.class);
org.apache.logging.log4j.LogManager.getLogger(CasinoBrowserController.class);
private static final int LOGO_HEIGHT = 40; private static final int LOGO_HEIGHT = 40;
private static final int CORNER_RADIUS = 40; private static final int CORNER_RADIUS = 40;
@@ -354,8 +360,9 @@ public class CasinoBrowserController {
fwdBtn.getStyleClass().add("gray-button"); fwdBtn.getStyleClass().add("gray-button");
fwdBtn.setOnAction( fwdBtn.setOnAction(
e -> { e -> {
if (engine.getHistory().getCurrentIndex() int currentIndex = engine.getHistory().getCurrentIndex();
< engine.getHistory().getEntries().size() - 1) { int lastIndex = engine.getHistory().getEntries().size() - 1;
if (currentIndex < lastIndex) {
engine.getHistory().go(1); engine.getHistory().go(1);
} }
}); });
@@ -423,7 +430,7 @@ public class CasinoBrowserController {
Scene scene = new Scene(root, WINDOW_WIDTH, WINDOW_HEIGHT); Scene scene = new Scene(root, WINDOW_WIDTH, WINDOW_HEIGHT);
var css = CasinoBrowserController.class.getResource("/ui-structure/casinogameui.css"); var css = CasinoBrowserController.class.getResource("/ui-structure/Casinogameui.css");
if (css != null) { if (css != null) {
scene.getStylesheets().add(css.toExternalForm()); scene.getStylesheets().add(css.toExternalForm());
@@ -512,10 +519,11 @@ public class CasinoBrowserController {
alert.setTitle("Unbekannte Website"); alert.setTitle("Unbekannte Website");
alert.setHeaderText("Diese Website ist nicht bekannt"); alert.setHeaderText("Diese Website ist nicht bekannt");
alert.setContentText( String content =
host host
+ "\n\nDiese Seite ist nicht vom " + "\n\nDiese Seite ist nicht vom Casono Browser verifiziert.\n"
+ "Casono Browser verifiziert.\nMöchten Sie sie trotzdem öffnen?"); + "Möchten Sie sie trotzdem öffnen?";
alert.setContentText(content);
var stream = CasinoBrowserController.class.getResourceAsStream(LOGO_PATH); var stream = CasinoBrowserController.class.getResourceAsStream(LOGO_PATH);
Image logo = new Image(stream); Image logo = new Image(stream);
@@ -1,12 +1,14 @@
package ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.gameuicomponents; package ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.gameuicomponents;
import javafx.application.Platform; import ch.unibas.dmi.dbis.cs108.casono.client.ui.lobbyui.Casinomainui;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.scene.control.TextField; import javafx.scene.control.TextField;
import javafx.scene.input.KeyCode; import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent; import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseEvent; import javafx.scene.input.MouseEvent;
import javafx.scene.layout.HBox; import javafx.scene.layout.HBox;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/** /**
* Controller für die interaktive Taskleiste innerhalb der Poker-UI. * Controller für die interaktive Taskleiste innerhalb der Poker-UI.
@@ -16,8 +18,12 @@ import javafx.scene.layout.HBox;
*/ */
public class TaskbarController { public class TaskbarController {
private static final org.apache.logging.log4j.Logger LOGGER = /** Standardkonstruktor. Wird von FXML verwendet. */
org.apache.logging.log4j.LogManager.getLogger(CasinoBrowserController.class); public TaskbarController() {
// default constructor for FXML
}
private static final Logger LOGGER = LogManager.getLogger(CasinoBrowserController.class);
@FXML private HBox taskbar; @FXML private HBox taskbar;
@FXML private TextField taskbarInput; @FXML private TextField taskbarInput;
@@ -92,15 +98,21 @@ public class TaskbarController {
processBet(); processBet();
} }
/**
* Wird aufgerufen, wenn der Exit-Button in der Taskleiste gedrückt wird.
*
* <p>TODO: Logik implementieren, um zur Lobby zurückzukehren, ohne die gesamte Anwendung zu
* schließen (kein System.exit/Platform.exit).
*/
@FXML @FXML
private void onExitButtonClick() { private void onExitButtonClick() {
Platform.exit(); javafx.application.Platform.runLater(
() -> {
// Close game stage
javafx.stage.Stage currentStage =
(javafx.stage.Stage) taskbar.getScene().getWindow();
currentStage.close();
// Lobby-UI starten
try {
new Casinomainui().start(new javafx.stage.Stage());
} catch (Exception e) {
LOGGER.error("Fehler beim Starten der Lobby-UI: {}", e.getMessage());
}
});
} }
/** /**
@@ -10,13 +10,13 @@ import javafx.stage.Stage;
/** /**
* JavaFX Application class for the Casono main UI. * JavaFX Application class for the Casono main UI.
* *
* <p>Standardkonstruktor für die Anwendung. * <p>Default constructor for the application.
*/ */
public class Casinomainui extends Application { public class Casinomainui extends Application {
/** Standardkonstruktor. */ /** Default constructor. */
public Casinomainui() { public Casinomainui() {
// Standardkonstruktor // Default constructor
} }
private static final int SCENE_WIDTH = 1200; private static final int SCENE_WIDTH = 1200;
@@ -28,6 +28,7 @@ public class CasinomainuiController {
private LobbyButtonGridManager gridManager; private LobbyButtonGridManager gridManager;
private int nextButtonId = 1; private int nextButtonId = 1;
/** Default constructor for dependency injection by FXMLLoader. */
public CasinomainuiController() { public CasinomainuiController() {
// Default constructor // Default constructor
} }
@@ -39,7 +40,7 @@ public class CasinomainuiController {
subtitleLabel.setText("Texas Hold'em Poker"); subtitleLabel.setText("Texas Hold'em Poker");
logoView.setImage(new Image(getClass().getResource("/images/logo.png").toExternalForm())); logoView.setImage(new Image(getClass().getResource("/images/logo.png").toExternalForm()));
translationManager = new LobbyButtonTranslationManager(); translationManager = LobbyButtonTranslationManager.getInstance();
gridManager = gridManager =
new LobbyButtonGridManager(new javafx.scene.layout.GridPane(), translationManager); new LobbyButtonGridManager(new javafx.scene.layout.GridPane(), translationManager);
casinoTable.getChildren().clear(); casinoTable.getChildren().clear();
@@ -57,7 +58,7 @@ public class CasinomainuiController {
@FXML @FXML
public void handleCreateLobbyButton() { public void handleCreateLobbyButton() {
if (translationManager.isFull()) { if (translationManager.isFull()) {
LOGGER.warn("Grid voll! Keine weiteren Lobbys moeglich."); LOGGER.warn("Grid is full! No more lobbies available.");
return; return;
} }
int buttonId = nextButtonId++; int buttonId = nextButtonId++;
@@ -67,7 +68,7 @@ public class CasinomainuiController {
LOGGER.info("ButtonID: {}, LobbyID: {}", buttonId, lobbyId); LOGGER.info("ButtonID: {}, LobbyID: {}", buttonId, lobbyId);
gridManager.renderLobbyButtons(); gridManager.renderLobbyButtons();
} catch (Exception e) { } catch (Exception e) {
LOGGER.error("Fehler beim Hinzufügen: {}", e.getMessage()); LOGGER.error("Error while adding lobby button: {}", e.getMessage());
} }
} }
} }
@@ -17,6 +17,8 @@ import org.apache.logging.log4j.Logger;
* ButtonID to LobbyID. * ButtonID to LobbyID.
*/ */
public class LobbyButtonGridManager { public class LobbyButtonGridManager {
private static final double BUTTON_WIDTH_MARGIN = 20.0;
private static final double BUTTON_MIN_SIZE = 10.0;
private static final Logger LOGGER = LogManager.getLogger(LobbyButtonGridManager.class); private static final Logger LOGGER = LogManager.getLogger(LobbyButtonGridManager.class);
/** GridPane for the button grid. */ /** GridPane for the button grid. */
@@ -46,7 +48,8 @@ public class LobbyButtonGridManager {
public LobbyButtonGridManager( public LobbyButtonGridManager(
GridPane gridPane, LobbyButtonTranslationManager translationManager) { GridPane gridPane, LobbyButtonTranslationManager translationManager) {
this.gridPane = gridPane; this.gridPane = gridPane;
this.translationManager = translationManager; // Singleton immer verwenden
this.translationManager = LobbyButtonTranslationManager.getInstance();
} }
/** /**
@@ -65,8 +68,21 @@ public class LobbyButtonGridManager {
int buttonId = entry.getKey(); int buttonId = entry.getKey();
Button btn = new Button(); Button btn = new Button();
btn.setId("lobbyBtn-" + buttonId); btn.setId("lobbyBtn-" + buttonId);
btn.setGraphic( ImageView imageView =
new ImageView(new Image(getClass().getResourceAsStream(BUTTON_IMAGE_PATH)))); new ImageView(new Image(getClass().getResourceAsStream(BUTTON_IMAGE_PATH)));
imageView.setPreserveRatio(true);
// Dynamische Breite: Bindung an die Zellengröße
imageView
.fitWidthProperty()
.bind(gridPane.widthProperty().divide(COLS).subtract(BUTTON_WIDTH_MARGIN));
imageView.setSmooth(true);
btn.setGraphic(imageView);
btn.setMaxWidth(Double.MAX_VALUE);
btn.setMaxHeight(Double.MAX_VALUE);
btn.setMinWidth(BUTTON_MIN_SIZE);
btn.setMinHeight(BUTTON_MIN_SIZE);
GridPane.setHgrow(btn, javafx.scene.layout.Priority.ALWAYS);
GridPane.setVgrow(btn, javafx.scene.layout.Priority.ALWAYS);
btn.setOnAction( btn.setOnAction(
e -> { e -> {
Integer lobbyId = translationManager.getLobbyIdForButton(buttonId); Integer lobbyId = translationManager.getLobbyIdForButton(buttonId);
@@ -99,8 +115,22 @@ public class LobbyButtonGridManager {
* @param lobbyId The lobbyId to join * @param lobbyId The lobbyId to join
*/ */
public void joinLobby(int lobbyId) { public void joinLobby(int lobbyId) {
// TODO: Replace with actual join logic // Game-UI starten und Lobby-UI schließen
LOGGER.info("Joining lobby: {}", lobbyId); LOGGER.info("Joining lobby: {}", lobbyId);
javafx.application.Platform.runLater(
() -> {
// Lobby-Stage schließen
javafx.stage.Stage currentStage =
(javafx.stage.Stage) gridPane.getScene().getWindow();
currentStage.close();
// Game-UI starten
try {
new ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.CasinoGameUI()
.start(new javafx.stage.Stage());
} catch (Exception e) {
LOGGER.error("Fehler beim Starten der Game-UI: {}", e.getMessage());
}
});
} }
/** /**
@@ -4,36 +4,53 @@ import java.util.HashMap;
import java.util.Map; import java.util.Map;
/** /**
* Verwaltet das Mapping zwischen Button-IDs und Lobby-IDs rein im Speicher. Keine Dateioperationen, * Manages the mapping between Button IDs and Lobby IDs in memory only. No file operations,
* nur Laufzeitdatenstruktur. * runtime-only data structure.
*/ */
public class LobbyButtonTranslationManager { public class LobbyButtonTranslationManager {
/** Maximale Anzahl an Buttons/Lobbys */
// Singleton instance
private static LobbyButtonTranslationManager instance;
// Singleton access
/**
* Returns the singleton instance of the manager.
*
* @return the single instance of {@code LobbyButtonTranslationManager}
*/
public static LobbyButtonTranslationManager getInstance() {
if (instance == null) {
instance = new LobbyButtonTranslationManager();
}
return instance;
}
/** Maximum number of buttons/lobbies */
private static final int MAX_BUTTONS = 8; private static final int MAX_BUTTONS = 8;
/** Zuordnung ButtonID → LobbyID */ /** Mapping ButtonID → LobbyID */
private final Map<Integer, Integer> buttonIdToLobbyId = new HashMap<>(); private final Map<Integer, Integer> buttonIdToLobbyId = new HashMap<>();
/** Konstruktor: initialisiert die Zuordnung leer. */ /** Private constructor for the singleton pattern */
public LobbyButtonTranslationManager() { private LobbyButtonTranslationManager() {
// Zuordnung bleibt leer beim Start // Mapping is empty at startup
} }
/** /**
* Prüft, ob das Grid voll ist (MAX_BUTTONS erreicht). * Checks whether the grid is full (MAX_BUTTONS reached).
* *
* @return true, wenn Grid voll; sonst false * @return true if the grid is full; otherwise false
*/ */
public boolean isFull() { public boolean isFull() {
return buttonIdToLobbyId.size() >= MAX_BUTTONS; return buttonIdToLobbyId.size() >= MAX_BUTTONS;
} }
/** /**
* Fügt eine Zuordnung ButtonID → LobbyID hinzu. * Adds a mapping ButtonID → LobbyID.
* *
* @param buttonId Die ID des Buttons * @param buttonId the ID of the button
* @param lobbyId Die ID der Lobby * @param lobbyId the ID of the lobby
* @throws Exception wenn das Grid voll ist * @throws Exception when the grid is full
*/ */
public void addLobbyButton(int buttonId, int lobbyId) throws Exception { public void addLobbyButton(int buttonId, int lobbyId) throws Exception {
if (isFull()) { if (isFull()) {
@@ -43,28 +60,28 @@ public class LobbyButtonTranslationManager {
} }
/** /**
* Entfernt eine Zuordnung für die gegebene ButtonID. * Removes the mapping for the given ButtonID.
* *
* @param buttonId Die ID des zu entfernenden Buttons * @param buttonId the ID of the button to remove
*/ */
public void removeLobbyButton(int buttonId) { public void removeLobbyButton(int buttonId) {
buttonIdToLobbyId.remove(buttonId); buttonIdToLobbyId.remove(buttonId);
} }
/** /**
* Gibt die LobbyID für eine gegebene ButtonID zurück. * Returns the LobbyID for a given ButtonID.
* *
* @param buttonId Die ButtonID * @param buttonId the ButtonID
* @return Die zugehoerige LobbyID oder null, falls nicht vorhanden * @return the associated LobbyID or null if not present
*/ */
public Integer getLobbyIdForButton(int buttonId) { public Integer getLobbyIdForButton(int buttonId) {
return buttonIdToLobbyId.get(buttonId); return buttonIdToLobbyId.get(buttonId);
} }
/** /**
* Gibt die gesamte Zuordnung ButtonID → LobbyID zurück. * Returns the full mapping ButtonID → LobbyID.
* *
* @return Map aller Zuordnungen * @return Map of all mappings
*/ */
public Map<Integer, Integer> getButtonIdToLobbyId() { public Map<Integer, Integer> getButtonIdToLobbyId() {
return buttonIdToLobbyId; return buttonIdToLobbyId;
@@ -3,10 +3,11 @@ package ch.unibas.dmi.dbis.cs108.casono.server;
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;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.CommandRouter; import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandRouter;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.CommandParserDispatcher;
import ch.unibas.dmi.dbis.cs108.casono.server.network.events.DisconnectEvent; import ch.unibas.dmi.dbis.cs108.casono.server.network.events.DisconnectEvent;
import ch.unibas.dmi.dbis.cs108.casono.server.network.events.EventBus; import ch.unibas.dmi.dbis.cs108.casono.server.network.events.EventBus;
import ch.unibas.dmi.dbis.cs108.casono.server.network.parser.CommandParserDispatcher; import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionDisconnectJob;
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionManager; import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionManager;
import java.time.Duration; import java.time.Duration;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
@@ -17,9 +18,12 @@ import org.apache.logging.log4j.Logger;
/** Application class for starting the server. */ /** Application class for starting the server. */
public class ServerApp { public class ServerApp {
public static final int USER_CLEANUP_JOB_DELAY = 0; private static final int USER_CLEANUP_JOB_DELAY = 0;
public static final int USER_CLEANUP_JOB_PERIOD = 10; private static final int USER_CLEANUP_JOB_PERIOD = 10;
public static final int USER_CLEANUP_JOB_RECONNECT_THRESHOLD = 10; private static final int USER_CLEANUP_JOB_RECONNECT_THRESHOLD = 10;
private static final int SESSION_DISCONNECT_JOB_DELAY = 0;
private static final int SESSION_DISCONNECT_JOB_PERIOD = 2;
private static final int SESSION_DISCONNECT_JOB_TIMEOUT = 5;
public static void start(String arg) { public static void start(String arg) {
int port = Integer.parseInt(arg); int port = Integer.parseInt(arg);
@@ -45,6 +49,14 @@ public class ServerApp {
USER_CLEANUP_JOB_DELAY, USER_CLEANUP_JOB_DELAY,
USER_CLEANUP_JOB_PERIOD, USER_CLEANUP_JOB_PERIOD,
TimeUnit.SECONDS); TimeUnit.SECONDS);
scheduler.scheduleAtFixedRate(
new SessionDisconnectJob(
sessionManager,
eventBus,
Duration.ofSeconds(SESSION_DISCONNECT_JOB_TIMEOUT)),
SESSION_DISCONNECT_JOB_DELAY,
SESSION_DISCONNECT_JOB_PERIOD,
TimeUnit.SECONDS);
networkManager.start(); networkManager.start();
} }
@@ -58,7 +58,6 @@ public class UserRegistry {
* *
* @param sessionId the session ID of the disconnected client * @param sessionId the session ID of the disconnected client
*/ */
// TODO: Add to EventRegistry with DisconnectEvent
public synchronized void onDisconnect(SessionId sessionId) { public synchronized void onDisconnect(SessionId sessionId) {
User user = bySessionId.remove(sessionId); User user = bySessionId.remove(sessionId);
if (user == null) { if (user == null) {
@@ -1,7 +0,0 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.command;
import ch.unibas.dmi.dbis.cs108.casono.server.network.parser.Request;
public interface CommandHandler<T extends Request> {
void execute(T request);
}
@@ -0,0 +1,7 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.Request;
public interface CommandHandler<T extends Request> {
void execute(T request);
}
@@ -1,6 +1,6 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.command; package ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution;
import ch.unibas.dmi.dbis.cs108.casono.server.network.parser.Request; import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.Request;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
@@ -1,4 +1,4 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.command; package ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution;
public class UnknownRequestException extends RuntimeException { public class UnknownRequestException extends RuntimeException {
private final String requestName; private final String requestName;
@@ -0,0 +1,18 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.PrimitiveRequest;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.Request;
/**
* Parser to convert the PrimitiveRequest to a Request and performing checks for required fields and
* data types
*/
public interface CommandParser<T extends Request> {
/**
* Parses the provided PrimitiveRequest into a command-specific request
*
* @param primitiveRequest
* @return
*/
T parse(PrimitiveRequest primitiveRequest);
}
@@ -1,5 +1,7 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.parser; package ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.PrimitiveRequest;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.Request;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
@@ -0,0 +1,4 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing;
/** Used in the PrimitiveRequest class to store the key of a parameter with its respective value */
public record RequestParameter(String key, String value) {}
@@ -1,4 +1,4 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.parser; package ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing;
/** /**
* Exception thrown when the CommandParserDispatcher has no registered handler for the provided * Exception thrown when the CommandParserDispatcher has no registered handler for the provided
@@ -1,15 +0,0 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.parser;
/**
* Parser to convert the PrimitiveRequest to a Request and performing checks for required fields and
* data types
*/
public interface CommandParser {
/**
* Parses the provided PrimitiveRequest into a command-specific request
*
* @param primitiveRequest
* @return
*/
Request parse(PrimitiveRequest primitiveRequest);
}
@@ -1,4 +0,0 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.parser;
/** Used in the PrimitiveRequest class to store the key of a parameter with its respective value */
public record Parameter(String key, String value) {}
@@ -1,7 +0,0 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.parser;
import java.util.List;
/** Created by the ProtocolParser to allow easy access to the request contents */
public record PrimitiveRequest(
RequestContext context, String command, List<Parameter> parameters) {}
@@ -1,5 +0,0 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.parser;
import java.util.List;
public record RawRequest(String command, List<Parameter> parameters) {}
@@ -1,10 +1,12 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.parser; package ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.parser;
import ch.unibas.dmi.dbis.cs108.casono.server.tokenizer.RawToken; import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.RequestParameter;
import ch.unibas.dmi.dbis.cs108.casono.server.tokenizer.Token; import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.parser.tokenizer.RawToken;
import ch.unibas.dmi.dbis.cs108.casono.server.tokenizer.TokenClassifier; import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.parser.tokenizer.Token;
import ch.unibas.dmi.dbis.cs108.casono.server.tokenizer.TokenType; import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.parser.tokenizer.TokenClassifier;
import ch.unibas.dmi.dbis.cs108.casono.server.tokenizer.Tokenizer; import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.parser.tokenizer.TokenType;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.parser.tokenizer.Tokenizer;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RawRequest;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
@@ -15,7 +17,7 @@ public class ProtocolParser {
/** /**
* Parses the payload of the provided RawPacket * Parses the payload of the provided RawPacket
* *
* @param packet the RawPacket containing the recieved data * @param payload the payload to parse and check for syntax
* @return created PrimitiveRequest * @return created PrimitiveRequest
*/ */
public static RawRequest parse(String payload) { public static RawRequest parse(String payload) {
@@ -24,7 +26,7 @@ public class ProtocolParser {
Iterator<Token> iterator = tokens.iterator(); Iterator<Token> iterator = tokens.iterator();
String command = readCommand(iterator); String command = readCommand(iterator);
List<Parameter> parameters = readParameters(iterator); List<RequestParameter> parameters = readParameters(iterator);
return new RawRequest(command, parameters); return new RawRequest(command, parameters);
} }
@@ -50,8 +52,8 @@ public class ProtocolParser {
* @param iterator * @param iterator
* @return list containing all parsed parameters * @return list containing all parsed parameters
*/ */
private static List<Parameter> readParameters(Iterator<Token> iterator) { private static List<RequestParameter> readParameters(Iterator<Token> iterator) {
List<Parameter> parameters = new ArrayList<>(); List<RequestParameter> parameters = new ArrayList<>();
try { try {
while (iterator.hasNext()) { while (iterator.hasNext()) {
@@ -65,7 +67,7 @@ public class ProtocolParser {
readSeperator(iterator.next()); readSeperator(iterator.next());
String value = readValue(iterator.next()); String value = readValue(iterator.next());
parameters.add(new Parameter(key, value)); parameters.add(new RequestParameter(key, value));
} }
} catch (NoSuchElementException e) { } catch (NoSuchElementException e) {
throw new ProtocolParserException("Ran out of tokens while reading parameter"); throw new ProtocolParserException("Ran out of tokens while reading parameter");
@@ -1,4 +1,4 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.parser; package ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.parser;
public class ProtocolParserException extends RuntimeException { public class ProtocolParserException extends RuntimeException {
public ProtocolParserException(String message) { public ProtocolParserException(String message) {
@@ -1,4 +1,4 @@
package ch.unibas.dmi.dbis.cs108.casono.server.tokenizer; package ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.parser.tokenizer;
/** Represents a raw (unclassified) token in the tokenizer. */ /** Represents a raw (unclassified) token in the tokenizer. */
public record RawToken(RawTokenType type, String value, int line, int column) {} public record RawToken(RawTokenType type, String value, int line, int column) {}
@@ -1,4 +1,4 @@
package ch.unibas.dmi.dbis.cs108.casono.server.tokenizer; package ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.parser.tokenizer;
public enum RawTokenType { public enum RawTokenType {
WORD, WORD,
@@ -1,4 +1,4 @@
package ch.unibas.dmi.dbis.cs108.casono.server.tokenizer; package ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.parser.tokenizer;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -1,4 +1,4 @@
package ch.unibas.dmi.dbis.cs108.casono.server.tokenizer; package ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.parser.tokenizer;
/** Represents a token in the tokenizer. */ /** Represents a token in the tokenizer. */
public record Token(TokenType type, String value, int line, int column) {} public record Token(TokenType type, String value, int line, int column) {}
@@ -1,4 +1,4 @@
package ch.unibas.dmi.dbis.cs108.casono.server.tokenizer; package ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.parser.tokenizer;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -1,4 +1,4 @@
package ch.unibas.dmi.dbis.cs108.casono.server.tokenizer; package ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.parser.tokenizer;
/** Enumeration of token types used in the tokenizer. */ /** Enumeration of token types used in the tokenizer. */
public enum TokenType { public enum TokenType {
@@ -1,4 +1,4 @@
package ch.unibas.dmi.dbis.cs108.casono.server.tokenizer; package ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.parser.tokenizer;
import java.util.List; import java.util.List;
@@ -1,4 +1,4 @@
package ch.unibas.dmi.dbis.cs108.casono.server.tokenizer; package ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.parser.tokenizer;
/** Exception thrown during tokenization. */ /** Exception thrown during tokenization. */
public class TokenizerException extends RuntimeException { public class TokenizerException extends RuntimeException {
@@ -0,0 +1,8 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.RequestParameter;
import java.util.List;
/** Created by the ProtocolParser to allow easy access to the request contents */
public record PrimitiveRequest(
RequestContext context, String command, List<RequestParameter> parameters) {}
@@ -0,0 +1,6 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.RequestParameter;
import java.util.List;
public record RawRequest(String command, List<RequestParameter> parameters) {}
@@ -1,4 +1,4 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.parser; package ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request;
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionId; import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionId;
@@ -1,4 +1,4 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.parser; package ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request;
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionId; import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionId;
@@ -0,0 +1,26 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.accessor;
/** Exception thrown when a required parameter key is not found. */
public class MissingParameterException extends RuntimeException {
private final String parameterKey;
/**
* Creates a new exception for a missing required parameter.
*
* @param message human-readable description of the missing parameter
* @param parameterKey key of the parameter that could not be found
*/
public MissingParameterException(String message, String parameterKey) {
super(message);
this.parameterKey = parameterKey;
}
/**
* Returns the missing parameter key.
*
* @return key of the parameter that could not be found
*/
public String getParameterKey() {
return parameterKey;
}
}
@@ -0,0 +1,27 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.accessor;
/** Exception thrown when a parameter value cannot be converted to the requested type. */
public class ParameterParseException extends RuntimeException {
private final String parameterKey;
/**
* Creates a new parse exception with a root cause.
*
* @param message human-readable description of the parsing failure
* @param parameterKey key for whose value the error occured
* @param cause original exception thrown during parsing
*/
public ParameterParseException(String message, String parameterKey, Throwable cause) {
super(message, cause);
this.parameterKey = parameterKey;
}
/**
* Returns the missing parameter key.
*
* @return key for whose value the error occured
*/
public String getParameterKey() {
return parameterKey;
}
}
@@ -0,0 +1,116 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.accessor;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.RequestParameter;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* Provides typed access to a request's parameters by indexing them by key.
*
* <p>Supports required and optional lookups, with optional conversion from {@link String} values to
* domain-specific types via parser functions.
*/
public class RequestParameterAccessor {
private final Map<String, String> index;
/**
* Creates an accessor
*
* @param parameters to use
*/
public RequestParameterAccessor(List<RequestParameter> parameters) {
this.index =
parameters.stream()
.collect(
Collectors.toUnmodifiableMap(
RequestParameter::key, RequestParameter::value));
}
/**
* Returns the raw value for a required parameter key.
*
* @param key parameter key to look up
* @return raw parameter value
* @throws MissingParameterException if no parameter with the given key exists
*/
public String require(String key) throws MissingParameterException {
String value = index.get(key);
if (value == null) {
throw new MissingParameterException(
"Required parameter with key '" + key + "' is missing.", key);
}
return value;
}
/**
* Returns a parsed value for a required parameter key.
*
* @param key parameter key to look up
* @param parser parser used to convert the raw value
* @param <T> target type returned by the parser
* @return parsed parameter value
* @throws MissingParameterException if no parameter with the given key exists
* @throws ParameterParseException if parsing the raw value fails
*/
public <T> T require(String key, ThrowingParser<T> parser)
throws MissingParameterException, ParameterParseException {
String value = require(key);
try {
return parser.parse(value);
} catch (Exception e) {
throw new ParameterParseException(
"Error while parsing '" + key + "' with specified parser", key, e);
}
}
/**
* Returns the raw value for a parameter key or the provided default value if missing.
*
* @param key parameter key to look up
* @param defaultValue value returned when the key does not exist
* @return found parameter value or {@code defaultValue} if absent
*/
public String optional(String key, String defaultValue) {
String value = index.get(key);
if (value == null) {
return defaultValue;
}
return value;
}
/**
* Returns a parsed value for a parameter key or the provided default value if missing.
*
* @param key parameter key to look up
* @param defaultValue value returned when the key does not exist
* @param parser parser used to convert the raw value
* @param <T> target type returned by the parser
* @return parsed parameter value or {@code defaultValue} if absent
* @throws ParameterParseException if parsing the raw value fails
*/
public <T> T optional(String key, T defaultValue, ThrowingParser<T> parser)
throws ParameterParseException {
String value = index.get(key);
if (value == null) {
return defaultValue;
}
try {
return parser.parse(value);
} catch (Exception e) {
throw new ParameterParseException(
"Error while parsing '" + key + "' with specified parser", key, e);
}
}
/**
* Checks whether a parameter with the given key exists.
*
* @param key parameter key to check
* @return {@code true} if the key exists, otherwise {@code false}
*/
public boolean containsKey(String key) {
return index.containsKey(key);
}
}
@@ -0,0 +1,18 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.accessor;
/**
* Functional parser interface used to convert a raw string parameter into a target type.
*
* @param <T> target type produced by the parser
*/
@FunctionalInterface
interface ThrowingParser<T> {
/**
* Parses the provided raw parameter value.
*
* @param value raw parameter value
* @return parsed value
* @throws Exception if the value cannot be parsed
*/
T parse(String value) throws Exception;
}
@@ -1,22 +1,20 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.response; package ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response;
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionId; import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder.ResponseBody;
/** Response representing an error outcome for a client's request. */ /** Response representing an error outcome for a client's request. */
public class ErrorResponse extends Response { public class ErrorResponse extends Response {
/** /**
* Construct an error response with a code and message. * Construct an error response with a code and message.
* *
* @param sessionId the target session id * @param context the RequestContext of the request
* @param requestId the originating request id
* @param errorCode a short error code identifying the failure * @param errorCode a short error code identifying the failure
* @param errorMessage a human readable error message * @param errorMessage a human readable error message
*/ */
public ErrorResponse( public ErrorResponse(RequestContext context, String errorCode, String errorMessage) {
SessionId sessionId, int requestId, String errorCode, String errorMessage) {
super( super(
sessionId, context,
requestId,
ResponseBody.builder().param("CODE", errorCode).param("MSG", errorMessage).build()); ResponseBody.builder().param("CODE", errorCode).param("MSG", errorMessage).build());
} }
@@ -0,0 +1,20 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder.ResponseBody;
/**
* A simple success response with an empty body.
*
* <p>Use this to acknowledge successful requests that do not carry additional payload data.
*/
public class OkResponse extends SuccessResponse {
/**
* Create a minimal successful response (no body content).
*
* @param context the RequestContext of the request
*/
public OkResponse(RequestContext context) {
super(context, ResponseBody.builder().build());
}
}
@@ -1,4 +1,4 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.response; package ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response;
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionId; import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionId;
@@ -1,23 +1,22 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.response; package ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder.ResponseBody;
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionId; import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionId;
/** Abstract base class for all server responses sent to clients. */ /** Abstract base class for all server responses sent to clients. */
public abstract class Response { public abstract class Response {
private final SessionId sessionId; private final RequestContext context;
private final int requestId;
private final ResponseBody body; private final ResponseBody body;
/** /**
* Create a new {@code Response}. * Create a new {@code Response}.
* *
* @param sessionId the id of the session this response targets * @param context the RequestContext of the request
* @param requestId the request identifier this response corresponds to
* @param body the structured response body * @param body the structured response body
*/ */
protected Response(SessionId sessionId, int requestId, ResponseBody body) { protected Response(RequestContext context, ResponseBody body) {
this.sessionId = sessionId; this.context = context;
this.requestId = requestId;
this.body = body; this.body = body;
} }
@@ -29,12 +28,12 @@ public abstract class Response {
public abstract String prefix(); public abstract String prefix();
/** /**
* Returns the session id that should receive this response. * Returns the session id of the session that should receive this response.
* *
* @return the target {@link SessionId} * @return the target {@link SessionId}
*/ */
public SessionId getSessionId() { public SessionId getSessionId() {
return sessionId; return context.sessionId();
} }
/** /**
@@ -43,7 +42,7 @@ public abstract class Response {
* @return the numeric request id * @return the numeric request id
*/ */
public int getRequestId() { public int getRequestId() {
return requestId; return context.requestId();
} }
/** /**
@@ -1,6 +1,7 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.response; package ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response;
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionId; import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder.ResponseBody;
/** /**
* Abstract {@link Response} specialization indicating a successful outcome. * Abstract {@link Response} specialization indicating a successful outcome.
@@ -12,12 +13,11 @@ public abstract class SuccessResponse extends Response {
/** /**
* Create a successful response with the provided body. * Create a successful response with the provided body.
* *
* @param sessionId the session id this response targets * @param context the RequestContext of the request
* @param requestId the originating request id
* @param body the response body * @param body the response body
*/ */
protected SuccessResponse(SessionId sessionId, int requestId, ResponseBody body) { protected SuccessResponse(RequestContext context, ResponseBody body) {
super(sessionId, requestId, body); super(context, body);
} }
/** /**
@@ -1,4 +1,4 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.response; package ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder;
import java.util.List; import java.util.List;
@@ -1,5 +1,6 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.response; package ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.Response;
import java.util.List; import java.util.List;
/** /**
@@ -1,4 +1,4 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.response; package ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -1,4 +1,4 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.response; package ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder;
/** /**
* Marker interface for elements that may appear in a {@link ResponseBody}. * Marker interface for elements that may appear in a {@link ResponseBody}.
@@ -1,4 +1,4 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.response; package ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder;
/** /**
* A parameter node stored in a {@link ResponseBody}. * A parameter node stored in a {@link ResponseBody}.
@@ -0,0 +1,7 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher;
public class ResponseDispatchException extends RuntimeException {
public ResponseDispatchException(String message, Throwable cause) {
super(message, cause);
}
}
@@ -1,5 +1,7 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.response; package ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.PrimitiveResponse;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.Response;
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.Session; import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.Session;
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionManager; import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionManager;
@@ -25,12 +27,17 @@ public class ResponseDispatcher {
* the target session's response queue. * the target session's response queue.
* *
* @param response the response to dispatch * @param response the response to dispatch
* @throws InterruptedException if the thread is interrupted while waiting to enqueue the * @throws ResponseDispatchException wraps any exceptions that occur during dispatching, such as
* primitive response * the {@link InterruptedException}
*/ */
public void dispatch(Response response) throws InterruptedException { public void dispatch(Response response) {
PrimitiveResponse primitiveResponse = ResponseEncoder.encode(response); PrimitiveResponse primitiveResponse = ResponseEncoder.encode(response);
Session session = sessionManager.getSessionById(response.getSessionId()); Session session = sessionManager.getSessionById(response.getSessionId());
try {
session.getResponseQueue().put(primitiveResponse); session.getResponseQueue().put(primitiveResponse);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new ResponseDispatchException("Interrupted while dispatching response", e);
}
} }
} }
@@ -1,4 +1,10 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.response; package ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.PrimitiveResponse;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.Response;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder.ResponseBlock;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder.ResponseNode;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder.ResponseParameter;
/** /**
* Utility responsible for encoding a {@link Response} into a protocol payload string and wrapping * Utility responsible for encoding a {@link Response} into a protocol payload string and wrapping
@@ -1,20 +0,0 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.response;
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionId;
/**
* A simple success response with an empty body.
*
* <p>Use this to acknowledge successful requests that do not carry additional payload data.
*/
public class OkResponse extends SuccessResponse {
/**
* Create a minimal successful response (no body content).
*
* @param sessionId the target session id
* @param requestId the originating request id
*/
public OkResponse(SessionId sessionId, int requestId) {
super(sessionId, requestId, ResponseBody.builder().build());
}
}
@@ -1,17 +1,18 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.sessions; package ch.unibas.dmi.dbis.cs108.casono.server.network.sessions;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.CommandRouter; import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandRouter;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.CommandParserDispatcher;
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.CommandParserDispatcher; import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.PrimitiveResponse;
import ch.unibas.dmi.dbis.cs108.casono.server.network.response.PrimitiveResponse;
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.IOException; import java.time.Instant;
import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue; import java.util.concurrent.BlockingQueue;
/** Represents a client session in the network server. */ /** Represents a client session in the network server. */
public class Session { public class Session {
private final SessionId id; private final SessionId id;
private Instant lastActivity;
private final TransportLayer transport; private final TransportLayer transport;
private final BlockingQueue<PrimitiveResponse> responseQueue; private final BlockingQueue<PrimitiveResponse> responseQueue;
private final CommandParserDispatcher dispatcher; private final CommandParserDispatcher dispatcher;
@@ -23,7 +24,6 @@ public class Session {
* *
* @param transport the transport layer for communication * @param transport the transport layer for communication
* @param eventBus the event bus for publishing events * @param eventBus the event bus for publishing events
* @throws IOException if an I/O error occurs during initialization
*/ */
public Session( public Session(
TransportLayer transport, TransportLayer transport,
@@ -31,6 +31,7 @@ public class Session {
CommandParserDispatcher dispatcher, CommandParserDispatcher dispatcher,
CommandRouter router) { CommandRouter router) {
this.id = new SessionId(); this.id = new SessionId();
this.lastActivity = Instant.now();
this.transport = transport; this.transport = transport;
this.dispatcher = dispatcher; this.dispatcher = dispatcher;
this.router = router; this.router = router;
@@ -46,6 +47,20 @@ public class Session {
return this.id; return this.id;
} }
/**
* Gets the timestamp of the last inbound activity for this session.
*
* @return an {@link Instant} representing the time of the last inbound activity
*/
public Instant getLastInboundActivity() {
return lastActivity;
}
/** Updates the timestamp of the last inbound activity for this session. */
public void updateLastInboundActivity() {
this.lastActivity = Instant.now();
}
/** /**
* Returns the TransportLayer of this session * Returns the TransportLayer of this session
* *
@@ -0,0 +1,40 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.sessions;
import ch.unibas.dmi.dbis.cs108.casono.server.network.events.DisconnectEvent;
import ch.unibas.dmi.dbis.cs108.casono.server.network.events.EventBus;
import java.time.Duration;
import java.time.Instant;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class SessionDisconnectJob implements Runnable {
private final Logger logger;
private final SessionManager sessionManager;
private final EventBus eventBus;
private final Duration timeoutThreshold;
public SessionDisconnectJob(
SessionManager sessionManager, EventBus eventBus, Duration timeoutThreshold) {
this.logger = LogManager.getLogger(SessionDisconnectJob.class);
this.sessionManager = sessionManager;
this.eventBus = eventBus;
this.timeoutThreshold = timeoutThreshold;
}
@Override
public void run() {
logger.debug("Job started.");
Instant threshold = Instant.now().minus(timeoutThreshold);
for (Session session : sessionManager.getAllSessions()) {
if (session.getLastInboundActivity().isBefore(threshold)) {
eventBus.publish(new DisconnectEvent(session.getId()));
logger.info(
"Initiated disconnect of {}, as it hasn't been active since a while",
session.getId());
}
}
logger.debug("Job finished.");
}
}
@@ -14,7 +14,7 @@ public class SessionId {
/** /**
* Creates a new SessionId with the specified UUID. * Creates a new SessionId with the specified UUID.
* *
* @param UUID to use for this SessionId * @param value UUID to use for this SessionId
*/ */
public SessionId(UUID value) { public SessionId(UUID value) {
this.value = value; this.value = value;
@@ -1,13 +1,15 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.sessions; package ch.unibas.dmi.dbis.cs108.casono.server.network.sessions;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.CommandRouter; import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandRouter;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.CommandParserDispatcher;
import ch.unibas.dmi.dbis.cs108.casono.server.network.events.DisconnectEvent; import ch.unibas.dmi.dbis.cs108.casono.server.network.events.DisconnectEvent;
import ch.unibas.dmi.dbis.cs108.casono.server.network.events.EventBus; import ch.unibas.dmi.dbis.cs108.casono.server.network.events.EventBus;
import ch.unibas.dmi.dbis.cs108.casono.server.network.parser.CommandParserDispatcher;
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.IOException; import java.io.IOException;
import java.util.Collection;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
@@ -86,7 +88,7 @@ public class SessionManager {
/** /**
* Handler for the DisconnectEvent * Handler for the DisconnectEvent
* *
* @param id of the session that disconnected * @param event the DisconnectEvent to handle
*/ */
public void onDisconnect(DisconnectEvent event) { public void onDisconnect(DisconnectEvent event) {
logger.debug("Recieved DisconnectEvent event for session {}", event.sessionId().value()); logger.debug("Recieved DisconnectEvent event for session {}", event.sessionId().value());
@@ -109,4 +111,8 @@ public class SessionManager {
return handle.session(); return handle.session();
} }
public Collection<Session> getAllSessions() {
return sessions.values().stream().map(SessionHandle::session).collect(Collectors.toList());
}
} }
@@ -1,18 +1,23 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.sessions; package ch.unibas.dmi.dbis.cs108.casono.server.network.sessions;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.CommandRouter; import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandRouter;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.CommandParserDispatcher;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.UnknownCommandException;
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.CommandParserDispatcher; import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.parser.ProtocolParser;
import ch.unibas.dmi.dbis.cs108.casono.server.network.parser.PrimitiveRequest; import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.parser.ProtocolParserException;
import ch.unibas.dmi.dbis.cs108.casono.server.network.parser.ProtocolParser; import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.parser.tokenizer.TokenizerException;
import ch.unibas.dmi.dbis.cs108.casono.server.network.parser.ProtocolParserException; import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.PrimitiveRequest;
import ch.unibas.dmi.dbis.cs108.casono.server.network.parser.RawRequest; import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RawRequest;
import ch.unibas.dmi.dbis.cs108.casono.server.network.parser.Request; import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.Request;
import ch.unibas.dmi.dbis.cs108.casono.server.network.parser.RequestContext; import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.ErrorResponse;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.PrimitiveResponse;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatchException;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseEncoder;
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.TransportLayer; import ch.unibas.dmi.dbis.cs108.casono.server.network.transport.TransportLayer;
import ch.unibas.dmi.dbis.cs108.casono.server.tokenizer.TokenizerException;
import java.io.EOFException; import java.io.EOFException;
import java.io.IOException; import java.io.IOException;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
@@ -41,34 +46,79 @@ public class SessionReader implements Runnable {
public void run() { public void run() {
while (!Thread.currentThread().isInterrupted()) { while (!Thread.currentThread().isInterrupted()) {
RawPacket rawPacket = null; RawPacket rawPacket = null;
RawRequest rawRequest = null;
RequestContext requestContext = null;
try { try {
// Step 1: Read from transport
rawPacket = transport.read(); rawPacket = transport.read();
session.updateLastInboundActivity();
logger.debug("Recieved: {}", rawPacket); logger.debug("Recieved: {}", rawPacket);
requestContext = new RequestContext(session.getId(), rawPacket.requestId());
RawRequest rawRequest = ProtocolParser.parse(rawPacket.payload()); // Step 2: Syntax validation and conversion into transport object
rawRequest = ProtocolParser.parse(rawPacket.payload());
logger.debug("Parsed request to {}", rawRequest); logger.debug("Parsed request to {}", rawRequest);
RequestContext requestContext =
new RequestContext(session.getId(), rawPacket.requestId());
PrimitiveRequest primitiveRequest = PrimitiveRequest primitiveRequest =
new PrimitiveRequest( new PrimitiveRequest(
requestContext, rawRequest.command(), rawRequest.parameters()); requestContext, rawRequest.command(), rawRequest.parameters());
logger.debug("Converted to {}", primitiveRequest); logger.debug("Converted to {}", primitiveRequest);
// Step 3: Parse into Request and execute Request
Request request = dispatcher.parse(primitiveRequest); Request request = dispatcher.parse(primitiveRequest);
router.execute(request); router.execute(request);
} catch (EOFException e) { } catch (EOFException e) {
logger.info("Client disconnected"); logger.info("Client disconnected");
eventBus.publish(new DisconnectEvent(session.getId())); eventBus.publish(new DisconnectEvent(session.getId()));
break; break;
} catch (TokenizerException | ProtocolParserException e) { } catch (TokenizerException | ProtocolParserException e) {
logger.error("Error occured while parsing request. RawPacket: {}", rawPacket, e); logger.error("Error occured while parsing request. RawPacket: {}", rawPacket, e);
// TODO: Send error response to client sendErrorResponse(
new ErrorResponse(
requestContext,
"PARSING_ERROR",
"Error occured during parsing. Likely due to malformed payload."));
} catch (UnknownCommandException e) {
logger.error("Recieved unknown command '{}' from client", rawRequest.command(), e);
sendErrorResponse(
new ErrorResponse(
requestContext,
"UNKNOWN_COMMAND",
"This command is unknown to the server."));
} catch (ResponseDispatchException e) {
logger.error(
"Unexpected ResponseDispatchException exception while dispatching request",
e);
} catch (IOException e) { } catch (IOException e) {
logger.error("Unexpected exception while reading from transport", e); logger.error("Unexpected IO exception while reading from transport", e);
} catch (RuntimeException e) {
logger.error("Unexpected RuntimeException occured", e);
sendErrorResponse(
new ErrorResponse(
requestContext,
"INTERNAL_ERROR",
"Unexpected internal server error occured."));
} }
} }
} }
/**
* Helperfunction to send ErrorResponse to client
*
* @param response to send to the client
*/
private void sendErrorResponse(ErrorResponse response) {
PrimitiveResponse primitiveResponse = ResponseEncoder.encode(response);
try {
session.getResponseQueue().put(primitiveResponse);
} catch (InterruptedException e) {
logger.error("Got interrupted while sending ErrorResponse to client.");
}
}
} }
@@ -1,6 +1,6 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.sessions; package ch.unibas.dmi.dbis.cs108.casono.server.network.sessions;
import ch.unibas.dmi.dbis.cs108.casono.server.network.response.PrimitiveResponse; import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.PrimitiveResponse;
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.TransportLayer; import ch.unibas.dmi.dbis.cs108.casono.server.network.transport.TransportLayer;
import java.io.IOException; import java.io.IOException;
@@ -50,8 +50,8 @@ public class TcpTransport implements TransportLayer {
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(requestId);
out.writeInt(rawPayload.length); out.writeInt(rawPayload.length);
out.writeInt(requestId);
out.write(rawPayload); out.write(rawPayload);
out.flush(); out.flush();
} }
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 902 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 701 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 651 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 665 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 659 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 660 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 657 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 659 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 661 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 658 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 663 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 662 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 663 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 657 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 664 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 665 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 648 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 661 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 656 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 656 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 653 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 655 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 658 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 654 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 660 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 658 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 659 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 653 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 660 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 660 KiB

Some files were not shown because too many files have changed in this diff Show More