Compare commits
121 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b082cbb5f2 | |||
| b10b736321 | |||
| d027696580 | |||
| 4a4af3807a | |||
| 978f54a57b | |||
| 9d4afdc3e8 | |||
| f9b866fc40 | |||
| f841eecb93 | |||
| 5ee3f34625 | |||
| f467b2861f | |||
| cb6c9cb339 | |||
| 9390c3503a | |||
| d990e37a68 | |||
| e6b62726bd | |||
| 2feb0579e2 | |||
| 160ec55957 | |||
| ed91494129 | |||
| db9ea45530 | |||
| 8d571172d5 | |||
| 1f4ccb1033 | |||
| fc4a88ee95 | |||
| 1547eda410 | |||
| c1cf6c1f83 | |||
| 8313a3b777 | |||
| 4b187fac27 | |||
| 58d06fad39 | |||
| 8ed1bab067 | |||
| a9ffb4daeb | |||
| 9a6ad9f1bb | |||
| 628e86a964 | |||
| d25f533ee9 | |||
| a520f69f3e | |||
| 52c347d233 | |||
| 2004740541 | |||
| fd17b49ed9 | |||
| d9f6838e05 | |||
| 69a5a94028 | |||
| f2f5a24bd7 | |||
| 68891815ee | |||
| 5a4cc25f19 | |||
| 9da31ac4dc | |||
| 6248935355 | |||
| 97571d6fe0 | |||
| 72ef67c47d | |||
| a772f2ce01 | |||
| c19e3f3229 | |||
| 085f97b37e | |||
| d51f080bad | |||
| 4430ddb0f3 | |||
| 92e4fcb269 | |||
| 6f1b6361bc | |||
| b0d7dc8b30 | |||
| 4234149b58 | |||
| d1510ddf8e | |||
| edc1b29884 | |||
| cdd5de620b | |||
| 34ad803073 | |||
| f4476ae1e8 | |||
| db22f12ed5 | |||
| 89753a7479 | |||
| 03b6c30625 | |||
| 9766fe52c5 | |||
| 696542003f | |||
| 676216d291 | |||
| d0b431b650 | |||
| a281f74789 | |||
| 02c82c4e95 | |||
| 796827cbb4 | |||
| e7c9063a5d | |||
| bcfbe06532 | |||
| eb63f6d39b | |||
| 0f58b4e998 | |||
| 6324c4a28c | |||
| 900bb4f26e | |||
| f3703e4dbd | |||
| 8d72a67b5e | |||
| e2e01addaa | |||
| 94fcd88223 | |||
| 0bb5a1e940 | |||
| c6833b71e8 | |||
| b19e923356 | |||
| f79f7f0ae2 | |||
| 2ed7ae4e5d | |||
| 107870ff78 | |||
| 9e8ea37e88 | |||
| 699b9053eb | |||
| 35da4d2770 | |||
| 761cf4a760 | |||
| 11a7f0036c | |||
| 7713069496 | |||
| 738b97a849 | |||
| 72da952e5a | |||
| 247f16e26e | |||
| ec5ebe5d42 | |||
| 8db7986131 | |||
| 355b1cdbd4 | |||
| c10aaf15b5 | |||
| 7b4fd4aeae | |||
| f612c0b9a1 | |||
| 918002c8cb | |||
| 4df8befd27 | |||
| 0ff4e320ba | |||
| 090d77054c | |||
| 1106c891aa | |||
| 2442e5a39f | |||
| ab82208283 | |||
| c9884bf7fa | |||
| 7ffeca75fa | |||
| 3581406833 | |||
| e1500845eb | |||
| 97c2dc6a02 | |||
| 62bfa51af5 | |||
| de4a424396 | |||
| 63a9974de2 | |||
| 039e3751bf | |||
| 5e74b83a1f | |||
| 98df1dc227 | |||
| d6468c9595 | |||
| 3809b7e0e2 | |||
| 892f0553f6 | |||
| e533262c38 |
+2
-1
@@ -121,4 +121,5 @@ $RECYCLE.BIN/
|
||||
.idea
|
||||
|
||||
## bin
|
||||
bin/
|
||||
bin/
|
||||
gradle.properties
|
||||
+10
-7
@@ -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:
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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.
|
||||
|
||||
It’s important that we look out for one another and **work together as a team**.
|
||||
+1
-1
@@ -55,7 +55,7 @@ checkstyle {
|
||||
|
||||
spotless {
|
||||
java {
|
||||
googleJavaFormat('1.25.2').aosp()
|
||||
googleJavaFormat('1.35.0').aosp()
|
||||
importOrder()
|
||||
removeUnusedImports()
|
||||
trimTrailingWhitespace()
|
||||
|
||||
@@ -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) {
|
||||
|
||||
+7
@@ -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);
|
||||
}
|
||||
+28
@@ -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);
|
||||
}
|
||||
}
|
||||
+14
@@ -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;
|
||||
}
|
||||
}
|
||||
+26
@@ -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;
|
||||
}
|
||||
}
|
||||
+27
@@ -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;
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -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) {}
|
||||
|
||||
+3
-4
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
+14
@@ -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) {}
|
||||
+113
@@ -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);
|
||||
}
|
||||
}
|
||||
+18
@@ -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;
|
||||
}
|
||||
+34
@@ -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());
|
||||
}
|
||||
}
|
||||
+12
@@ -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;
|
||||
}
|
||||
}
|
||||
+12
@@ -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 {}
|
||||
+40
@@ -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;
|
||||
}
|
||||
}
|
||||
+51
@@ -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);
|
||||
}
|
||||
}
|
||||
+36
@@ -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);
|
||||
}
|
||||
}
|
||||
+98
@@ -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;
|
||||
}
|
||||
}
|
||||
+8
@@ -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 {}
|
||||
+23
@@ -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();
|
||||
}
|
||||
}
|
||||
+34
@@ -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";
|
||||
}
|
||||
}
|
||||
+58
-47
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+40
@@ -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.");
|
||||
}
|
||||
}
|
||||
+4
@@ -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) {}
|
||||
+82
-17
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
+75
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+42
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -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) {}
|
||||
|
||||
+2
-2
@@ -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;
|
||||
|
||||
+2
-2
@@ -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() {
|
||||
|
||||
+2
-1
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user