Compare commits

...

121 Commits

Author SHA1 Message Date
Lars Simon Winzer b082cbb5f2 Ci: Update severity for checkstyle file and group rules accordingly 2026-03-30 15:48:26 +02:00
Lars Simon Winzer b10b736321 Merge branch 'chore/23-improve-speed-of-gradle-ci-jobs' into 'main'
Enable gradle configuration cache with flag in ci and fix path issue

See merge request cs108-fs26/Gruppe-13!55
2026-03-30 14:59:41 +02:00
Lars Simon Winzer d027696580 Ci: Modify gradle-cache definition to fix path issue 2026-03-30 14:50:42 +02:00
Lars Simon Winzer 4a4af3807a Ci: Enable configuration-cache via flag and warn on problems 2026-03-30 14:46:53 +02:00
Lars Simon Winzer 978f54a57b Merge branch 'feat/22-disallow-failure-of-checkstyle-mr-job' into 'main'
Disallow failure of checkstyle linter in merge requests

See merge request cs108-fs26/Gruppe-13!54
2026-03-30 14:23:48 +02:00
Lars Simon Winzer 9d4afdc3e8 Remove: Violator dummy file 2026-03-30 14:20:01 +02:00
Lars Simon Winzer f9b866fc40 Add: Violator dummy class to test pipeline behaviour 2026-03-30 14:15:11 +02:00
Lars Simon Winzer f841eecb93 Fix: disallow failure for checkstyle job for merge requests 2026-03-30 14:13:29 +02:00
Lars Simon Winzer 5ee3f34625 Merge branch 'chore/21-fix-style' into 'main'
Fix line length code quality issue from !52

See merge request cs108-fs26/Gruppe-13!53
2026-03-30 13:31:35 +02:00
Lars Simon Winzer f467b2861f Style: Line length 2026-03-30 13:27:59 +02:00
Lars Simon Winzer cb6c9cb339 Merge branch 'feat/21-requestparameteraccessor' into 'main'
RequestParameterAccessor to allow simple access to Parameters in PrimitiveRequest

See merge request cs108-fs26/Gruppe-13!52
2026-03-30 13:22:38 +02:00
Lars Simon Winzer 9390c3503a Style: Formatting by Spotless 2026-03-30 13:02:16 +02:00
Lars Simon Winzer d990e37a68 Add: parameterKey field to ParameterParseException (docstrings included) 2026-03-30 13:01:42 +02:00
Lars Simon Winzer e6b62726bd Docs: Add JavaDoc to all new interfaces, classes and methods
Co-authored-by: Copilot
2026-03-30 12:56:15 +02:00
Lars Simon Winzer 2feb0579e2 Add: RequestParameterAccessor as utility class for simplified access of parameters of PrimitiveRequest 2026-03-30 12:40:59 +02:00
Lars Simon Winzer 160ec55957 Add: ThrowingParser interface used to implicitly convert 2026-03-30 12:39:37 +02:00
Lars Simon Winzer ed91494129 Add: Exception to throw when error during parsing occurs 2026-03-30 12:38:44 +02:00
Lars Simon Winzer db9ea45530 Add: Exception to throw when required Parameter is not found 2026-03-30 12:06:52 +02:00
Lars Simon Winzer 8d571172d5 Merge branch 'feat/15-session-disconnect-job' into 'main'
Add SessionDisconnectJob to publish DisconnectEvent when Session timeouts

Closes #15

See merge request cs108-fs26/Gruppe-13!51
2026-03-28 13:17:42 +01:00
Lars Simon Winzer 1f4ccb1033 Docs: JavaDoc for Session 2026-03-28 13:12:43 +01:00
Lars Simon Winzer fc4a88ee95 Add: SessionDisconnectJob scheduling to ServerApp 2026-03-28 13:12:30 +01:00
Lars Simon Winzer 1547eda410 Add: SessionDisconnectJob to publish DisconnectEvent when client timeouts 2026-03-28 13:11:59 +01:00
Lars Simon Winzer c1cf6c1f83 Add: getAllSessions method to SessionManager 2026-03-28 13:01:43 +01:00
Lars Simon Winzer 8313a3b777 Add: lastActivity field to Session and update from SessionReader 2026-03-28 13:01:13 +01:00
Lars Simon Winzer 4b187fac27 Merge branch 'feat/14-commandrouter-and-commandhandler' into 'main'
Create CommandRouter and CommandHandler to execute request

Closes #17

See merge request cs108-fs26/Gruppe-13!50
2026-03-26 18:23:18 +01:00
Lars Simon Winzer 58d06fad39 Style: Line length in SessionManager 2026-03-26 18:10:32 +01:00
Lars Simon Winzer 8ed1bab067 Style: Put call to superclass constructor before variable assignment in UnknownRequestException constructor 2026-03-26 18:09:07 +01:00
Lars Simon Winzer a9ffb4daeb Style: Apply formatting by Spotless 2026-03-26 18:01:57 +01:00
Lars Simon Winzer 9a6ad9f1bb Add: Create CommandRouter at server startup and pass to SessionManager 2026-03-26 17:59:41 +01:00
Lars Simon Winzer 628e86a964 Add: Pass CommandRouter to Session and SessionReader 2026-03-26 17:58:25 +01:00
Lars Simon Winzer d25f533ee9 Add: Integrate CommandRouter into SessionManager for request routing 2026-03-26 17:57:00 +01:00
Lars Simon Winzer a520f69f3e Fix: Ensure matching type at compile-time 2026-03-26 17:56:22 +01:00
Lars Simon Winzer 52c347d233 Add: Modify CommandRouter to use generic CommandHandler type 2026-03-26 17:55:26 +01:00
Lars Simon Winzer 2004740541 Add: CommandHandler interface to be generic 2026-03-26 17:53:19 +01:00
Lars Simon Winzer fd17b49ed9 Add: CommandRouter to route Request to matching CommandHandler 2026-03-26 17:51:32 +01:00
Lars Simon Winzer d9f6838e05 Add: UnknownRequestException class to throw when encountering unknown request type 2026-03-26 17:20:25 +01:00
Lars Simon Winzer 69a5a94028 Add: CommandHandler interface for executing requests 2026-03-26 17:09:33 +01:00
Lars Simon Winzer f2f5a24bd7 Merge branch 'docs/19-create-contributing-file' into 'main'
Create CONTRIBUTING.md with guidelines and workflows

Closes #19

See merge request cs108-fs26/Gruppe-13!49
2026-03-26 14:18:35 +01:00
Lars Simon Winzer 68891815ee Docs: Create CONTRIBUTING.md with guidelines and workflows 2026-03-26 14:15:42 +01:00
Lars Simon Winzer 5a4cc25f19 Merge branch 'fix/18-resolve-issues-introduced-in-16' into 'main'
Resolve issues introduced to issue templates in last update

Closes #18

See merge request cs108-fs26/Gruppe-13!48
2026-03-26 13:19:25 +01:00
Lars Simon Winzer 9da31ac4dc Remove: Unnecessary checklist item introduced by copy & pasting from bug report issue template 2026-03-26 13:12:51 +01:00
Lars Simon Winzer 6248935355 Add: Whitespaces after environment description of bug report issue template 2026-03-26 13:11:36 +01:00
Lars Simon Winzer 97571d6fe0 Merge branch 'feat/16-rework-issue-templates-to-better-match-our-usecase' into 'main'
Rework issue templates to better match our usecase

See merge request cs108-fs26/Gruppe-13!47
2026-03-26 13:00:47 +01:00
Lars Simon Winzer 72ef67c47d Rework: Task issue template 2026-03-26 12:56:54 +01:00
Lars Simon Winzer a772f2ce01 Remove: Protocol change issue template as it is now incorporated into the others 2026-03-26 12:53:52 +01:00
Lars Simon Winzer c19e3f3229 Rework: Feature request issue templates 2026-03-26 12:52:59 +01:00
Lars Simon Winzer 085f97b37e Rework: Bug report issue templates 2026-03-26 12:48:16 +01:00
Lars Simon Winzer d51f080bad Merge branch 'feat/use-parser-dispatcher-in-reader' into 'main'
Made SessionReader invoke parsing of request with CommandParserDispatcher uppon recieving request

Closes #13

See merge request cs108-fs26/Gruppe-13!46
2026-03-25 19:39:14 +01:00
Lars Simon Winzer 4430ddb0f3 Add: Pass CommandParserDispatcher instance to session upon creation 2026-03-25 13:50:54 +01:00
Lars Simon Winzer 92e4fcb269 Add: CommandParserDispatcher field to Session and SessionReader 2026-03-25 13:50:35 +01:00
Lars Simon Winzer 6f1b6361bc Add: Create CommandParserDispatcher on startup and pass to SessionManager 2026-03-25 13:48:30 +01:00
Lars Simon Winzer b0d7dc8b30 Add: CommandParserDispatcher as field to SessionManager 2026-03-25 13:47:54 +01:00
Lars Simon Winzer 4234149b58 Merge branch 'feat/request-context-record' into 'main'
Add RequestContext to simplify creation of Request which requires SessionId and RequestId

Closes #12

See merge request cs108-fs26/Gruppe-13!45
2026-03-24 18:29:28 +01:00
Lars Simon Winzer d1510ddf8e Docs: Write JavaDoc for RequestContext 2026-03-24 18:12:02 +01:00
Lars Simon Winzer edc1b29884 Add: PrimitiveRequest takes now RequestContext instead of requestId 2026-03-24 18:11:37 +01:00
Lars Simon Winzer cdd5de620b Add: Create PrimitiveRequest from RawRequest with RequestContext 2026-03-24 18:10:15 +01:00
Lars Simon Winzer 34ad803073 Refactor: ProtocolParser to accept payload string and return RawRequest
Prior it took the RawPacket as an argument and returned an PrimitiveRequest
2026-03-24 18:07:20 +01:00
Lars Simon Winzer f4476ae1e8 Add: RawRequest record to contain command and parameters 2026-03-24 18:03:26 +01:00
Lars Simon Winzer db22f12ed5 Add: RequestContext field to abstract Request class
BREAKING CHANGE: Request is now an abstract class instead of an interface
2026-03-24 18:00:09 +01:00
Lars Simon Winzer 89753a7479 Add: RequestContext record 2026-03-24 17:58:21 +01:00
Lars Simon Winzer 03b6c30625 Merge branch 'feat/serverside-response' into 'main'
Create components to respond to client request

Closes #8

See merge request cs108-fs26/Gruppe-13!44
2026-03-24 15:02:16 +01:00
Lars Simon Winzer 9766fe52c5 Add: OkResponse class as default for successful response with no body 2026-03-24 14:34:59 +01:00
Lars Simon Winzer 696542003f Add: Detailed JavaDoc documentation for response related components
Disclaimer: The docstrings have been written with Copilot but fact-checked by me.
2026-03-24 14:33:46 +01:00
Lars Simon Winzer 676216d291 Add: ResponseDispatcher for handling response dispatching 2026-03-24 13:49:24 +01:00
Lars Simon Winzer d0b431b650 Fix: Escape single quote when masking string 2026-03-24 13:40:34 +01:00
Lars Simon Winzer a281f74789 Add: ResponseEncoder to encode Response to PrimitiveResponse 2026-03-24 13:39:06 +01:00
Lars Simon Winzer 02c82c4e95 Refactor: Responses to use new ResponseBody 2026-03-24 13:11:25 +01:00
Lars Simon Winzer 796827cbb4 Add: ResponseBody and ResponseBodyBuilder classes for building response body 2026-03-24 12:52:35 +01:00
Lars Simon Winzer e7c9063a5d Remove: Encodable as encode-method is no longer used 2026-03-24 12:33:36 +01:00
Lars Simon Winzer bcfbe06532 Add: ResponseNode and implementations to store body of response 2026-03-24 12:19:25 +01:00
Lars Simon Winzer eb63f6d39b Add: Base classes for all responses 2026-03-23 15:45:44 +01:00
Lars Simon Winzer 0f58b4e998 Add: Encodable interface as lowest instance 2026-03-23 15:44:58 +01:00
Lars Simon Winzer 6324c4a28c Merge branch 'main' into 'feat/serverside-response' 2026-03-23 10:41:19 +01:00
Lars Simon Winzer 900bb4f26e Merge branch 'issue/11-bump-spotless-version' into 'main'
Bumb google-java-format version from '1.25.2' to '1.35.0'

Closes #11

See merge request cs108-fs26/Gruppe-13!43
2026-03-23 10:33:19 +01:00
Lars Simon Winzer f3703e4dbd Build: Bumb google-java-format version from '1.25.2' to '1.35.0' 2026-03-23 10:28:28 +01:00
Lars Simon Winzer 8d72a67b5e Merge branch 'feat/session-reader-and-writer-thread' into 'main'
Add SessionReader and SessionWriter worker threads for session

Closes #9

See merge request cs108-fs26/Gruppe-13!42
2026-03-20 17:39:44 +01:00
Lars Simon Winzer e2e01addaa Fix: Leak of SessionManager by removing both removeSession method 2026-03-20 17:32:16 +01:00
Lars Simon Winzer 94fcd88223 Fix: Avoid Race condition in create method of SessionManager 2026-03-20 17:31:44 +01:00
Lars Simon Winzer 0bb5a1e940 Fix: Remove session requested to be disconnected from sessions map 2026-03-20 17:21:36 +01:00
Lars Simon Winzer c6833b71e8 Style: Add missing 'static' keyword to static variable 2026-03-20 17:12:55 +01:00
Lars Simon Winzer b19e923356 Style: Magic number, linelength and trailing whitespace 2026-03-20 17:09:59 +01:00
Lars Simon Winzer f79f7f0ae2 Add: Useful information to log statement to ease debugging 2026-03-20 16:56:03 +01:00
Lars Simon Winzer 2ed7ae4e5d Fix: Replace logging level 'trace' with 'error' and 'warn' 2026-03-20 16:47:01 +01:00
Lars Simon Winzer 107870ff78 Remove: eventBus argument from NetworkManager creation 2026-03-20 16:46:26 +01:00
Lars Simon Winzer 9e8ea37e88 Remove: DisconnectEvent handler from NetworkManager 2026-03-20 16:37:46 +01:00
Lars Simon Winzer 699b9053eb Add: Instantiate SessionManager with provided eventManager and correct DisconnectEvent handler 2026-03-20 16:31:26 +01:00
Lars Simon Winzer 35da4d2770 Add: Make NetworkManager use SessionManager to create session 2026-03-20 16:30:38 +01:00
Lars Simon Winzer 761cf4a760 Add: DisconnectEvent handler function to SessionManager 2026-03-20 16:30:06 +01:00
Lars Simon Winzer 11a7f0036c Add: create and disconnect methods to SessionManager 2026-03-20 16:28:33 +01:00
Lars Simon Winzer 7713069496 Add: Make SessionManager use SessionHandle 2026-03-20 16:22:47 +01:00
Jona Walpert 738b97a849 Merge branch 'chore/gradle-properties-to-gitignore' into 'main'
Add: Gradle properties to gitignore

See merge request cs108-fs26/Gruppe-13!41
2026-03-20 10:47:02 +00:00
Jona Walpert 72da952e5a Add: Gradle properties to gitignore 2026-03-20 11:44:26 +01:00
Lars Simon Winzer 247f16e26e Add: SessionHandle to store Session and two worker threads 2026-03-19 19:01:33 +01:00
Lars Simon Winzer ec5ebe5d42 Add: SessionWriter to use queue of session and write to transport 2026-03-19 18:42:34 +01:00
Lars Simon Winzer 8db7986131 Add: PrimitiveResponse as pendant to PrimitiveRequest 2026-03-19 18:42:10 +01:00
Lars Simon Winzer 355b1cdbd4 Add: Queue to Session 2026-03-19 18:32:57 +01:00
Lars Simon Winzer c10aaf15b5 Add: Move read logic to SessionReader runnable 2026-03-19 18:09:31 +01:00
Lars Simon Winzer 7b4fd4aeae Add: Make TransportLayer accessible through get-method 2026-03-19 17:41:45 +01:00
Lars Simon Winzer f612c0b9a1 Merge branch 'chore/7-gitlab-issue-templates' into 'main'
Creation of GitLab issue templates

Closes #7

See merge request cs108-fs26/Gruppe-13!40
2026-03-19 15:57:23 +01:00
Lars Simon Winzer 918002c8cb Remove: Automatic assignment of 'server' label to protocol_change issue 2026-03-19 15:53:58 +01:00
Lars Simon Winzer 4df8befd27 Style: Cleanup and addition of checklist 2026-03-19 15:51:02 +01:00
Lars Simon Winzer 0ff4e320ba Add: Task template 2026-03-19 15:48:11 +01:00
Lars Simon Winzer 090d77054c Add: Protocol schema modification template 2026-03-19 15:46:25 +01:00
Lars Simon Winzer 1106c891aa Add: Feature request template 2026-03-19 15:33:43 +01:00
Lars Simon Winzer 2442e5a39f Add: Bug report template 2026-03-19 15:29:47 +01:00
Lars Simon Winzer ab82208283 Merge branch 'chore/followup-to-mr-38' into 'main'
Conform to checkstyle

See merge request cs108-fs26/Gruppe-13!39
2026-03-18 18:32:48 +01:00
Lars Simon Winzer c9884bf7fa Style: Comment length 2026-03-18 18:28:34 +01:00
Lars Simon Winzer 7ffeca75fa Style: Import order 2026-03-18 18:28:16 +01:00
Lars Simon Winzer 3581406833 Merge branch 'issue/2-inconsistency-of-linting-between-checkstyle-and-spotless' into 'main'
Align Checkstyle rules with Spotless

Closes #2

See merge request cs108-fs26/Gruppe-13!38
2026-03-18 18:23:45 +01:00
Lars Simon Winzer e1500845eb Remove: Indentation module, as Checkstyle can not match Spotless
Explanation: Spotless uses the context to calculate the required indentation
2026-03-18 17:41:38 +01:00
Lars Simon Winzer 97c2dc6a02 Docs: Add comment to LineLength regarding difference between AOSP and Google Java Style Guide 2026-03-18 17:36:53 +01:00
Lars Simon Winzer 62bfa51af5 Add: Change ImportOrder in Checkstyle to match configured styleguide in Spotless 2026-03-18 17:34:33 +01:00
Lars Simon Winzer de4a424396 Merge branch 'main' into 'issue/2-inconsistency-of-linting-between-checkstyle-and-spotless' 2026-03-18 17:23:34 +01:00
Lars Simon Winzer 63a9974de2 Merge branch 'revert-5e74b83a' into 'main'
Revert "Merge branch 'feat/client-network' into 'main'"

See merge request cs108-fs26/Gruppe-13!37
2026-03-18 17:11:14 +01:00
Lars Simon Winzer 039e3751bf Revert "Merge branch 'feat/client-network' into 'main'"
This reverts merge request !36
2026-03-18 17:08:19 +01:00
Mathis Ginkel 5e74b83a1f Merge branch 'feat/client-network' into 'main'
Feat/client network

See merge request cs108-fs26/Gruppe-13!36
2026-03-16 13:04:24 +00:00
Mathis Ginkel 98df1dc227 add: first implementation of login 2026-03-15 22:26:19 +01:00
Mathis Ginkel d6468c9595 add: first version of client to client chat 2026-03-15 22:26:11 +01:00
ginkelmath 3809b7e0e2 Fix: update clientservice 2026-03-15 22:17:18 +01:00
ginkelmath 892f0553f6 Change protocol implementation 2026-03-15 22:17:18 +01:00
ginkelmath e533262c38 Add: First version of a Client-side Networking Service 2026-03-15 22:17:18 +01:00
45 changed files with 1473 additions and 204 deletions
+2 -1
View File
@@ -121,4 +121,5 @@ $RECYCLE.BIN/
.idea
## bin
bin/
bin/
gradle.properties
+10 -7
View File
@@ -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:
+40
View File
@@ -0,0 +1,40 @@
## Bug Report
<!-- The reccommended type is: Issue -->
### Environment
- **Branch & Commit:**
- **Operating system:**
- **How was execution started (Gradle task / IDE debug / IDE run):**
- **Java version:** `25`
- **Gradle version**: `9.3.1`
### Summary
<!-- Short, precise description of the bug -->
### Expected Behavior
<!-- What you expected to happen -->
### Actual Behavior
<!-- What actually happened instead -->
### Steps to Reproduce
<!--
1. Do this first
2. Then do that next
3. Lastly, see the unexprected ...
-->
### Stack Trace / Error Message
```
<!-- Paste stack trace or log output here -->
```
### Possible Cause / Notes
<!-- Optional: your own hypothesis about the root cause -->
### Checklist
- [ ] I reproduced the problem using the steps above
- [ ] I searched documentation docs for relevant information
- [ ] I added relevant labels
/label ~bug
@@ -0,0 +1,18 @@
## Feature Request
<!-- The reccommended type is: Task -->
### Summary
<!-- Brief description of the desired functionality -->
### Required workarround
<!-- Workarround required to achieve x (if applicable) -->
### Description
<!-- Detailed description of the desired behavior -->
### Checklist
- [ ] I reproduced the problem using the steps above
- [ ] I searched documentation docs for relevant information
- [ ] I added relevant labels
/label ~enhancement
+24
View File
@@ -0,0 +1,24 @@
## Task
<!-- The reccommended type is: Task -->
### Summary
<!-- What needs to be implemented? -->
### Context
<!-- Which part of the system does it belong to? -->
<!-- Examples: Networking, Game-Engine, User interface, ... -->
### Current Progress
- [ ]
- [ ]
- [ ]
### Notes
<!-- Optional: Implementation hints, links to prior design discussions, etc. -->
### 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
+208
View File
@@ -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>/<short-description>
```
| 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:
```
<type>: <short summary>
[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.
Its important that we look out for one another and **work together as a team**.
+1 -1
View File
@@ -55,7 +55,7 @@ checkstyle {
spotless {
java {
googleJavaFormat('1.25.2').aosp()
googleJavaFormat('1.35.0').aosp()
importOrder()
removeUnusedImports()
trimTrailingWhitespace()
+130 -90
View File
@@ -16,20 +16,49 @@
<property name="file" value="${suppressionFile}"/>
</module>
<!-- ERROR — Correctness & Bugs -->
<!-- Violations that can cause runtime errors or directly affect correctness. -->
<!-- Only allow spaces -->
<!-- https://checkstyle.sourceforge.io/checks/whitespace/filetabcharacter.html -->
<module name="FileTabCharacter"/>
<!-- Maximale Zeilenlänge -->
<!-- https://checkstyle.sourceforge.io/checks/sizes/linelength.html -->
<module name="LineLength">
<property name="max" value="100"/>
</module>
<module name="TreeWalker">
<!-- Allows for emptylines BETWEEN methods and classes but DISALLOWS newlines after an opening bracket -->
<!-- https://checkstyle.sourceforge.io/checks/whitespace/emptylineseparator.html -->
<!-- No wildcard imports (import x.*) -->
<!-- https://checkstyle.sourceforge.io/checks/imports/avoidstarimport.html -->
<module name="AvoidStarImport"/>
<!-- No unused imports -->
<!-- https://checkstyle.sourceforge.io/checks/imports/unusedimports.html -->
<module name="UnusedImports"/>
<!-- Require string literals to be compared with .equals and not == -->
<!-- https://checkstyle.sourceforge.io/checks/coding/stringliteralequality.html -->
<module name="StringLiteralEquality"/>
<!-- Disallow empty code blocks after catch -->
<!-- https://checkstyle.sourceforge.io/checks/blocks/emptycatchblock.html -->
<module name="EmptyCatchBlock">
<property name="exceptionVariableName" value="expected|ignored"/>
</module>
<!-- Disallow System.out.println -->
<!-- https://checkstyle.sourceforge.io/checks/regexp/regexp.html -->
<module name="Regexp">
<property name="id" value="SystemOutErr"/>
<property name="format" value="System\.(out|err)\.print"/>
<property name="illegalPattern" value="true"/>
<property name="message" value="No `System.out.print(err)` allowed — use a logger."/>
</module>
<!-- WARNING — Style & Conventions -->
<!-- Violations against agreed conventions that reduce readability and maintainability. -->
<module name="EmptyLineSeparator">
<property name="severity" value="warning"/>
<!-- Allows for emptylines BETWEEN methods and classes but DISALLOWS newlines after an opening bracket -->
<!-- https://checkstyle.sourceforge.io/checks/whitespace/emptylineseparator.html -->
<property name="allowNoEmptyLineBetweenFields" value="true"/>
<property name="tokens" value="
METHOD_DEF,
@@ -42,57 +71,107 @@
"/>
</module>
<!-- Enforce indentation of four whitespaces -->
<!-- https://checkstyle.sourceforge.io/checks/misc/indentation.html -->
<module name="Indentation">
<property name="basicOffset" value="4"/>
<property name="caseIndent" value="4"/>
<property name="throwsIndent" value="4"/>
<property name="arrayInitIndent" value="4"/>
<property name="lineWrappingIndentation" value="8"/>
</module>
<!-- No wildcard imports (import x.*) -->
<!-- https://checkstyle.sourceforge.io/checks/imports/avoidstarimport.html -->
<module name="AvoidStarImport"/>
<!-- No unused imports -->
<!-- https://checkstyle.sourceforge.io/checks/imports/unusedimports.html -->
<module name="UnusedImports"/>
<!-- No unordered / ungrouped imports -->
<!-- https://checkstyle.sourceforge.io/checks/imports/importorder.html -->
<module name="ImportOrder"/>
<module name="ImportOrder">
<property name="severity" value="warning"/>
<property name="option" value="top"/>
<property name="groups" value="/^import static\..+/,*"/>
<property name="separated" value="true"/>
<property name="separatedStaticGroups" value="true"/>
<property name="sortStaticImportsAlphabetically" value="true"/>
</module>
<!-- Classes, enums, records, ... as PascalCase -->
<!-- https://checkstyle.sourceforge.io/checks/naming/typename.html -->
<module name="TypeName"/>
<module name="TypeName">
<property name="severity" value="warning"/>
</module>
<!-- Method names as camelCase -->
<!-- https://checkstyle.sourceforge.io/checks/naming/methodname.html -->
<module name="MethodName"/>
<module name="MethodName">
<property name="severity" value="warning"/>
</module>
<!-- Parameter names as camelCase -->
<!-- https://checkstyle.sourceforge.io/checks/naming/parametername.html -->
<module name="ParameterName"/>
<module name="ParameterName">
<property name="severity" value="warning"/>
</module>
<!-- Lokal variables as camelCase -->
<!-- https://checkstyle.sourceforge.io/checks/naming/localvariablename.html -->
<module name="LocalVariableName"/>
<module name="LocalVariableName">
<property name="severity" value="warning"/>
</module>
<!-- Constants as UPPER_SNAKE_CASE -->
<!-- https://checkstyle.sourceforge.io/checks/naming/constantname.html -->
<module name="ConstantName"/>
<module name="ConstantName">
<property name="severity" value="warning"/>
</module>
<!-- Field names as camelCase -->
<!-- https://checkstyle.sourceforge.io/checks/naming/membername.html -->
<module name="MemberName">
<property name="severity" value="warning"/>
<property name="format" value="^[a-z][a-zA-Z0-9]*$"/>
</module>
<!-- Modifier-Reihenfolge: public static final ... -->
<!-- https://checkstyle.sourceforge.io/checks/modifier/modifierorder.html -->
<module name="ModifierOrder">
<property name="severity" value="warning"/>
</module>
<!-- Require braces arround code block (no single instruction after if/else/while/...)-->
<!-- https://checkstyle.sourceforge.io/checks/blocks/needbraces.html -->
<module name="NeedBraces">
<property name="severity" value="warning"/>
</module>
<!-- Require the left curly braces at the same line -->
<!-- https://checkstyle.sourceforge.io/checks/blocks/leftcurly.html -->
<module name="LeftCurly">
<property name="severity" value="warning"/>
</module>
<!-- Require right curly braces at the same line as following instructions -->
<!-- https://checkstyle.sourceforge.io/checks/blocks/rightcurly.html -->
<module name="RightCurly">
<property name="severity" value="warning"/>
</module>
<!-- Disallow whitespace before semicolon or bracket -->
<!-- https://checkstyle.sourceforge.io/checks/whitespace/nowhitespacebefore.html -->
<module name="NoWhitespaceBefore">
<property name="severity" value="warning"/>
</module>
<!-- Disallow whitespace after code -->
<!-- https://checkstyle.sourceforge.io/checks/whitespace/whitespaceafter.html -->
<module name="WhitespaceAfter">
<property name="severity" value="warning"/>
</module>
<!-- No trailing whitespace at line ending -->
<!-- https://checkstyle.sourceforge.io/checks/regexp/regexp.html -->
<module name="Regexp">
<property name="severity" value="warning"/>
<property name="format" value=" +$"/>
<property name="illegalPattern" value="true"/>
<property name="message" value="No trailing whitespace."/>
</module>
<!-- INFO — Complexity Limits -->
<!-- Metrics that serve as guidance but may legitimately be exceeded depending on context. -->
<!-- Restrict length of method name -->
<!-- https://checkstyle.sourceforge.io/checks/sizes/methodlength.html -->
<module name="MethodLength">
<property name="severity" value="info"/>
<property name="max" value="60"/>
<property name="countEmpty" value="false"/>
</module>
@@ -100,75 +179,36 @@
<!-- Restrict number of parameters -->
<!-- https://checkstyle.sourceforge.io/checks/sizes/parameternumber.html -->
<module name="ParameterNumber">
<property name="severity" value="info"/>
<property name="max" value="5"/>
</module>
<!-- Modifier-Reihenfolge: public static final ... -->
<!-- https://checkstyle.sourceforge.io/checks/modifier/modifierorder.html -->
<module name="ModifierOrder"/>
<!-- Require braces arround code block (no single instruction after if/else/while/...)-->
<!-- https://checkstyle.sourceforge.io/checks/blocks/needbraces.html -->
<module name="NeedBraces"/>
<!-- Require the left curly braces at the same line -->
<!-- https://checkstyle.sourceforge.io/checks/blocks/leftcurly.html -->
<module name="LeftCurly"/>
<!-- Require right curly braces at the same line as following instructions -->
<!-- https://checkstyle.sourceforge.io/checks/blocks/rightcurly.html -->
<module name="RightCurly"/>
<!-- Disallow empty code blocks -->
<!-- https://checkstyle.sourceforge.io/checks/blocks/emptyblock.html -->
<module name="EmptyBlock">
<property name="option" value="text"/>
</module>
<!-- Disallow empty code blocks after catch -->
<!-- https://checkstyle.sourceforge.io/checks/blocks/emptycatchblock.html -->
<module name="EmptyCatchBlock">
<property name="exceptionVariableName" value="expected|ignored"/>
</module>
<!-- Require string literals to be compared with .equals and not == -->
<!-- https://checkstyle.sourceforge.io/checks/coding/stringliteralequality.html -->
<module name="StringLiteralEquality"/>
<!-- Disallow standalone numbers in code -->
<!-- https://checkstyle.sourceforge.io/checks/coding/magicnumber.html -->
<module name="MagicNumber">
<property name="severity" value="info"/>
<property name="ignoreNumbers" value="-1, 0, 1, 2"/>
<property name="ignoreAnnotation" value="true"/>
</module>
<!-- Disallow empty code blocks -->
<!-- https://checkstyle.sourceforge.io/checks/blocks/emptyblock.html -->
<module name="EmptyBlock">
<property name="severity" value="info"/>
<property name="option" value="text"/>
</module>
<!-- Disallow nested code blocks dangling in code -->
<!-- https://checkstyle.sourceforge.io/checks/blocks/avoidnestedblocks.html -->
<module name="AvoidNestedBlocks"/>
<!-- Disallow System.out.println -->
<!-- https://checkstyle.sourceforge.io/checks/regexp/regexp.html -->
<module name="Regexp">
<property name="id" value="SystemOutErr"/>
<property name="format" value="System\.(out|err)\.print"/>
<property name="illegalPattern" value="true"/>
<property name="message" value="No `System.out.print(err)` allowed — use a logger."/>
</module>
<!-- Disallow whitespace before semicolon or bracket -->
<!-- https://checkstyle.sourceforge.io/checks/whitespace/nowhitespacebefore.html -->
<module name="NoWhitespaceBefore"/>
<!-- Disallow whitespace after code -->
<!-- https://checkstyle.sourceforge.io/checks/whitespace/whitespaceafter.html -->
<module name="WhitespaceAfter"/>
<!-- No trailing whitespace at line ending -->
<!-- https://checkstyle.sourceforge.io/checks/regexp/regexp.html -->
<module name="Regexp">
<property name="format" value=" +$"/>
<property name="illegalPattern" value="true"/>
<property name="message" value="No trailing whitespace."/>
<module name="AvoidNestedBlocks">
<property name="severity" value="info"/>
</module>
</module>
</module>
<!-- Maximale Zeilenlänge (AOSP enforces 100 lines, the Google Java Style Guide 80)-->
<!-- https://checkstyle.sourceforge.io/checks/sizes/linelength.html -->
<module name="LineLength">
<property name="severity" value="info"/>
<property name="max" value="100"/>
</module>
</module>
@@ -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.CommandRouter;
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.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 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();
}
@@ -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) {
@@ -0,0 +1,7 @@
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,28 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.command;
import ch.unibas.dmi.dbis.cs108.casono.server.network.parser.Request;
import java.util.HashMap;
import java.util.Map;
public class CommandRouter {
private final Map<Class<? extends Request>, CommandHandler<?>> handlers = new HashMap<>();
public <T extends Request> void register(Class<T> request, CommandHandler<T> 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<Request> handler =
(CommandHandler<Request>) 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);
}
}
@@ -0,0 +1,14 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.command;
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;
}
}
@@ -0,0 +1,26 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.parser;
/** 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.parser;
/** 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;
}
}
@@ -3,4 +3,5 @@ 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<Parameter> parameters) {}
public record PrimitiveRequest(
RequestContext context, String command, List<Parameter> parameters) {}
@@ -1,6 +1,5 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.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;
@@ -19,15 +18,15 @@ public class ProtocolParser {
* @param packet the RawPacket containing the recieved data
* @return created PrimitiveRequest
*/
public static PrimitiveRequest parse(RawPacket packet) {
List<RawToken> rawTokens = Tokenizer.tokenize(packet.payload());
public static RawRequest parse(String payload) {
List<RawToken> rawTokens = Tokenizer.tokenize(payload);
List<Token> tokens = TokenClassifier.classify(rawTokens);
Iterator<Token> iterator = tokens.iterator();
String command = readCommand(iterator);
List<Parameter> parameters = readParameters(iterator);
return new PrimitiveRequest(packet.requestId(), command, parameters);
return new RawRequest(command, parameters);
}
/**
@@ -0,0 +1,5 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.parser;
import java.util.List;
public record RawRequest(String command, List<Parameter> parameters) {}
@@ -1,4 +1,24 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.parser;
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionId;
/** Request, produced by the CommandParser */
public interface Request {}
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();
}
}
@@ -0,0 +1,14 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.parser;
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionId;
/**
* Immutable context for a network request.
*
* <p>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) {}
@@ -0,0 +1,113 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.parser;
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<Parameter> parameters) {
this.index =
parameters.stream()
.collect(Collectors.toUnmodifiableMap(Parameter::key, Parameter::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.parser;
/**
* 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;
}
@@ -0,0 +1,34 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.response;
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}
*
* <p>This implementation returns the fixed {@code -ERR} prefix.
*
* @return the {@code -ERR} prefix
*/
@Override
public String prefix() {
return "-ERR";
}
}
@@ -0,0 +1,20 @@
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());
}
}
@@ -0,0 +1,12 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.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) {}
@@ -0,0 +1,58 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.response;
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;
}
}
@@ -0,0 +1,12 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.response;
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<ResponseNode> children) implements ResponseNode {}
@@ -0,0 +1,40 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.response;
import java.util.List;
/**
* Immutable container for the structured content of a {@link Response}.
*
* <p>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<ResponseNode> nodes;
/**
* Package-private constructor used by {@link ResponseBodyBuilder}.
*
* @param nodes the list of response nodes to include in this body
*/
ResponseBody(List<ResponseNode> 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<ResponseNode> nodes() {
return nodes;
}
}
@@ -0,0 +1,51 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.response;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
/**
* Builder for {@link ResponseBody} instances.
*
* <p>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<ResponseNode> 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<ResponseBodyBuilder> 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);
}
}
@@ -0,0 +1,36 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.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);
}
}
@@ -0,0 +1,98 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.response;
/**
* 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;
}
}
@@ -0,0 +1,8 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.response;
/**
* Marker interface for elements that may appear in a {@link ResponseBody}.
*
* <p>Implementations include {@link ResponseParameter} and {@link ResponseBlock}.
*/
public interface ResponseNode {}
@@ -0,0 +1,23 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.response;
/**
* A parameter node stored in a {@link ResponseBody}.
*
* <p>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();
}
}
@@ -0,0 +1,34 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.response;
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionId;
/**
* Abstract {@link Response} specialization indicating a successful outcome.
*
* <p>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}
*
* <p>This implementation returns the fixed {@code +OK} prefix.
*
* @return the {@code +OK} prefix
*/
@Override
public final String prefix() {
return "+OK";
}
}
@@ -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.CommandRouter;
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.parser.CommandParserDispatcher;
import ch.unibas.dmi.dbis.cs108.casono.server.network.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<PrimitiveResponse> 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<PrimitiveResponse> 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;
}
}
@@ -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.");
}
}
@@ -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) {}
@@ -1,44 +1,99 @@
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.events.DisconnectEvent;
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 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<SessionId, Session> sessions;
private Map<SessionId, SessionHandle> 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
* <p>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
* <p>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<Session> getAllSessions() {
return sessions.values().stream().map(SessionHandle::session).collect(Collectors.toList());
}
}
@@ -0,0 +1,75 @@
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.events.DisconnectEvent;
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.parser.PrimitiveRequest;
import ch.unibas.dmi.dbis.cs108.casono.server.network.parser.ProtocolParser;
import ch.unibas.dmi.dbis.cs108.casono.server.network.parser.ProtocolParserException;
import ch.unibas.dmi.dbis.cs108.casono.server.network.parser.RawRequest;
import ch.unibas.dmi.dbis.cs108.casono.server.network.parser.Request;
import ch.unibas.dmi.dbis.cs108.casono.server.network.parser.RequestContext;
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.tokenizer.TokenizerException;
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);
}
}
}
}
@@ -0,0 +1,42 @@
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.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<PrimitiveResponse> 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;
}
}
}
}
@@ -1,7 +1,7 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.transport;
/**
* Transport object, created by the transport layer to store raw data about the request
* prior to any processing
* Transport object, created by the transport layer to store raw data about the request prior to any
* processing
*/
public record RawPacket(int requestId, String payload) {}
@@ -1,9 +1,9 @@
package ch.unibas.dmi.dbis.cs108.casono.client.ui.lobbyui;
import static org.junit.jupiter.api.Assertions.*;
import javafx.scene.layout.GridPane;
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
class LobbyButtonGridManagerTest {
LobbyButtonGridManager gridManager;
@@ -1,9 +1,9 @@
package ch.unibas.dmi.dbis.cs108.casono.client.ui.lobbyui;
import static org.junit.jupiter.api.Assertions.*;
import java.util.Map;
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
class LobbyButtonTranslationManagerTest {
LobbyButtonTranslationManager manager;
@@ -1,10 +1,11 @@
package ch.unibas.dmi.dbis.cs108.casono.server.tokenizer;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.Test;
class StateTest {
@Test
void testConstructor() {
@@ -1,8 +1,9 @@
package ch.unibas.dmi.dbis.cs108.casono.server.tokenizer;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;
class TokenizerExceptionTest {
@Test
void testConstructorAndGetters() {