Fix: Resolving Git Merge Conflicts #253
@@ -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
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
## Bug Report
|
|
||||||
|
|
||||||
### Summary
|
|
||||||
<!-- Short, precise description of the bug -->
|
|
||||||
|
|
||||||
### Affected Component
|
|
||||||
<!-- e.g. Lobby Menu, Global Chat, Loading screen, ... -->
|
|
||||||
|
|
||||||
### Steps to Reproduce
|
|
||||||
1. Do this first
|
|
||||||
2. Then do that next
|
|
||||||
3. Lastly, see the unexprected error message ...
|
|
||||||
|
|
||||||
### Expected Behavior
|
|
||||||
<!-- What should happen? -->
|
|
||||||
|
|
||||||
### Actual Behavior
|
|
||||||
<!-- What happens instead? -->
|
|
||||||
|
|
||||||
### Stack Trace / Error Message
|
|
||||||
```
|
|
||||||
<!-- Paste stack trace or log output here -->
|
|
||||||
```
|
|
||||||
|
|
||||||
### Possible Cause
|
|
||||||
<!-- Optional: your own hypothesis about the root cause -->
|
|
||||||
|
|
||||||
|
|
||||||
/label ~bug
|
|
||||||
|
|
||||||
/label ~client
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
## Bug Report
|
|
||||||
|
|
||||||
### Summary
|
|
||||||
<!-- Short, precise description of the bug -->
|
|
||||||
|
|
||||||
### Affected Component
|
|
||||||
<!-- e.g. TransportLayer, CommandRouter, UserRegistry, Response, ... -->
|
|
||||||
|
|
||||||
### Steps to Reproduce
|
|
||||||
1. Do this first
|
|
||||||
2. Then do that next
|
|
||||||
3. Lastly, see the unexprected error message ...
|
|
||||||
|
|
||||||
### Expected Behavior
|
|
||||||
<!-- What should happen? -->
|
|
||||||
|
|
||||||
### Actual Behavior
|
|
||||||
<!-- What happens instead? -->
|
|
||||||
|
|
||||||
### Stack Trace / Error Message
|
|
||||||
```
|
|
||||||
<!-- Paste stack trace or log output here -->
|
|
||||||
```
|
|
||||||
|
|
||||||
### Possible Cause
|
|
||||||
<!-- Optional: your own hypothesis about the root cause -->
|
|
||||||
|
|
||||||
/label ~bug
|
|
||||||
|
|
||||||
/label ~server
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
## Feature Request
|
|
||||||
|
|
||||||
### Summary
|
|
||||||
<!-- Brief description of the desired functionality -->
|
|
||||||
|
|
||||||
### Motivation / Problem
|
|
||||||
<!-- What problem does this feature solve? Why is it needed? -->
|
|
||||||
|
|
||||||
### Description
|
|
||||||
<!-- Detailed description of the desired behavior -->
|
|
||||||
|
|
||||||
### Affected Components
|
|
||||||
<!-- Which existing components are affected or need to be extended? -->
|
|
||||||
<!-- e.g. Lobby Menu, Global Chat, Loading screen, … -->
|
|
||||||
|
|
||||||
### Acceptance Criteria
|
|
||||||
<!-- What must be fulfilled for this feature to be considered done? -->
|
|
||||||
- [ ]
|
|
||||||
- [ ]
|
|
||||||
- [ ]
|
|
||||||
|
|
||||||
### Open Questions / Trade-offs
|
|
||||||
<!-- Are there design decisions that still need to be discussed? -->
|
|
||||||
|
|
||||||
/label ~enhancement
|
|
||||||
|
|
||||||
/label ~client
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
## Feature Request
|
|
||||||
|
|
||||||
### Summary
|
|
||||||
<!-- Brief description of the desired functionality -->
|
|
||||||
|
|
||||||
### Motivation / Problem
|
|
||||||
<!-- What problem does this feature solve? Why is it needed? -->
|
|
||||||
|
|
||||||
### Description
|
|
||||||
<!-- Detailed description of the desired behavior -->
|
|
||||||
|
|
||||||
### Affected Components
|
|
||||||
<!-- Which existing components are affected or need to be extended? -->
|
|
||||||
<!-- e.g. CommandRouter, SessionManager, UserRegistry, … -->
|
|
||||||
|
|
||||||
### Acceptance Criteria
|
|
||||||
<!-- What must be fulfilled for this feature to be considered done? -->
|
|
||||||
- [ ]
|
|
||||||
- [ ]
|
|
||||||
- [ ]
|
|
||||||
|
|
||||||
### Open Questions / Trade-offs
|
|
||||||
<!-- Are there design decisions that still need to be discussed? -->
|
|
||||||
|
|
||||||
/label ~enhancement
|
|
||||||
|
|
||||||
/label ~server
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
## Protocol / API Change
|
|
||||||
|
|
||||||
### Summary
|
|
||||||
<!-- Brief description of the protocol change -->
|
|
||||||
|
|
||||||
### Affected Commands / Responses
|
|
||||||
<!-- Which commands or response types are being added, changed, or removed? -->
|
|
||||||
|
|
||||||
| Command | Response | Type of Change | Description |
|
|
||||||
|---------|----------|----------------|-------------|
|
|
||||||
| | | New / Changed / Removed | |
|
|
||||||
|
|
||||||
### New / Changed Syntax
|
|
||||||
```
|
|
||||||
<!-- Before / after or new command syntax here -->
|
|
||||||
<!-- Example:
|
|
||||||
BEFORE: LOGIN UNAME=<username> PASS=<password>
|
|
||||||
AFTER: LOGIN USERNAME=<username> PASSWORD=<password>
|
|
||||||
-->
|
|
||||||
```
|
|
||||||
|
|
||||||
### Affected Implementation Layers
|
|
||||||
<!-- Which components / classes need to be updated? -->
|
|
||||||
- [ ] `Session`
|
|
||||||
- [ ] `User` or `UserRegistry`
|
|
||||||
- [ ] `Tokenizer` / `RequestTokenClassifier`
|
|
||||||
- [ ] `ProtocolParser`
|
|
||||||
- [ ] `CommandParserDispatcher` + corresponding `CommandParser`
|
|
||||||
- [ ] `CommandHandler`
|
|
||||||
- [ ] `Response` hierarchy
|
|
||||||
- [ ] `GameEngine` or `GameState`
|
|
||||||
- [ ] Client-side (create your own issue and create link)
|
|
||||||
|
|
||||||
### Motivation
|
|
||||||
<!-- Why is this protocol change necessary? -->
|
|
||||||
|
|
||||||
/label ~protocol
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
## Task
|
|
||||||
|
|
||||||
### Summary
|
|
||||||
<!-- What needs to be implemented? -->
|
|
||||||
|
|
||||||
### Context
|
|
||||||
<!-- Which part of the system does it belong to? -->
|
|
||||||
|
|
||||||
### Affected Components
|
|
||||||
<!-- e.g. Controller, View, Model, Network, ... -->
|
|
||||||
|
|
||||||
### Definition of Done
|
|
||||||
- [ ]
|
|
||||||
- [ ]
|
|
||||||
- [ ]
|
|
||||||
|
|
||||||
### Notes
|
|
||||||
<!-- Optional: implementation hints, links to prior design discussions, etc. -->
|
|
||||||
|
|
||||||
### Checklist
|
|
||||||
- [ ] Linked to other issues or branches that must be completed first?
|
|
||||||
|
|
||||||
/label ~task
|
|
||||||
|
|
||||||
/label ~client
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
## Task
|
|
||||||
|
|
||||||
### Summary
|
|
||||||
<!-- What needs to be implemented? -->
|
|
||||||
|
|
||||||
### Context
|
|
||||||
<!-- Which part of the system does it belong to? -->
|
|
||||||
|
|
||||||
### Affected Components
|
|
||||||
<!-- e.g. Response hierarchy, CommandRouter, UserRegistry, ... -->
|
|
||||||
|
|
||||||
### Definition of Done
|
|
||||||
- [ ]
|
|
||||||
- [ ]
|
|
||||||
- [ ]
|
|
||||||
|
|
||||||
### Notes
|
|
||||||
<!-- Optional: implementation hints, links to prior design discussions, etc. -->
|
|
||||||
|
|
||||||
### Checklist
|
|
||||||
- [ ] Linked to other issues or branches that must be completed first?
|
|
||||||
|
|
||||||
/label ~task
|
|
||||||
|
|
||||||
/label ~server
|
|
||||||
+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**.
|
||||||
@@ -3,8 +3,10 @@ package ch.unibas.dmi.dbis.cs108.casono.server;
|
|||||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserCleanupJob;
|
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserCleanupJob;
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserRegistry;
|
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserRegistry;
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.NetworkManager;
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.NetworkManager;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.CommandRouter;
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.events.DisconnectEvent;
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.events.DisconnectEvent;
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.events.EventBus;
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.events.EventBus;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.parser.CommandParserDispatcher;
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionManager;
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionManager;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
@@ -26,7 +28,10 @@ public class ServerApp {
|
|||||||
logger.info("Starting server at port {}", port);
|
logger.info("Starting server at port {}", port);
|
||||||
|
|
||||||
EventBus eventBus = new EventBus();
|
EventBus eventBus = new EventBus();
|
||||||
SessionManager sessionManager = new 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));
|
eventBus.subscribe(DisconnectEvent.class, event -> sessionManager.onDisconnect(event));
|
||||||
NetworkManager networkManager = new NetworkManager(port, sessionManager);
|
NetworkManager networkManager = new NetworkManager(port, sessionManager);
|
||||||
|
|
||||||
|
|||||||
+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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-1
@@ -3,4 +3,5 @@ package ch.unibas.dmi.dbis.cs108.casono.server.network.parser;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/** Created by the ProtocolParser to allow easy access to the request contents */
|
/** 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;
|
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.RawToken;
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.server.tokenizer.Token;
|
import ch.unibas.dmi.dbis.cs108.casono.server.tokenizer.Token;
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.server.tokenizer.TokenClassifier;
|
import ch.unibas.dmi.dbis.cs108.casono.server.tokenizer.TokenClassifier;
|
||||||
@@ -19,15 +18,15 @@ public class ProtocolParser {
|
|||||||
* @param packet the RawPacket containing the recieved data
|
* @param packet the RawPacket containing the recieved data
|
||||||
* @return created PrimitiveRequest
|
* @return created PrimitiveRequest
|
||||||
*/
|
*/
|
||||||
public static PrimitiveRequest parse(RawPacket packet) {
|
public static RawRequest parse(String payload) {
|
||||||
List<RawToken> rawTokens = Tokenizer.tokenize(packet.payload());
|
List<RawToken> rawTokens = Tokenizer.tokenize(payload);
|
||||||
List<Token> tokens = TokenClassifier.classify(rawTokens);
|
List<Token> tokens = TokenClassifier.classify(rawTokens);
|
||||||
|
|
||||||
Iterator<Token> iterator = tokens.iterator();
|
Iterator<Token> iterator = tokens.iterator();
|
||||||
String command = readCommand(iterator);
|
String command = readCommand(iterator);
|
||||||
List<Parameter> parameters = readParameters(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;
|
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 */
|
/** 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) {}
|
||||||
+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());
|
||||||
|
}
|
||||||
|
}
|
||||||
+7
@@ -2,4 +2,11 @@ package ch.unibas.dmi.dbis.cs108.casono.server.network.response;
|
|||||||
|
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionId;
|
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) {}
|
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";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
package ch.unibas.dmi.dbis.cs108.casono.server.network.sessions;
|
package ch.unibas.dmi.dbis.cs108.casono.server.network.sessions;
|
||||||
|
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.CommandRouter;
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.events.EventBus;
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.events.EventBus;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.parser.CommandParserDispatcher;
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.response.PrimitiveResponse;
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.response.PrimitiveResponse;
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.transport.TransportLayer;
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.transport.TransportLayer;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@@ -12,6 +14,8 @@ public class Session {
|
|||||||
private final SessionId id;
|
private final SessionId id;
|
||||||
private final TransportLayer transport;
|
private final TransportLayer transport;
|
||||||
private final BlockingQueue<PrimitiveResponse> responseQueue;
|
private final BlockingQueue<PrimitiveResponse> responseQueue;
|
||||||
|
private final CommandParserDispatcher dispatcher;
|
||||||
|
private final CommandRouter router;
|
||||||
private static final int RESPOND_QUEUE_SIZE = 32;
|
private static final int RESPOND_QUEUE_SIZE = 32;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -21,9 +25,15 @@ public class Session {
|
|||||||
* @param eventBus the event bus for publishing events
|
* @param eventBus the event bus for publishing events
|
||||||
* @throws IOException if an I/O error occurs during initialization
|
* @throws IOException if an I/O error occurs during initialization
|
||||||
*/
|
*/
|
||||||
public Session(TransportLayer transport, EventBus eventBus) {
|
public Session(
|
||||||
|
TransportLayer transport,
|
||||||
|
EventBus eventBus,
|
||||||
|
CommandParserDispatcher dispatcher,
|
||||||
|
CommandRouter router) {
|
||||||
this.id = new SessionId();
|
this.id = new SessionId();
|
||||||
this.transport = transport;
|
this.transport = transport;
|
||||||
|
this.dispatcher = dispatcher;
|
||||||
|
this.router = router;
|
||||||
this.responseQueue = new ArrayBlockingQueue<>(RESPOND_QUEUE_SIZE);
|
this.responseQueue = new ArrayBlockingQueue<>(RESPOND_QUEUE_SIZE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,4 +63,17 @@ public class Session {
|
|||||||
public BlockingQueue<PrimitiveResponse> getResponseQueue() {
|
public BlockingQueue<PrimitiveResponse> getResponseQueue() {
|
||||||
return responseQueue;
|
return responseQueue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-8
@@ -1,7 +1,9 @@
|
|||||||
package ch.unibas.dmi.dbis.cs108.casono.server.network.sessions;
|
package ch.unibas.dmi.dbis.cs108.casono.server.network.sessions;
|
||||||
|
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.CommandRouter;
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.events.DisconnectEvent;
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.events.DisconnectEvent;
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.events.EventBus;
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.events.EventBus;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.parser.CommandParserDispatcher;
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.transport.TransportLayer;
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.transport.TransportLayer;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -14,12 +16,17 @@ public class SessionManager {
|
|||||||
private Map<SessionId, SessionHandle> sessions;
|
private Map<SessionId, SessionHandle> sessions;
|
||||||
private final EventBus eventBus;
|
private final EventBus eventBus;
|
||||||
private final Logger logger;
|
private final Logger logger;
|
||||||
|
private final CommandParserDispatcher dispatcher;
|
||||||
|
private final CommandRouter router;
|
||||||
|
|
||||||
/** Constructs a new SessionManager. */
|
/** Constructs a new SessionManager. */
|
||||||
public SessionManager(EventBus eventBus) {
|
public SessionManager(
|
||||||
|
EventBus eventBus, CommandParserDispatcher dispatcher, CommandRouter router) {
|
||||||
this.sessions = new ConcurrentHashMap<>();
|
this.sessions = new ConcurrentHashMap<>();
|
||||||
this.eventBus = eventBus;
|
this.eventBus = eventBus;
|
||||||
this.logger = LogManager.getLogger(SessionManager.class);
|
this.logger = LogManager.getLogger(SessionManager.class);
|
||||||
|
this.dispatcher = dispatcher;
|
||||||
|
this.router = router;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -31,16 +38,18 @@ public class SessionManager {
|
|||||||
* @return newly created session
|
* @return newly created session
|
||||||
*/
|
*/
|
||||||
public Session create(TransportLayer transport) {
|
public Session create(TransportLayer transport) {
|
||||||
Session session = new Session(transport, eventBus);
|
Session session = new Session(transport, eventBus, dispatcher, router);
|
||||||
SessionReader reader = new SessionReader(session, eventBus);
|
SessionReader reader = new SessionReader(session, eventBus);
|
||||||
SessionWriter writer = new SessionWriter(session);
|
SessionWriter writer = new SessionWriter(session);
|
||||||
|
|
||||||
Thread readerThread = Thread.ofVirtual()
|
Thread readerThread =
|
||||||
.name("session-" + session.getId().value() + "-reader")
|
Thread.ofVirtual()
|
||||||
.unstarted(reader);
|
.name("session-" + session.getId().value() + "-reader")
|
||||||
Thread writerThread = Thread.ofVirtual()
|
.unstarted(reader);
|
||||||
.name("session-" + session.getId().value() + "-writer")
|
Thread writerThread =
|
||||||
.unstarted(writer);
|
Thread.ofVirtual()
|
||||||
|
.name("session-" + session.getId().value() + "-writer")
|
||||||
|
.unstarted(writer);
|
||||||
|
|
||||||
sessions.put(session.getId(), new SessionHandle(session, readerThread, writerThread));
|
sessions.put(session.getId(), new SessionHandle(session, readerThread, writerThread));
|
||||||
readerThread.start();
|
readerThread.start();
|
||||||
|
|||||||
+22
-2
@@ -1,10 +1,15 @@
|
|||||||
package ch.unibas.dmi.dbis.cs108.casono.server.network.sessions;
|
package ch.unibas.dmi.dbis.cs108.casono.server.network.sessions;
|
||||||
|
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.CommandRouter;
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.events.DisconnectEvent;
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.events.DisconnectEvent;
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.events.EventBus;
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.events.EventBus;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.parser.CommandParserDispatcher;
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.parser.PrimitiveRequest;
|
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.ProtocolParser;
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.parser.ProtocolParserException;
|
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.RawPacket;
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.transport.TransportLayer;
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.transport.TransportLayer;
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.server.tokenizer.TokenizerException;
|
import ch.unibas.dmi.dbis.cs108.casono.server.tokenizer.TokenizerException;
|
||||||
@@ -17,12 +22,16 @@ public class SessionReader implements Runnable {
|
|||||||
private final Session session;
|
private final Session session;
|
||||||
private final TransportLayer transport;
|
private final TransportLayer transport;
|
||||||
private final EventBus eventBus;
|
private final EventBus eventBus;
|
||||||
|
private final CommandParserDispatcher dispatcher;
|
||||||
|
private final CommandRouter router;
|
||||||
private final Logger logger;
|
private final Logger logger;
|
||||||
|
|
||||||
public SessionReader(Session session, EventBus eventBus) {
|
public SessionReader(Session session, EventBus eventBus) {
|
||||||
this.session = session;
|
this.session = session;
|
||||||
this.transport = session.getTransport();
|
this.transport = session.getTransport();
|
||||||
this.eventBus = eventBus;
|
this.eventBus = eventBus;
|
||||||
|
this.dispatcher = session.getDispatcher();
|
||||||
|
this.router = session.getRouter();
|
||||||
this.logger =
|
this.logger =
|
||||||
LogManager.getLogger(
|
LogManager.getLogger(
|
||||||
SessionReader.class.toString() + "-" + session.getId().value());
|
SessionReader.class.toString() + "-" + session.getId().value());
|
||||||
@@ -36,8 +45,19 @@ public class SessionReader implements Runnable {
|
|||||||
rawPacket = transport.read();
|
rawPacket = transport.read();
|
||||||
logger.debug("Recieved: {}", rawPacket);
|
logger.debug("Recieved: {}", rawPacket);
|
||||||
|
|
||||||
PrimitiveRequest primitiveRequest = ProtocolParser.parse(rawPacket);
|
RawRequest rawRequest = ProtocolParser.parse(rawPacket.payload());
|
||||||
logger.debug("Parsed request to {}", primitiveRequest);
|
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) {
|
} catch (EOFException e) {
|
||||||
logger.info("Client disconnected");
|
logger.info("Client disconnected");
|
||||||
eventBus.publish(new DisconnectEvent(session.getId()));
|
eventBus.publish(new DisconnectEvent(session.getId()));
|
||||||
|
|||||||
Reference in New Issue
Block a user