diff --git a/.gitignore b/.gitignore index c29286e..56624a0 100644 --- a/.gitignore +++ b/.gitignore @@ -121,4 +121,5 @@ $RECYCLE.BIN/ .idea ## bin -bin/ \ No newline at end of file +bin/ +gradle.properties \ No newline at end of file diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 04bb8bd..ef2d1f5 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -6,11 +6,13 @@ stages: # Reusable definitions .gradle-cache: &gradle-cache + variables: + GRADLE_USER_HOME: '$CI_PROJECT_DIR/.gradle-home' cache: key: '$CI_PROJECT_ID-gradle' paths: - .gradle/ - - ~/.gradle/caches/ + - .gradle-home/ .on-commit: &on-commit rules: @@ -22,7 +24,7 @@ stages: .on-mr: &on-mr rules: - if: '$CI_PIPELINE_SOURCE == "merge_request_event"' - allow_failure: true # deactivate after all issues (shown by checkstyle) have been resolved + allow_failure: false # Jobs checkstyle: @@ -30,7 +32,7 @@ checkstyle: stage: lint image: gradle:9.3.1-jdk25 script: - - gradle checkstyleMain checkstyleTest + - gradle checkstyleMain checkstyleTest --configuration-cache --configuration-cache-problems=warn allow_failure: true artifacts: when: always @@ -47,8 +49,8 @@ checkstyle-mr: stage: lint image: gradle:9.3.1-jdk25 script: - - gradle checkstyleMain checkstyleTest - allow_failure: true + - gradle checkstyleMain checkstyleTest --configuration-cache --configuration-cache-problems=warn + allow_failure: false artifacts: when: always paths: @@ -66,6 +68,7 @@ checkstyle-report: - job: checkstyle-mr artifacts: true optional: true + when: always rules: - if: '$CI_PIPELINE_SOURCE == "merge_request_event"' script: @@ -81,7 +84,7 @@ compile-check: stage: build image: gradle:9.3.1-jdk25 script: - - gradle compileTestJava + - gradle compileTestJava --configuration-cache --configuration-cache-problems=warn needs: [] rules: - if: '$CI_PIPELINE_SOURCE == "merge_request_event"' @@ -94,7 +97,7 @@ test: stage: test image: gradle:9.3.1-jdk25 script: - - gradle test + - gradle test --configuration-cache --configuration-cache-problems=warn artifacts: when: always reports: diff --git a/.gitlab/issue_templates/Bug Report.md b/.gitlab/issue_templates/Bug Report.md new file mode 100644 index 0000000..2f04712 --- /dev/null +++ b/.gitlab/issue_templates/Bug Report.md @@ -0,0 +1,40 @@ +## Bug Report + + +### Environment +- **Branch & Commit:** +- **Operating system:** +- **How was execution started (Gradle task / IDE debug / IDE run):** +- **Java version:** `25` +- **Gradle version**: `9.3.1` + +### Summary + + +### Expected Behavior + + +### Actual Behavior + + +### Steps to Reproduce + + +### Stack Trace / Error Message +``` + +``` + +### Possible Cause / Notes + + +### Checklist +- [ ] I reproduced the problem using the steps above +- [ ] I searched documentation docs for relevant information +- [ ] I added relevant labels + +/label ~bug diff --git a/.gitlab/issue_templates/Feature Request.md b/.gitlab/issue_templates/Feature Request.md new file mode 100644 index 0000000..5a11cf5 --- /dev/null +++ b/.gitlab/issue_templates/Feature Request.md @@ -0,0 +1,18 @@ +## Feature Request + + +### Summary + + +### Required workarround + + +### Description + + +### Checklist +- [ ] I reproduced the problem using the steps above +- [ ] I searched documentation docs for relevant information +- [ ] I added relevant labels + +/label ~enhancement diff --git a/.gitlab/issue_templates/Task.md b/.gitlab/issue_templates/Task.md new file mode 100644 index 0000000..27761d5 --- /dev/null +++ b/.gitlab/issue_templates/Task.md @@ -0,0 +1,24 @@ +## Task + + +### Summary + + +### Context + + + +### Current Progress +- [ ] +- [ ] +- [ ] + +### Notes + + +### Checklist +- [ ] I outlined checkpoints describing phases of my work related to this task +- [ ] I added relevant labels +- [ ] I linked to other issues or branches that must be completed first + +/label ~task diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..92a520a --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,208 @@ +# Contribution Guidelines +This document describes the conventions and workflows everyone must follow to keep the codebase consistent and the collaboration smooth. +If you notice a violation, speak to the person involved respectfully. + +Since this project is part of a course at the University of Basel, the [Code of Conduct](https://www.unibas.ch/de/Universitaet/Administration-Services/Vizerektorat-People-And-Culture/Persoenliche-Integritaet/Code-of-Conduct.html) applies. + + +## Table of Contents +- [Contribution Guidelines](#contribution-guidelines) + - [Table of Contents](#table-of-contents) + - [Issues \& Tasks](#issues--tasks) + - [Creating an issue](#creating-an-issue) + - [During implementation](#during-implementation) + - [Collaborative work](#collaborative-work) + - [Git Workflow](#git-workflow) + - [Creating a branch](#creating-a-branch) + - [Working on a branch](#working-on-a-branch) + - [Commit Messages](#commit-messages) + - [Rules](#rules) + - [Examples](#examples) + - [Code Style](#code-style) + - [Linter](#linter) + - [Formatter](#formatter) + - [General guidelines](#general-guidelines) + - [CI/CD Pipeline](#cicd-pipeline) + - [Before pushing](#before-pushing) + - [Merge Requests](#merge-requests) + - [Opening a MR](#opening-a-mr) + - [Merging](#merging) + - [After merging](#after-merging) + - [Be human](#be-human) + + +## Issues & Tasks +Every piece of work - whether a new feature, a bug fix, or a refactoring - must be tracked as an +Issue or Task in GitLab **before** any implementation begins. + +### Creating an issue +1. Open a new Issue or Task using the **relevant template** provided in the repository. +2. Fill in **all fields** specified by the template thoughtfully and completely. A well-written issue is the single source of truth for the work being done - treat it accordingly. +3. Work through the **checklist** in the template before marking the issue as ready. Do not skip items. + +### During implementation +- If you encounter a problem or an unexpected finding while working on an issue, record it as a **comment** on the issue. This keeps the history intact and visible to the whole team. +- **Do not restructurally edit the original description** to incorporate new information. The description reflects the intent at the time the issue was created; comments document what happened along the way. + +### Collaborative work +- When multiple people are working on the same issue, **prefer issue comments over private messages** for coordination. This keeps the current status, decisions, and open questions + centrally visible and searchable. +- Before starting work that overlaps with an existing issue, check its comment thread first to avoid duplicating effort. + + +## Git Workflow +We use a **feature branch -> main** strategy. The `main` branch is always in a releasable state. + +### Creating a branch +We follow the [**Conventional Branch**](https://conventional-branch.github.io/) specification. Branch names follow this pattern: + +``` +/ +``` + +| Type | When to use | +|------------|--------------------------------------------------| +| `feat` | New feature or capability | +| `fix` | Bug fix | +| `refactor` | Restructuring without behaviour change | +| `test` | Adding or fixing tests | +| `ci` | Pipeline, Gradle, or tooling changes | +| `docs` | Documentation only | + +**Examples:** + +``` +feat/reconnect-command +fix/session-writer-flush +refactor/user-registry-cleanup +docs/contributing +``` + +### Working on a branch +Keep branches short-lived. A branch should represent one cohesive unit of work. + +It is permissible to commit changes within a feature branch that cause the program to become non-functional, but these should be fixed as soon as possible. In any case, the code that is merged into `main` must be functional. + +And most importantly: **Do not commit directly to `main`**. + + +## Commit Messages +We follow the [**Conventional Commits**](https://www.conventionalcommits.org/) specification. Every commit message must have the form: + +``` +: + +[optional body] +``` + +### Rules +The summary line must be **<= 72 characters**, written in the **imperative mood** (e.g. "add", not "added" or "adds"). + +### Examples +``` +Reat: Add RECONNECT command handler + +The handler re-associates an existing User with a new Session after +a connection drop, preserving in-flight state. +``` + +``` +Fix: Flush output stream before closing +``` + +``` +Ci: Tighten Checkstyle failure policy to allow_failure: false +``` + +``` +Refactor: Replace ArrayList with CopyOnWriteArrayList +``` + + +## Code Style +Code formatting is enforced automatically. **Do not submit a MR with formatting violations.** + +### Linter +We use **Checkstyle** as our linter with a custom set of rules tailored to our project. + +Checkstyle runs on every pipeline. Fix all violations locally before pushing: + +```bash +./gradlew checkstyleMain checkstyleTest +``` + + +### Formatter +We use **Spotless** with the **[Google](https://google.github.io/styleguide/javaguide.html) / [AOSP Java style](https://source.android.com/docs/core/architecture/hidl/code-style?hl=en)**: + +- **Indentation:** 4 spaces (no tabs) +- **Line length:** 100 characters +- No decorative blank lines directly after opening braces `{` +- Blank lines are reserved for separating logical sections within a block + +Run the formatter before committing: + +```bash +./gradlew spotlessApply +``` + +Check without applying: + +```bash +./gradlew spotlessCheck +``` + +### General guidelines +- Add **JavaDoc** docstrings to classes, interfaces, records and methods. +- Exercise **clean architecture** +- Prefer **stateless components** +- Use **`record` types** for immutable data carriers. +- Log with **Log4J 2** (`log4j-api`). Use the appropriate level (`DEBUG` for pipeline internals, `INFO` for lifecycle events, `WARN`/`ERROR` for recoverable/unrecoverable problems). + + +## CI/CD Pipeline +The pipeline runs automatically on every push. It has four stages: + +``` +lint > report > build > test +``` + +| Stage | Jobs | +|----------|--------------------------------------------| +| `lint` | Spotless check, Checkstyle | +| `report` | Code Quality JSON conversion (only for mr) | +| `build` | `./gradlew assemble` | +| `test` | `./gradlew test` + JUnit result reporting | + +### Before pushing +Run the full check suite locally to avoid a broken pipeline: + +```bash +./gradlew spotlessCheck checkstyleMain checkstyleTest build test +``` + +A red pipeline blocks merging. Fix failures before creating your merge request. + + +## Merge Requests + +### Opening a MR +- Target branch is always **`main`**. +- Fill in the MR description: what changed and why. Link the relevant issue (with 'Closing #x') if one exists. + +### Merging +A MR can be merged when **all main CI pipeline stages are green** (lint, build, test). + +No explicit peer approval is required, but leaving a note or question in the MR thread for non-trivial changes is encouraged. +If you spot a problem in someone else's open MR, comment - **do not push directly to their branch** and try to fix the issue yourself. + +### After merging +Delete the feature branch after the MR is merged. GitLab can do this automatically via the +"Delete source branch" checkbox in the MR. + + +## Be human +We are all human. +We all forget things or make mistakes sometimes. + +It’s important that we look out for one another and **work together as a team**. diff --git a/build.gradle b/build.gradle index e126d6d..4360c82 100644 --- a/build.gradle +++ b/build.gradle @@ -55,7 +55,7 @@ checkstyle { spotless { java { - googleJavaFormat('1.25.2').aosp() + googleJavaFormat('1.35.0').aosp() importOrder() removeUnusedImports() trimTrailingWhitespace() @@ -108,3 +108,17 @@ tasks.register('fatJar', Jar) { 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') + } diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/ServerApp.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/ServerApp.java index d44df55..5687b5b 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/ServerApp.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/ServerApp.java @@ -3,8 +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.UserRegistry; import ch.unibas.dmi.dbis.cs108.casono.server.network.NetworkManager; +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.EventBus; +import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionDisconnectJob; import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionManager; import java.time.Duration; import java.util.concurrent.Executors; @@ -15,9 +18,12 @@ import org.apache.logging.log4j.Logger; /** Application class for starting the server. */ public class ServerApp { - public static final int USER_CLEANUP_JOB_DELAY = 0; - public 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_DELAY = 0; + private static final int USER_CLEANUP_JOB_PERIOD = 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) { int port = Integer.parseInt(arg); @@ -26,10 +32,12 @@ public class ServerApp { logger.info("Starting server at port {}", port); EventBus eventBus = new EventBus(); - SessionManager sessionManager = new SessionManager(); - eventBus.subscribe( - DisconnectEvent.class, event -> sessionManager.removeSession(event.sessionId())); - NetworkManager networkManager = new NetworkManager(port, sessionManager, eventBus); + CommandParserDispatcher dispatcher = new CommandParserDispatcher(); + CommandRouter router = new CommandRouter(); + + SessionManager sessionManager = new SessionManager(eventBus, dispatcher, router); + eventBus.subscribe(DisconnectEvent.class, event -> sessionManager.onDisconnect(event)); + NetworkManager networkManager = new NetworkManager(port, sessionManager); UserRegistry userRegistry = new UserRegistry(); eventBus.subscribe( @@ -41,6 +49,14 @@ public class ServerApp { USER_CLEANUP_JOB_DELAY, USER_CLEANUP_JOB_PERIOD, 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(); } diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/user/UserRegistry.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/user/UserRegistry.java index 6a60877..adb1abe 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/user/UserRegistry.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/user/UserRegistry.java @@ -58,7 +58,6 @@ public class UserRegistry { * * @param sessionId the session ID of the disconnected client */ - // TODO: Add to EventRegistry with DisconnectEvent public synchronized void onDisconnect(SessionId sessionId) { User user = bySessionId.remove(sessionId); if (user == null) { diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/NetworkManager.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/NetworkManager.java index ad2e5b3..d9382a4 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/NetworkManager.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/NetworkManager.java @@ -1,8 +1,5 @@ package ch.unibas.dmi.dbis.cs108.casono.server.network; -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.sessions.Session; import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionManager; import ch.unibas.dmi.dbis.cs108.casono.server.network.transport.TcpTransport; import java.io.IOException; @@ -18,23 +15,19 @@ public class NetworkManager implements Runnable { private Thread thread; private Boolean running; private SessionManager sessionManager; - private EventBus eventBus; /** * Creates a new NetworkManager with the given port, session manager, and event bus. * * @param port the port to listen on * @param sessionManager the session manager to use - * @param eventBus the event bus for events */ - public NetworkManager(Integer port, SessionManager sessionManager, EventBus eventBus) { + public NetworkManager(Integer port, SessionManager sessionManager) { this.port = port; this.logger = LogManager.getLogger(NetworkManager.class); this.thread = new Thread(this, "networkManager"); this.running = true; this.sessionManager = sessionManager; - this.eventBus = eventBus; - this.eventBus.subscribe(DisconnectEvent.class, event -> clientDisconnected(event)); } /** Starts the internal thread to accept new connections. */ @@ -43,15 +36,6 @@ public class NetworkManager implements Runnable { thread.start(); } - /** - * Handles client disconnection events. - * - * @param event the disconnect event - */ - public void clientDisconnected(DisconnectEvent event) { - logger.info("Session {} disconnected", event.sessionId().value()); - } - /** Runs the network manager loop, accepting connections. */ @Override public void run() { @@ -61,9 +45,7 @@ public class NetworkManager implements Runnable { logger.debug("Accepted connection from {}", clientSocket.getRemoteSocketAddress()); - Session session = new Session(new TcpTransport(clientSocket), eventBus); - sessionManager.addSession(session); - session.start(); + sessionManager.create(new TcpTransport(clientSocket)); } } catch (IOException e) { diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/command/execution/CommandHandler.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/command/execution/CommandHandler.java new file mode 100644 index 0000000..0fdae3c --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/command/execution/CommandHandler.java @@ -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 { + void execute(T request); +} diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/command/execution/CommandRouter.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/command/execution/CommandRouter.java new file mode 100644 index 0000000..269c6a4 --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/command/execution/CommandRouter.java @@ -0,0 +1,28 @@ +package ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution; + +import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.Request; +import java.util.HashMap; +import java.util.Map; + +public class CommandRouter { + private final Map, CommandHandler> handlers = new HashMap<>(); + + public void register(Class request, CommandHandler handler) { + handlers.put(request, handler); + } + + // Safe, because during registration, it's ensured that the provided CommandHandler only + // receives requests it can handle. + @SuppressWarnings("unchecked") + public void execute(Request request) { + CommandHandler handler = + (CommandHandler) handlers.get(request.getClass()); + + if (handler == null) { + String requestName = request.getClass().toString(); + throw new UnknownRequestException( + "Unable to execute request " + requestName + ". Type unknown", requestName); + } + handler.execute(request); + } +} diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/command/execution/UnknownRequestException.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/command/execution/UnknownRequestException.java new file mode 100644 index 0000000..1493f25 --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/command/execution/UnknownRequestException.java @@ -0,0 +1,14 @@ +package ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution; + +public class UnknownRequestException extends RuntimeException { + private final String requestName; + + public UnknownRequestException(String message, String requestName) { + super(message); + this.requestName = requestName; + } + + public String getRequestName() { + return requestName; + } +} diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/parser/CommandParser.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/command/parsing/CommandParser.java similarity index 59% rename from src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/parser/CommandParser.java rename to src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/command/parsing/CommandParser.java index ae00bca..3a3b76f 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/parser/CommandParser.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/command/parsing/CommandParser.java @@ -1,4 +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; /** * Parser to convert the PrimitiveRequest to a Request and performing checks for required fields and diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/parser/CommandParserDispatcher.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/command/parsing/CommandParserDispatcher.java similarity index 80% rename from src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/parser/CommandParserDispatcher.java rename to src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/command/parsing/CommandParserDispatcher.java index 722815c..02480ac 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/parser/CommandParserDispatcher.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/command/parsing/CommandParserDispatcher.java @@ -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.Map; diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/command/parsing/RequestParameter.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/command/parsing/RequestParameter.java new file mode 100644 index 0000000..d0ca720 --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/command/parsing/RequestParameter.java @@ -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) {} diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/parser/UnknownCommandException.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/command/parsing/UnknownCommandException.java similarity index 78% rename from src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/parser/UnknownCommandException.java rename to src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/command/parsing/UnknownCommandException.java index 5048d70..0b5759b 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/parser/UnknownCommandException.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/command/parsing/UnknownCommandException.java @@ -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 diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/parser/Parameter.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/parser/Parameter.java deleted file mode 100644 index fb35343..0000000 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/parser/Parameter.java +++ /dev/null @@ -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) {} diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/parser/PrimitiveRequest.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/parser/PrimitiveRequest.java deleted file mode 100644 index 4ac06de..0000000 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/parser/PrimitiveRequest.java +++ /dev/null @@ -1,6 +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(int requestId, String command, List parameters) {} diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/parser/Request.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/parser/Request.java deleted file mode 100644 index 4018a31..0000000 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/parser/Request.java +++ /dev/null @@ -1,4 +0,0 @@ -package ch.unibas.dmi.dbis.cs108.casono.server.network.parser; - -/** Request, produced by the CommandParser */ -public interface Request {} diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/parser/ProtocolParser.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/parser/ProtocolParser.java similarity index 69% rename from src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/parser/ProtocolParser.java rename to src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/parser/ProtocolParser.java index acb48a0..b3afd0c 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/parser/ProtocolParser.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/parser/ProtocolParser.java @@ -1,11 +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.network.transport.RawPacket; -import ch.unibas.dmi.dbis.cs108.casono.server.tokenizer.RawToken; -import ch.unibas.dmi.dbis.cs108.casono.server.tokenizer.Token; -import ch.unibas.dmi.dbis.cs108.casono.server.tokenizer.TokenClassifier; -import ch.unibas.dmi.dbis.cs108.casono.server.tokenizer.TokenType; -import ch.unibas.dmi.dbis.cs108.casono.server.tokenizer.Tokenizer; +import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.RequestParameter; +import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.parser.tokenizer.RawToken; +import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.parser.tokenizer.Token; +import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.parser.tokenizer.TokenClassifier; +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.Iterator; import java.util.List; @@ -19,15 +20,15 @@ public class ProtocolParser { * @param packet the RawPacket containing the recieved data * @return created PrimitiveRequest */ - public static PrimitiveRequest parse(RawPacket packet) { - List rawTokens = Tokenizer.tokenize(packet.payload()); + public static RawRequest parse(String payload) { + List rawTokens = Tokenizer.tokenize(payload); List tokens = TokenClassifier.classify(rawTokens); Iterator iterator = tokens.iterator(); String command = readCommand(iterator); - List parameters = readParameters(iterator); + List parameters = readParameters(iterator); - return new PrimitiveRequest(packet.requestId(), command, parameters); + return new RawRequest(command, parameters); } /** @@ -51,8 +52,8 @@ public class ProtocolParser { * @param iterator * @return list containing all parsed parameters */ - private static List readParameters(Iterator iterator) { - List parameters = new ArrayList<>(); + private static List readParameters(Iterator iterator) { + List parameters = new ArrayList<>(); try { while (iterator.hasNext()) { @@ -66,7 +67,7 @@ public class ProtocolParser { readSeperator(iterator.next()); String value = readValue(iterator.next()); - parameters.add(new Parameter(key, value)); + parameters.add(new RequestParameter(key, value)); } } catch (NoSuchElementException e) { throw new ProtocolParserException("Ran out of tokens while reading parameter"); diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/parser/ProtocolParserException.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/parser/ProtocolParserException.java similarity index 67% rename from src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/parser/ProtocolParserException.java rename to src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/parser/ProtocolParserException.java index e00e960..63e57a9 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/parser/ProtocolParserException.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/parser/ProtocolParserException.java @@ -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 ProtocolParserException(String message) { diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/tokenizer/RawToken.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/parser/tokenizer/RawToken.java similarity index 63% rename from src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/tokenizer/RawToken.java rename to src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/parser/tokenizer/RawToken.java index c8bc6bb..1a07f35 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/tokenizer/RawToken.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/parser/tokenizer/RawToken.java @@ -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. */ public record RawToken(RawTokenType type, String value, int line, int column) {} diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/tokenizer/RawTokenType.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/parser/tokenizer/RawTokenType.java similarity index 51% rename from src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/tokenizer/RawTokenType.java rename to src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/parser/tokenizer/RawTokenType.java index b37d430..15949a3 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/tokenizer/RawTokenType.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/parser/tokenizer/RawTokenType.java @@ -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 { WORD, diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/tokenizer/State.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/parser/tokenizer/State.java similarity index 89% rename from src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/tokenizer/State.java rename to src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/parser/tokenizer/State.java index d130a52..2848d99 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/tokenizer/State.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/parser/tokenizer/State.java @@ -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.List; diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/tokenizer/Token.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/parser/tokenizer/Token.java similarity index 59% rename from src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/tokenizer/Token.java rename to src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/parser/tokenizer/Token.java index 93b9026..c99d3cd 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/tokenizer/Token.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/parser/tokenizer/Token.java @@ -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. */ public record Token(TokenType type, String value, int line, int column) {} diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/tokenizer/TokenClassifier.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/parser/tokenizer/TokenClassifier.java similarity index 97% rename from src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/tokenizer/TokenClassifier.java rename to src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/parser/tokenizer/TokenClassifier.java index cb19021..330dcbb 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/tokenizer/TokenClassifier.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/parser/tokenizer/TokenClassifier.java @@ -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.List; diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/tokenizer/TokenType.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/parser/tokenizer/TokenType.java similarity index 63% rename from src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/tokenizer/TokenType.java rename to src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/parser/tokenizer/TokenType.java index af52b02..5096da9 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/tokenizer/TokenType.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/parser/tokenizer/TokenType.java @@ -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. */ public enum TokenType { diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/tokenizer/Tokenizer.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/parser/tokenizer/Tokenizer.java similarity index 98% rename from src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/tokenizer/Tokenizer.java rename to src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/parser/tokenizer/Tokenizer.java index 805d05b..843edf9 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/tokenizer/Tokenizer.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/parser/tokenizer/Tokenizer.java @@ -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; diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/tokenizer/TokenizerException.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/parser/tokenizer/TokenizerException.java similarity index 84% rename from src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/tokenizer/TokenizerException.java rename to src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/parser/tokenizer/TokenizerException.java index 0975b1d..c514a1e 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/tokenizer/TokenizerException.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/parser/tokenizer/TokenizerException.java @@ -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. */ public class TokenizerException extends RuntimeException { diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/request/PrimitiveRequest.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/request/PrimitiveRequest.java new file mode 100644 index 0000000..e591a2f --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/request/PrimitiveRequest.java @@ -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 parameters) {} diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/request/RawRequest.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/request/RawRequest.java new file mode 100644 index 0000000..f5033a3 --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/request/RawRequest.java @@ -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 parameters) {} diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/request/Request.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/request/Request.java new file mode 100644 index 0000000..1e618ad --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/request/Request.java @@ -0,0 +1,24 @@ +package ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request; + +import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionId; + +/** Request, produced by the CommandParser */ +public abstract class Request { + protected final RequestContext context; + + public Request(RequestContext context) { + this.context = context; + } + + public RequestContext getContext() { + return context; + } + + public SessionId getSessionId() { + return context.sessionId(); + } + + public int getRequestId() { + return context.requestId(); + } +} diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/request/RequestContext.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/request/RequestContext.java new file mode 100644 index 0000000..63816f6 --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/request/RequestContext.java @@ -0,0 +1,14 @@ +package ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request; + +import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionId; + +/** + * Immutable context for a network request. + * + *

Contains the originating session's identifier and the request's id. Later used to create + * response. + * + * @param sessionId the identifier of the session that initiated the request + * @param requestId the request's numeric id within the session + */ +public record RequestContext(SessionId sessionId, int requestId) {} diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/request/accessor/MissingParameterException.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/request/accessor/MissingParameterException.java new file mode 100644 index 0000000..bda6e7e --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/request/accessor/MissingParameterException.java @@ -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; + } +} diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/request/accessor/ParameterParseException.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/request/accessor/ParameterParseException.java new file mode 100644 index 0000000..e6f9f1f --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/request/accessor/ParameterParseException.java @@ -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; + } +} diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/request/accessor/RequestParameterAccessor.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/request/accessor/RequestParameterAccessor.java new file mode 100644 index 0000000..04071e3 --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/request/accessor/RequestParameterAccessor.java @@ -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. + * + *

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 index; + + /** + * Creates an accessor + * + * @param parameters to use + */ + public RequestParameterAccessor(List 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 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 require(String key, ThrowingParser 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 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 optional(String key, T defaultValue, ThrowingParser 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); + } +} diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/request/accessor/ThrowingParser.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/request/accessor/ThrowingParser.java new file mode 100644 index 0000000..688304b --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/request/accessor/ThrowingParser.java @@ -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 target type produced by the parser + */ +@FunctionalInterface +interface ThrowingParser { + /** + * 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; +} diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/response/ErrorResponse.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/response/ErrorResponse.java new file mode 100644 index 0000000..1136e11 --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/response/ErrorResponse.java @@ -0,0 +1,35 @@ +package ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response; + +import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder.ResponseBody; +import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionId; + +/** Response representing an error outcome for a client's request. */ +public class ErrorResponse extends Response { + /** + * Construct an error response with a code and message. + * + * @param sessionId the target session id + * @param requestId the originating request id + * @param errorCode a short error code identifying the failure + * @param errorMessage a human readable error message + */ + public ErrorResponse( + SessionId sessionId, int requestId, String errorCode, String errorMessage) { + super( + sessionId, + requestId, + ResponseBody.builder().param("CODE", errorCode).param("MSG", errorMessage).build()); + } + + /** + * {@inheritDoc} + * + *

This implementation returns the fixed {@code -ERR} prefix. + * + * @return the {@code -ERR} prefix + */ + @Override + public String prefix() { + return "-ERR"; + } +} diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/response/OkResponse.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/response/OkResponse.java new file mode 100644 index 0000000..f0d7cd6 --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/response/OkResponse.java @@ -0,0 +1,21 @@ +package ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response; + +import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder.ResponseBody; +import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionId; + +/** + * A simple success response with an empty body. + * + *

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()); + } +} diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/response/PrimitiveResponse.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/response/PrimitiveResponse.java new file mode 100644 index 0000000..3f7ce3d --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/response/PrimitiveResponse.java @@ -0,0 +1,12 @@ +package ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response; + +import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionId; + +/** + * Immutable transport record representing an encoded response ready for delivery to a session. + * + * @param sessionId the target session id + * @param requestId the originating request id + * @param payload the serialized response payload + */ +public record PrimitiveResponse(SessionId sessionId, int requestId, String payload) {} diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/response/Response.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/response/Response.java new file mode 100644 index 0000000..d0918fb --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/response/Response.java @@ -0,0 +1,59 @@ +package ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response; + +import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder.ResponseBody; +import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionId; + +/** Abstract base class for all server responses sent to clients. */ +public abstract class Response { + private final SessionId sessionId; + private final int requestId; + private final ResponseBody body; + + /** + * Create a new {@code Response}. + * + * @param sessionId the id of the session this response targets + * @param requestId the request identifier this response corresponds to + * @param body the structured response body + */ + protected Response(SessionId sessionId, int requestId, ResponseBody body) { + this.sessionId = sessionId; + this.requestId = requestId; + this.body = body; + } + + /** + * Returns the protocol prefix for this response (for example {@code "+OK"} or {@code "-ERR"}). + * + * @return the response prefix string used by the encoder + */ + public abstract String prefix(); + + /** + * Returns the session id that should receive this response. + * + * @return the target {@link SessionId} + */ + public SessionId getSessionId() { + return sessionId; + } + + /** + * Returns the request identifier associated with this response. + * + * @return the numeric request id + */ + public int getRequestId() { + return requestId; + } + + /** + * Returns the immutable {@link ResponseBody} that carries the structured payload for this + * response. + * + * @return the response body + */ + public ResponseBody getBody() { + return body; + } +} diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/response/SuccessResponse.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/response/SuccessResponse.java new file mode 100644 index 0000000..3df95b9 --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/response/SuccessResponse.java @@ -0,0 +1,35 @@ +package ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response; + +import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder.ResponseBody; +import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionId; + +/** + * Abstract {@link Response} specialization indicating a successful outcome. + * + *

Implementations of this class use the {@code +OK} prefix. It provides a protected constructor + * so subclasses can supply the response body content. + */ +public abstract class SuccessResponse extends Response { + /** + * Create a successful response with the provided body. + * + * @param sessionId the session id this response targets + * @param requestId the originating request id + * @param body the response body + */ + protected SuccessResponse(SessionId sessionId, int requestId, ResponseBody body) { + super(sessionId, requestId, body); + } + + /** + * {@inheritDoc} + * + *

This implementation returns the fixed {@code +OK} prefix. + * + * @return the {@code +OK} prefix + */ + @Override + public final String prefix() { + return "+OK"; + } +} diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/response/builder/ResponseBlock.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/response/builder/ResponseBlock.java new file mode 100644 index 0000000..e738a27 --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/response/builder/ResponseBlock.java @@ -0,0 +1,12 @@ +package ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder; + +import java.util.List; + +/** + * A block node that contains a tag and a list of child {@link ResponseNode} elements. Blocks can be + * nested to build hierarchical response bodies. + * + * @param tag the block tag + * @param children the child nodes contained in this block + */ +public record ResponseBlock(String tag, List children) implements ResponseNode {} diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/response/builder/ResponseBody.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/response/builder/ResponseBody.java new file mode 100644 index 0000000..bb783d7 --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/response/builder/ResponseBody.java @@ -0,0 +1,41 @@ +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; + +/** + * Immutable container for the structured content of a {@link Response}. + * + *

A {@code ResponseBody} holds an ordered list of {@link ResponseNode} items (parameters and + * blocks). Use {@link #builder()} to construct instances. + */ +public class ResponseBody { + private final List nodes; + + /** + * Package-private constructor used by {@link ResponseBodyBuilder}. + * + * @param nodes the list of response nodes to include in this body + */ + ResponseBody(List nodes) { + this.nodes = List.copyOf(nodes); + } + + /** + * Create a new {@link ResponseBodyBuilder} for assembling a response body. + * + * @return a fresh builder instance + */ + public static ResponseBodyBuilder builder() { + return new ResponseBodyBuilder(); + } + + /** + * Returns the ordered list of {@link ResponseNode} elements contained in this body. + * + * @return an immutable list of nodes + */ + public List nodes() { + return nodes; + } +} diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/response/builder/ResponseBodyBuilder.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/response/builder/ResponseBodyBuilder.java new file mode 100644 index 0000000..c05083d --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/response/builder/ResponseBodyBuilder.java @@ -0,0 +1,51 @@ +package ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +/** + * Builder for {@link ResponseBody} instances. + * + *

Provides methods to append parameter nodes and nested blocks and to produce an immutable + * {@link ResponseBody} via {@link #build()}. + */ +public class ResponseBodyBuilder { + private final List nodes = new ArrayList<>(); + + /** + * Add a key/value parameter to the response body under construction. + * + * @param key the parameter name + * @param value the parameter value (will be converted to string when encoded) + * @return this builder for fluent chaining + */ + public ResponseBodyBuilder param(String key, Object value) { + nodes.add(new ResponseParameter(key, value)); + return this; + } + + /** + * Add a nested block with the given tag. The provided consumer receives a child builder to + * populate the block content. + * + * @param tag the block tag + * @param content consumer that appends child nodes to the block + * @return this builder for fluent chaining + */ + public ResponseBodyBuilder block(String tag, Consumer content) { + ResponseBodyBuilder childBuilder = new ResponseBodyBuilder(); + content.accept(childBuilder); + nodes.add(new ResponseBlock(tag, childBuilder.build().nodes())); + return this; + } + + /** + * Build an immutable {@link ResponseBody} from the accumulated nodes. + * + * @return a new {@link ResponseBody} + */ + public ResponseBody build() { + return new ResponseBody(nodes); + } +} diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/response/builder/ResponseNode.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/response/builder/ResponseNode.java new file mode 100644 index 0000000..b2bb118 --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/response/builder/ResponseNode.java @@ -0,0 +1,8 @@ +package ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder; + +/** + * Marker interface for elements that may appear in a {@link ResponseBody}. + * + *

Implementations include {@link ResponseParameter} and {@link ResponseBlock}. + */ +public interface ResponseNode {} diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/response/builder/ResponseParameter.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/response/builder/ResponseParameter.java new file mode 100644 index 0000000..73ffbf7 --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/response/builder/ResponseParameter.java @@ -0,0 +1,23 @@ +package ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder; + +/** + * A parameter node stored in a {@link ResponseBody}. + * + *

Represents a simple key/value pair. Callers can use {@link #rawValue()} to obtain the string + * representation of the stored value. + * + * @param key the parameter name + * @param value the parameter value + */ +public record ResponseParameter(String key, Object value) implements ResponseNode { + /** + * Returns the raw string representation of the stored value. This is a convenience wrapper + * around {@code Object#toString()} and may throw {@link NullPointerException} if the stored + * value is {@code null}. + * + * @return the value as string + */ + public String rawValue() { + return value.toString(); + } +} diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/response/dispatcher/ResponseDispatcher.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/response/dispatcher/ResponseDispatcher.java new file mode 100644 index 0000000..1be2f27 --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/response/dispatcher/ResponseDispatcher.java @@ -0,0 +1,38 @@ +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.SessionManager; + +/** + * Helper that dispatches {@link Response} instances to the corresponding {@link Session} by + * encoding them and enqueuing the resulting {@link PrimitiveResponse} into the session's response + * queue. + */ +public class ResponseDispatcher { + private final SessionManager sessionManager; + + /** + * Create a dispatcher bound to a {@link SessionManager}. + * + * @param sessionManager manager used to resolve sessions + */ + public ResponseDispatcher(SessionManager sessionManager) { + this.sessionManager = sessionManager; + } + + /** + * Encode the given {@link Response} and enqueue the resulting {@link PrimitiveResponse} into + * the target session's response queue. + * + * @param response the response to dispatch + * @throws InterruptedException if the thread is interrupted while waiting to enqueue the + * primitive response + */ + public void dispatch(Response response) throws InterruptedException { + PrimitiveResponse primitiveResponse = ResponseEncoder.encode(response); + Session session = sessionManager.getSessionById(response.getSessionId()); + session.getResponseQueue().put(primitiveResponse); + } +} diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/response/dispatcher/ResponseEncoder.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/response/dispatcher/ResponseEncoder.java new file mode 100644 index 0000000..013ddd5 --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/response/dispatcher/ResponseEncoder.java @@ -0,0 +1,104 @@ +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 + * it into a {@link PrimitiveResponse} suitable for transmission. + */ +public class ResponseEncoder { + private static final String INDENT = "\t"; + private static final String NEWLINE = "\n"; + + /** + * Encode a {@link Response} into a {@link PrimitiveResponse} containing the serialized payload + * string. + * + * @param response the response to encode + * @return a {@link PrimitiveResponse} with encoded payload + */ + public static PrimitiveResponse encode(Response response) { + StringBuilder sb = new StringBuilder(); + sb.append(response.prefix()); + + for (ResponseNode node : response.getBody().nodes()) { + sb.append(NEWLINE); + encodeNode(node, sb, 1); + } + + sb.append(NEWLINE).append("END"); + + return new PrimitiveResponse( + response.getSessionId(), response.getRequestId(), sb.toString()); + } + + /** + * Internal helper to encode any {@link ResponseNode}. + * + * @param node node to encode + * @param sb string builder to append to + * @param depth current indentation depth + */ + private static void encodeNode(ResponseNode node, StringBuilder sb, int depth) { + if (node instanceof ResponseParameter param) { + encodeParameter(param, sb, depth); + } else if (node instanceof ResponseBlock block) { + encodeBlock(block, sb, depth); + } + } + + /** + * Encode a {@link ResponseParameter} into the string builder. + * + * @param param the parameter to encode + * @param sb the output builder + * @param depth indentation depth + */ + private static void encodeParameter(ResponseParameter param, StringBuilder sb, int depth) { + sb.append(INDENT.repeat(depth)); + sb.append(param.key()); + sb.append("="); + sb.append(maskIfNeeded(param.value().toString())); + } + + /** + * Encode a {@link ResponseBlock}, including its children and terminating with an {@code END} + * marker. + * + * @param block the block to encode + * @param sb the output builder + * @param depth current indentation depth + */ + private static void encodeBlock(ResponseBlock block, StringBuilder sb, int depth) { + sb.append(INDENT.repeat(depth)); + sb.append(block.tag()); + + for (ResponseNode child : block.children()) { + sb.append(NEWLINE); + encodeNode(child, sb, depth + 1); + } + + sb.append(NEWLINE); + sb.append(INDENT.repeat(depth)); + sb.append("END"); + } + + /** + * Quote or escape the provided value if it contains whitespace or single quotes so the encoded + * payload remains parseable. + * + * @param value the raw string value + * @return quoted/escaped value + */ + private static String maskIfNeeded(String value) { + if (value.contains(" ") || value.contains("'")) { + String escaped = value.replace("'", "\\'"); + return "'" + escaped + "'"; + } + return value; + } +} diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/sessions/Session.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/sessions/Session.java index 97e3943..5f58068 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/sessions/Session.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/sessions/Session.java @@ -1,24 +1,24 @@ 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.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.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.protocol.response.PrimitiveResponse; 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; +import java.time.Instant; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; /** Represents a client session in the network server. */ -public class Session implements Runnable { - private SessionId id; - private Thread thread; - private TransportLayer transport; - private Logger logger; - private Boolean running; - private EventBus eventBus; +public class Session { + private final SessionId id; + private Instant lastActivity; + private final TransportLayer transport; + private final BlockingQueue responseQueue; + private final CommandParserDispatcher dispatcher; + private final CommandRouter router; + private static final int RESPOND_QUEUE_SIZE = 32; /** * Creates a new Session with the given transport and event bus. @@ -27,15 +27,17 @@ public class Session implements Runnable { * @param eventBus the event bus for publishing events * @throws IOException if an I/O error occurs during initialization */ - public Session(TransportLayer transport, EventBus eventBus) throws IOException { + public Session( + TransportLayer transport, + EventBus eventBus, + CommandParserDispatcher dispatcher, + CommandRouter router) { this.id = new SessionId(); - this.thread = new Thread(this, "session-" + this.id.value()); + this.lastActivity = Instant.now(); this.transport = transport; - this.running = true; - this.eventBus = eventBus; - - this.logger = LogManager.getLogger(Session.class.toString() + id.value()); - this.logger.info("Created new session"); + this.dispatcher = dispatcher; + this.router = router; + this.responseQueue = new ArrayBlockingQueue<>(RESPOND_QUEUE_SIZE); } /** @@ -47,39 +49,48 @@ public class Session implements Runnable { return this.id; } - /** Starts the session thread. */ - public void start() { - thread.start(); + /** + * 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(); } /** - * Closes the session and its transport. + * Returns the TransportLayer of this session * - * @throws IOException if an I/O error occurs + * @return the transport layer of the session */ - public void close() throws IOException { - transport.close(); - this.running = false; + public TransportLayer getTransport() { + return transport; } - /** Runs the session loop, reading from the transport. */ - @Override - public void run() { - while (running) { - try { - RawPacket rawPacket = transport.read(); - logger.debug("Recieved: {}", rawPacket); + /** + * Returns the BlockingQueue of this session + * + * @return the queue containing outgoing responses + */ + public BlockingQueue getResponseQueue() { + return responseQueue; + } - PrimitiveRequest primitiveRequest = ProtocolParser.parse(rawPacket); - logger.debug("Parsed request to {}", primitiveRequest); - } catch (EOFException e) { - logger.info("Client disconnected"); - eventBus.publish(new DisconnectEvent(id)); - break; - } catch (IOException e) { - e.printStackTrace(); - break; - } - } + /** + * Returns the CommandParserDispatcher of this session + * + * @return the dispatcher to dispatch PrimitiveRequests to for parsing + */ + public CommandParserDispatcher getDispatcher() { + return dispatcher; + } + + public CommandRouter getRouter() { + return router; } } diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/sessions/SessionDisconnectJob.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/sessions/SessionDisconnectJob.java new file mode 100644 index 0000000..6fd481d --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/sessions/SessionDisconnectJob.java @@ -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."); + } +} diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/sessions/SessionHandle.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/sessions/SessionHandle.java new file mode 100644 index 0000000..a603a8a --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/sessions/SessionHandle.java @@ -0,0 +1,4 @@ +package ch.unibas.dmi.dbis.cs108.casono.server.network.sessions; + +/** The SessionHandle stores the session and the two worker threads associated with the session */ +record SessionHandle(Session session, Thread reader, Thread writer) {} diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/sessions/SessionManager.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/sessions/SessionManager.java index 6702543..ba25cf4 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/sessions/SessionManager.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/sessions/SessionManager.java @@ -1,44 +1,99 @@ package ch.unibas.dmi.dbis.cs108.casono.server.network.sessions; +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.EventBus; +import ch.unibas.dmi.dbis.cs108.casono.server.network.transport.TransportLayer; +import java.io.IOException; +import java.util.Collection; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; /** Manages active sessions in the server. */ public class SessionManager { - private Map sessions; + private Map sessions; + private final EventBus eventBus; + private final Logger logger; + private final CommandParserDispatcher dispatcher; + private final CommandRouter router; /** Constructs a new SessionManager. */ - public SessionManager() { + public SessionManager( + EventBus eventBus, CommandParserDispatcher dispatcher, CommandRouter router) { this.sessions = new ConcurrentHashMap<>(); + this.eventBus = eventBus; + this.logger = LogManager.getLogger(SessionManager.class); + this.dispatcher = dispatcher; + this.router = router; } /** - * Adds a session to the manager. + * Create new Session from provided transport. * - * @param session the session to add + *

Will create both worker threads and start them. + * + * @param transport to create session from + * @return newly created session */ - public void addSession(Session session) { - sessions.put(session.getId(), session); + public Session create(TransportLayer transport) { + Session session = new Session(transport, eventBus, dispatcher, router); + SessionReader reader = new SessionReader(session, eventBus); + SessionWriter writer = new SessionWriter(session); + + Thread readerThread = + Thread.ofVirtual() + .name("session-" + session.getId().value() + "-reader") + .unstarted(reader); + Thread writerThread = + Thread.ofVirtual() + .name("session-" + session.getId().value() + "-writer") + .unstarted(writer); + + sessions.put(session.getId(), new SessionHandle(session, readerThread, writerThread)); + readerThread.start(); + writerThread.start(); + return session; } /** - * Removes a session by its ID. + * Disconnect specified client * - * @param id the ID of the session to remove - * @return the removed session, or null if not found + *

WARNING: Client will be uninformed about disconnect. Use with caution. + * + * @param id of the client to disconnect */ - public Session removeSession(SessionId id) { - return sessions.remove(id); + public void disconnect(SessionId id) { + SessionHandle handle = sessions.remove(id); + if (handle == null) { + logger.warn( + "Requested to disconnect client with id {}. Failed as client is not found", + id.value()); + return; + } + logger.debug("Disconnecting session {}", id.value()); + + handle.reader().interrupt(); + handle.writer().interrupt(); + try { + handle.session().getTransport().close(); + } catch (IOException e) { + logger.error("Unexpected exception while closing transport", e); + } } /** - * Removes the specified session. + * Handler for the DisconnectEvent * - * @param session the session to remove - * @return the removed session, or null if not found + * @param id of the session that disconnected */ - public Session removeSession(Session session) { - return sessions.remove(session.getId()); + public void onDisconnect(DisconnectEvent event) { + logger.debug("Recieved DisconnectEvent event for session {}", event.sessionId().value()); + + disconnect(event.sessionId()); } /** @@ -48,6 +103,16 @@ public class SessionManager { * @return the session with the specified ID, or null if not found */ public Session getSessionById(SessionId id) { - return sessions.get(id); + SessionHandle handle = sessions.get(id); + + if (handle == null) { + return null; + } + + return handle.session(); + } + + public Collection getAllSessions() { + return sessions.values().stream().map(SessionHandle::session).collect(Collectors.toList()); } } diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/sessions/SessionReader.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/sessions/SessionReader.java new file mode 100644 index 0000000..81589a0 --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/sessions/SessionReader.java @@ -0,0 +1,75 @@ +package ch.unibas.dmi.dbis.cs108.casono.server.network.sessions; + +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.EventBus; +import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.parser.ProtocolParser; +import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.parser.ProtocolParserException; +import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.parser.tokenizer.TokenizerException; +import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.PrimitiveRequest; +import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RawRequest; +import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.Request; +import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext; +import ch.unibas.dmi.dbis.cs108.casono.server.network.transport.RawPacket; +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; + +public class SessionReader implements Runnable { + private final Session session; + private final TransportLayer transport; + private final EventBus eventBus; + private final CommandParserDispatcher dispatcher; + private final CommandRouter router; + private final Logger logger; + + public SessionReader(Session session, EventBus eventBus) { + this.session = session; + this.transport = session.getTransport(); + this.eventBus = eventBus; + this.dispatcher = session.getDispatcher(); + this.router = session.getRouter(); + this.logger = + LogManager.getLogger( + SessionReader.class.toString() + "-" + session.getId().value()); + } + + @Override + public void run() { + while (!Thread.currentThread().isInterrupted()) { + RawPacket rawPacket = null; + try { + rawPacket = transport.read(); + session.updateLastInboundActivity(); + logger.debug("Recieved: {}", rawPacket); + + RawRequest rawRequest = ProtocolParser.parse(rawPacket.payload()); + logger.debug("Parsed request to {}", rawRequest); + + RequestContext requestContext = + new RequestContext(session.getId(), rawPacket.requestId()); + PrimitiveRequest primitiveRequest = + new PrimitiveRequest( + requestContext, rawRequest.command(), rawRequest.parameters()); + logger.debug("Converted to {}", primitiveRequest); + + Request request = dispatcher.parse(primitiveRequest); + + router.execute(request); + } catch (EOFException e) { + logger.info("Client disconnected"); + eventBus.publish(new DisconnectEvent(session.getId())); + break; + } catch (TokenizerException | ProtocolParserException e) { + logger.error("Error occured while parsing request. RawPacket: {}", rawPacket, e); + + // TODO: Send error response to client + } catch (IOException e) { + logger.error("Unexpected exception while reading from transport", e); + } + } + } +} diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/sessions/SessionWriter.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/sessions/SessionWriter.java new file mode 100644 index 0000000..c59abb3 --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/sessions/SessionWriter.java @@ -0,0 +1,42 @@ +package ch.unibas.dmi.dbis.cs108.casono.server.network.sessions; + +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.TransportLayer; +import java.io.IOException; +import java.util.concurrent.BlockingQueue; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class SessionWriter implements Runnable { + private final TransportLayer transport; + private final BlockingQueue queue; + private final Logger logger; + + public SessionWriter(Session session) { + this.transport = session.getTransport(); + this.queue = session.getResponseQueue(); + this.logger = + LogManager.getLogger( + SessionReader.class.toString() + "-" + session.getId().value()); + } + + public void run() { + while (!Thread.currentThread().isInterrupted()) { + RawPacket packet = null; + try { + PrimitiveResponse response = queue.take(); + packet = new RawPacket(response.requestId(), response.payload()); + transport.write(packet); + } catch (IOException e) { + logger.error( + "Unexpected exception while writing to transport. RawPacket: {}", + packet, + e); + } catch (InterruptedException e) { + logger.warn("Thread got interrupted", e); + break; + } + } + } +} diff --git a/src/test/java/ch/unibas/dmi/dbis/cs108/casono/server/tokenizer/StateTest.java b/src/test/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/parser/tokenizer/StateTest.java similarity index 95% rename from src/test/java/ch/unibas/dmi/dbis/cs108/casono/server/tokenizer/StateTest.java rename to src/test/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/parser/tokenizer/StateTest.java index fd1717b..19832cc 100644 --- a/src/test/java/ch/unibas/dmi/dbis/cs108/casono/server/tokenizer/StateTest.java +++ b/src/test/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/parser/tokenizer/StateTest.java @@ -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 static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; diff --git a/src/test/java/ch/unibas/dmi/dbis/cs108/casono/server/tokenizer/TokenizerExceptionTest.java b/src/test/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/parser/tokenizer/TokenizerExceptionTest.java similarity index 85% rename from src/test/java/ch/unibas/dmi/dbis/cs108/casono/server/tokenizer/TokenizerExceptionTest.java rename to src/test/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/parser/tokenizer/TokenizerExceptionTest.java index d161b9b..bb6bc58 100644 --- a/src/test/java/ch/unibas/dmi/dbis/cs108/casono/server/tokenizer/TokenizerExceptionTest.java +++ b/src/test/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/parser/tokenizer/TokenizerExceptionTest.java @@ -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 static org.junit.jupiter.api.Assertions.assertEquals;