Compare commits
303 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 | |||
| 0d8f1ac4f7 | |||
| 54fd43142b | |||
| 5bd88445d2 | |||
| e6c9c12e3b | |||
| be124f4a05 | |||
| 85d604b978 | |||
| 47544c4f1b | |||
| 46b1388cad | |||
| 68827e177e | |||
| 0a5eb5d11c | |||
| 8fae4789ad | |||
| 8dd99726ce | |||
| 1223e1a4a9 | |||
| 9d9b6ae2c7 | |||
| a7cdf995c4 | |||
| 82582dae83 | |||
| 7c5a4675dd | |||
| 2979c91ee0 | |||
| 8263a1355a | |||
| 118869f461 | |||
| f7f570638b | |||
| 80c7e1c25d | |||
| 135e6a5327 | |||
| 4e853cf35f | |||
| f6c889c250 | |||
| 8f7c92684b | |||
| a5f2ac635d | |||
| 7ca0bcc3e0 | |||
| aa508dfd6c | |||
| 9c3f62db24 | |||
| 99a7154701 | |||
| 2d2bdddfa8 | |||
| 0bfcbe95b0 | |||
| 94a2e1f977 | |||
| 1bfbb5c75d | |||
| 61f0a23bfa | |||
| 08dc94b4b0 | |||
| ca11c135f1 | |||
| 60f59e03d4 | |||
| 1e794337b5 | |||
| 3509a967a6 | |||
| 4c0e6aeb36 | |||
| 3e138ab84e | |||
| 45fd696fec | |||
| b898357d76 | |||
| 830c04a706 | |||
| d242aeef7c | |||
| 71d6ac9ecc | |||
| 8415e3f155 | |||
| a15a0a0044 | |||
| e3e1da0082 | |||
| d1bf8e4c9a | |||
| f3c34dfa7f | |||
| 2af3fe7c35 | |||
| d01bbdec7b | |||
| 58c473fc9e | |||
| c411e9c560 | |||
| 1c5c4c8a34 | |||
| 9376b1d406 | |||
| cb98c70562 | |||
| d8bfc1c8e9 | |||
| 14c18df0eb | |||
| 208a284204 | |||
| ef2a00268f | |||
| d403908341 | |||
| fa54fd75c1 | |||
| 50d75e2f03 | |||
| 3dbd3a6c15 | |||
| 1aafc0bc4b | |||
| 2f7040485a | |||
| 736cf230fb | |||
| 3ce0a44a18 | |||
| 59494ea2d4 | |||
| 34c89b671e | |||
| bb0f805b4e | |||
| 573c5fd36d | |||
| 09ebacc1e4 | |||
| 7be2229280 | |||
| 3a8dc98e91 | |||
| b731f48ba7 | |||
| 9a88cfc945 | |||
| e22446bffc | |||
| 59a4648ad6 | |||
| ec4d8bfb07 | |||
| 1fe6d9ce5d | |||
| 8f5d90c538 | |||
| 8610f77386 | |||
| c38f177a16 | |||
| 815caff372 | |||
| c441318df9 | |||
| 7d2486d3be | |||
| 18d8b68fc8 | |||
| 4ecbe21602 | |||
| e718083010 | |||
| b810b2187b | |||
| 5b6b58f1cc | |||
| 6e35c599da | |||
| 66f840b269 | |||
| 39b6c5e13e | |||
| 000a5c0d9a | |||
| 0157102ae5 | |||
| a7bfb59e2f | |||
| 37faa39f10 | |||
| 147f84665e | |||
| 8aa71a4430 | |||
| cfeea5222d | |||
| fb02faffc9 | |||
| 815b60232c | |||
| 28f8d17237 | |||
| d311553de7 | |||
| e797470ca3 | |||
| 18f17564d5 | |||
| 57010d2872 | |||
| 004b2e2313 | |||
| 9b40396eed | |||
| 4bcb9a9874 | |||
| 9c3114b5f3 | |||
| 1cefe9c9d9 | |||
| 0e30e08247 | |||
| 38133e6761 | |||
| 4231ca3e97 | |||
| 75567c59b2 | |||
| a39775bd04 | |||
| 49019075e4 | |||
| 97a860df9e | |||
| 0b51f4f45a | |||
| 711de2e6aa | |||
| 74566f99cf | |||
| c301039bfc | |||
| e1029932d8 | |||
| db7892d9c4 | |||
| 7b32fe1147 | |||
| 54588276d1 | |||
| fc8a92789a | |||
| 7fcb47ed6f | |||
| d4bf69dc59 | |||
| b073760eef | |||
| 48d77b6cbe | |||
| 59765e33bb | |||
| fa95ce1418 | |||
| d2407d61f4 | |||
| d414c27ff2 | |||
| f4a32b1649 | |||
| 5407e245f7 | |||
| 02dc75c331 | |||
| 4989e26ac6 | |||
| 89d4cad336 | |||
| c0b6a5424a | |||
| f9b32bc979 | |||
| 25d145cffd | |||
| d47084d6f5 | |||
| c87e6f1463 | |||
| 23faf0f371 | |||
| 268cf283bd | |||
| 5ff4dcec53 | |||
| dac05de331 | |||
| b1ab8f3982 | |||
| 82dd5a9b8a | |||
| 18331a7cc3 | |||
| a2256b2c1e | |||
| 85c872df23 | |||
| 2b012aa256 | |||
| 4879b1b464 | |||
| a5204eaf29 | |||
| 14384dea5b | |||
| e5e93c1a65 | |||
| f1b6243d6d | |||
| 7daae485da | |||
| a7e43619f8 | |||
| 34cb17d0e4 | |||
| a6afbeeae1 | |||
| a5272b99ab | |||
| fb6f14371f | |||
| 32e4b04c87 | |||
| 22990fa35e | |||
| ac9e7eecff | |||
| c1a4b436fe | |||
| 9c291e4e3b | |||
| 91a513e214 | |||
| 66925bc665 | |||
| ff38c9d6ef | |||
| e6b5cac53c |
@@ -119,3 +119,7 @@ $RECYCLE.BIN/
|
|||||||
|
|
||||||
## IntelliJ
|
## IntelliJ
|
||||||
.idea
|
.idea
|
||||||
|
|
||||||
|
## bin
|
||||||
|
bin/
|
||||||
|
gradle.properties
|
||||||
+113
@@ -0,0 +1,113 @@
|
|||||||
|
stages:
|
||||||
|
- lint
|
||||||
|
- report
|
||||||
|
- build
|
||||||
|
- test
|
||||||
|
|
||||||
|
# Reusable definitions
|
||||||
|
.gradle-cache: &gradle-cache
|
||||||
|
variables:
|
||||||
|
GRADLE_USER_HOME: '$CI_PROJECT_DIR/.gradle-home'
|
||||||
|
cache:
|
||||||
|
key: '$CI_PROJECT_ID-gradle'
|
||||||
|
paths:
|
||||||
|
- .gradle/
|
||||||
|
- .gradle-home/
|
||||||
|
|
||||||
|
.on-commit: &on-commit
|
||||||
|
rules:
|
||||||
|
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
||||||
|
when: never
|
||||||
|
- if: '$CI_COMMIT_BRANCH'
|
||||||
|
allow_failure: true
|
||||||
|
|
||||||
|
.on-mr: &on-mr
|
||||||
|
rules:
|
||||||
|
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
||||||
|
allow_failure: false
|
||||||
|
|
||||||
|
# Jobs
|
||||||
|
checkstyle:
|
||||||
|
<<: [*gradle-cache, *on-commit]
|
||||||
|
stage: lint
|
||||||
|
image: gradle:9.3.1-jdk25
|
||||||
|
script:
|
||||||
|
- gradle checkstyleMain checkstyleTest --configuration-cache --configuration-cache-problems=warn
|
||||||
|
allow_failure: true
|
||||||
|
artifacts:
|
||||||
|
when: always
|
||||||
|
paths:
|
||||||
|
- build/reports/checkstyle/main.html
|
||||||
|
- build/reports/checkstyle/main.xml
|
||||||
|
- build/reports/checkstyle/test.html
|
||||||
|
- build/reports/checkstyle/test.xml
|
||||||
|
expose_as: 'Checkstyle Report'
|
||||||
|
expire_in: 1 week
|
||||||
|
|
||||||
|
checkstyle-mr:
|
||||||
|
<<: [*gradle-cache, *on-mr]
|
||||||
|
stage: lint
|
||||||
|
image: gradle:9.3.1-jdk25
|
||||||
|
script:
|
||||||
|
- gradle checkstyleMain checkstyleTest --configuration-cache --configuration-cache-problems=warn
|
||||||
|
allow_failure: false
|
||||||
|
artifacts:
|
||||||
|
when: always
|
||||||
|
paths:
|
||||||
|
- build/reports/checkstyle/main.html
|
||||||
|
- build/reports/checkstyle/main.xml
|
||||||
|
- build/reports/checkstyle/test.html
|
||||||
|
- build/reports/checkstyle/test.xml
|
||||||
|
expose_as: 'Checkstyle Report'
|
||||||
|
expire_in: 1 week
|
||||||
|
|
||||||
|
checkstyle-report:
|
||||||
|
stage: report
|
||||||
|
image: python:3.14
|
||||||
|
needs:
|
||||||
|
- job: checkstyle-mr
|
||||||
|
artifacts: true
|
||||||
|
optional: true
|
||||||
|
when: always
|
||||||
|
rules:
|
||||||
|
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
||||||
|
script:
|
||||||
|
- python3 scripts/convert-checkstyle-gl-report.py
|
||||||
|
artifacts:
|
||||||
|
when: always
|
||||||
|
reports:
|
||||||
|
codequality: gl-code-quality-report.json
|
||||||
|
expire_in: 1 week
|
||||||
|
|
||||||
|
compile-check:
|
||||||
|
<<: *gradle-cache
|
||||||
|
stage: build
|
||||||
|
image: gradle:9.3.1-jdk25
|
||||||
|
script:
|
||||||
|
- gradle compileTestJava --configuration-cache --configuration-cache-problems=warn
|
||||||
|
needs: []
|
||||||
|
rules:
|
||||||
|
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
||||||
|
allow_failure: false
|
||||||
|
- if: '$CI_COMMIT_BRANCH'
|
||||||
|
allow_failure: false
|
||||||
|
|
||||||
|
test:
|
||||||
|
<<: *gradle-cache
|
||||||
|
stage: test
|
||||||
|
image: gradle:9.3.1-jdk25
|
||||||
|
script:
|
||||||
|
- gradle test --configuration-cache --configuration-cache-problems=warn
|
||||||
|
artifacts:
|
||||||
|
when: always
|
||||||
|
reports:
|
||||||
|
junit: build/test-results/test/*.xml
|
||||||
|
paths:
|
||||||
|
- build/reports/tests/test/
|
||||||
|
expose_as: 'Test Report'
|
||||||
|
expire_in: 1 week
|
||||||
|
rules:
|
||||||
|
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
||||||
|
allow_failure: false
|
||||||
|
- if: '$CI_COMMIT_BRANCH'
|
||||||
|
allow_failure: true
|
||||||
@@ -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
|
||||||
Vendored
+3
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"java.configuration.updateBuildConfiguration": "automatic"
|
||||||
|
}
|
||||||
+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**.
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 4.8 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 3.0 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 194 KiB |
+45
-1
@@ -2,6 +2,8 @@ plugins {
|
|||||||
id 'application'
|
id 'application'
|
||||||
id 'java'
|
id 'java'
|
||||||
id 'org.openjfx.javafxplugin' version '0.1.0'
|
id 'org.openjfx.javafxplugin' version '0.1.0'
|
||||||
|
id 'checkstyle'
|
||||||
|
id 'com.diffplug.spotless' version '7.0.2'
|
||||||
}
|
}
|
||||||
|
|
||||||
group = 'ch.unibas.dmi.dbis'
|
group = 'ch.unibas.dmi.dbis'
|
||||||
@@ -23,18 +25,44 @@ repositories {
|
|||||||
|
|
||||||
javafx {
|
javafx {
|
||||||
version = "25.0.2"
|
version = "25.0.2"
|
||||||
modules = ['javafx.controls', 'javafx.fxml', 'javafx.base']
|
modules = ['javafx.controls', 'javafx.fxml', 'javafx.base', 'javafx.graphics', 'javafx.web']
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
// Source: https://mvnrepository.com/artifact/org.apache.logging.log4j
|
||||||
|
implementation("org.apache.logging.log4j:log4j-api:2.25.3")
|
||||||
|
runtimeOnly("org.apache.logging.log4j:log4j-core:2.25.3")
|
||||||
|
|
||||||
|
// Source: https://mvnrepository.com/artifact/org.fusesource.jansi/jansi
|
||||||
|
runtimeOnly("org.fusesource.jansi:jansi:2.4.2")
|
||||||
|
|
||||||
testImplementation("org.junit.jupiter:junit-jupiter:5.10.0")
|
testImplementation("org.junit.jupiter:junit-jupiter:5.10.0")
|
||||||
testRuntimeOnly("org.junit.platform:junit-platform-launcher:1.10.0")
|
testRuntimeOnly("org.junit.platform:junit-platform-launcher:1.10.0")
|
||||||
|
implementation 'org.json:json:20240303'
|
||||||
}
|
}
|
||||||
|
|
||||||
test {
|
test {
|
||||||
useJUnitPlatform()
|
useJUnitPlatform()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
checkstyle {
|
||||||
|
toolVersion = '10.21.0'
|
||||||
|
configFile = file('config/checkstyle/checkstyle.xml')
|
||||||
|
configProperties = [
|
||||||
|
'suppressionFile': file('config/checkstyle/suppressions.xml').absolutePath
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
spotless {
|
||||||
|
java {
|
||||||
|
googleJavaFormat('1.35.0').aosp()
|
||||||
|
importOrder()
|
||||||
|
removeUnusedImports()
|
||||||
|
trimTrailingWhitespace()
|
||||||
|
endWithNewline()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
tasks.named('jar', Jar) {
|
tasks.named('jar', Jar) {
|
||||||
manifest {
|
manifest {
|
||||||
attributes('Main-Class': application.mainClass.get())
|
attributes('Main-Class': application.mainClass.get())
|
||||||
@@ -42,6 +70,22 @@ tasks.named('jar', Jar) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Dieses JVM-Argument erlaubt JavaFX und anderen Libraries den Zugriff auf native Methoden.
|
||||||
|
// Ohne diese Einstellung erscheinen Warnungen und zukünftige Java-Versionen könnten den Zugriff blockieren.
|
||||||
|
// Siehe: https://openjdk.org/jeps/472
|
||||||
|
tasks.withType(JavaExec) {
|
||||||
|
jvmArgs += '--enable-native-access=ALL-UNNAMED'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
tasks.register('cruntest', JavaExec) {
|
||||||
|
classpath = sourceSets.main.runtimeClasspath
|
||||||
|
mainClass = 'ch.unibas.dmi.dbis.cs108.casono.Main'
|
||||||
|
jvmArgs '--enable-native-access=ALL-UNNAMED'
|
||||||
|
args 'client', '0.0.0.0:1234'
|
||||||
|
}
|
||||||
|
|
||||||
tasks.register('fatJar', Jar) {
|
tasks.register('fatJar', Jar) {
|
||||||
group = 'build'
|
group = 'build'
|
||||||
description = 'Assembles a runnable fat JAR including runtime dependencies.'
|
description = 'Assembles a runnable fat JAR including runtime dependencies.'
|
||||||
|
|||||||
@@ -0,0 +1,214 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
<!DOCTYPE module PUBLIC
|
||||||
|
"-//Checkstyle//DTD Checkstyle Configuration 1.3//EN"
|
||||||
|
"https://checkstyle.org/dtds/configuration_1_3.dtd">
|
||||||
|
|
||||||
|
<module name="Checker">
|
||||||
|
<!-- Fail build if checkstyle finds violations -->
|
||||||
|
<!-- https://checkstyle.sourceforge.io/config.html#Properties_1 -->
|
||||||
|
<property name="severity" value="error"/>
|
||||||
|
|
||||||
|
<!-- Only check java files -->
|
||||||
|
<!-- https://checkstyle.sourceforge.io/config.html#Properties_1 -->
|
||||||
|
<property name="fileExtensions" value="java"/>
|
||||||
|
|
||||||
|
<module name="SuppressionFilter">
|
||||||
|
<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"/>
|
||||||
|
|
||||||
|
<module name="TreeWalker">
|
||||||
|
<!-- 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,
|
||||||
|
CTOR_DEF,
|
||||||
|
STATIC_INIT,
|
||||||
|
INSTANCE_INIT,
|
||||||
|
CLASS_DEF,
|
||||||
|
INTERFACE_DEF,
|
||||||
|
ENUM_DEF
|
||||||
|
"/>
|
||||||
|
</module>
|
||||||
|
|
||||||
|
<!-- No unordered / ungrouped imports -->
|
||||||
|
<!-- https://checkstyle.sourceforge.io/checks/imports/importorder.html -->
|
||||||
|
<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">
|
||||||
|
<property name="severity" value="warning"/>
|
||||||
|
</module>
|
||||||
|
|
||||||
|
<!-- Method names as camelCase -->
|
||||||
|
<!-- https://checkstyle.sourceforge.io/checks/naming/methodname.html -->
|
||||||
|
<module name="MethodName">
|
||||||
|
<property name="severity" value="warning"/>
|
||||||
|
</module>
|
||||||
|
|
||||||
|
<!-- Parameter names as camelCase -->
|
||||||
|
<!-- https://checkstyle.sourceforge.io/checks/naming/parametername.html -->
|
||||||
|
<module name="ParameterName">
|
||||||
|
<property name="severity" value="warning"/>
|
||||||
|
</module>
|
||||||
|
|
||||||
|
<!-- Lokal variables as camelCase -->
|
||||||
|
<!-- https://checkstyle.sourceforge.io/checks/naming/localvariablename.html -->
|
||||||
|
<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">
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<!-- 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>
|
||||||
|
|
||||||
|
<!-- 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">
|
||||||
|
<property name="severity" value="info"/>
|
||||||
|
</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>
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
<!DOCTYPE suppressions PUBLIC
|
||||||
|
"-//Checkstyle//DTD SuppressionFilter Configuration 1.2//EN"
|
||||||
|
"https://checkstyle.org/dtds/suppressions_1_2.dtd">
|
||||||
|
|
||||||
|
<suppressions>
|
||||||
|
<!-- Allow literal, for example assertEquals(42, life.answer()) -->
|
||||||
|
<suppress checks="MagicNumber" files=".*Test\.java"/>
|
||||||
|
|
||||||
|
<!-- Allow longer method names as the are usually descriptive of the test -->
|
||||||
|
<suppress checks="MethodLength" files=".*Test\.java"/>
|
||||||
|
|
||||||
|
<!-- Allow methods with more parameters -->
|
||||||
|
<suppress checks="ParameterNumber" files=".*Test\.java"/>
|
||||||
|
|
||||||
|
<!-- Allow for System.out.println in tests -->
|
||||||
|
<suppress id="SystemOutErr" files=".*Test\.java"/>
|
||||||
|
|
||||||
|
<!-- Allow wildcard imports (import x.*) -->
|
||||||
|
<suppress checks="AvoidStarImport" files=".*Test\.java"/>
|
||||||
|
|
||||||
|
<!-- Allow for compacter line seperators -->
|
||||||
|
<suppress checks="EmptyLineSeparator" files=".*Test\.java"/>
|
||||||
|
</suppressions>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# MS2 Meeting
|
||||||
|
|
||||||
|
> 14.03.2025 (14:00)
|
||||||
|
|
||||||
|
In einem Meeting haben wir die Struktur zwischen **LobbyScreen**, **GameScreen**
|
||||||
|
und der **Chatbox** besprochen. Dabei ging es vor allem darum, wie der Chat in beide
|
||||||
|
UIs eingebettet wird und wie die Verbindung zwischen den Komponenten umgesetzt wird.
|
||||||
|
|
||||||
|
Außerdem wurde festgelegt, dass der **Endscreen** erst nach MS3 umgesetzt wird.
|
||||||
|
Zunächst werden die Texturen fertiggestellt und im GameScreen integriert.
|
||||||
|
Falls sich die Struktur (z. B. FXML oder CSS) noch ändert, kann der Endscreen
|
||||||
|
später aus der bestehenden Struktur abgeleitet werden, ohne Anpassungen doppelt
|
||||||
|
durchführen zu müssen.
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
# Server-Side Networking — How It All Fits Together
|
||||||
|
|
||||||
|
This doc is meant to give a solid mixed-level understanding of how our server-side networking works. No deep dives — just a clear picture of what's happening under the hood when a client connects and sends a command.
|
||||||
|
|
||||||
|
> **Note:** This is v1. Annotations and Reflection-based dispatching are on the roadmap but not covered here yet.
|
||||||
|
|
||||||
|
> **Disclamer:** This document has been written by Claude. I modified certain parts and verified its contents for correctness.
|
||||||
|
|
||||||
|
## Our Protocol at a Glance
|
||||||
|
|
||||||
|
Our protocol is inspired by **POP3** - a classic, text-based protocol that communicates over a raw TCP connection. The idea is simple: the client sends a command as a plain-text string, and the server responds with either a success or an error.
|
||||||
|
|
||||||
|
A typical exchange looks something like this:
|
||||||
|
|
||||||
|
```
|
||||||
|
Client → GET_DELTA SINCE=42
|
||||||
|
Server → +OK
|
||||||
|
PLAYER_FOLDED playerId=3
|
||||||
|
POT 240
|
||||||
|
NEXT_TURN playerId=1
|
||||||
|
.
|
||||||
|
```
|
||||||
|
|
||||||
|
Responses start with `+OK` on success or `-ERR` when something goes wrong. Commands are short, uppercase strings - sometimes followed by arguments.
|
||||||
|
|
||||||
|
We don't use HTTP, there's no JSON body, no headers. Just a raw socket, a text stream, and a clearly defined set of commands.
|
||||||
|
|
||||||
|
## Core Components & Their Roles
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Here's a quick rundown of the main building blocks:
|
||||||
|
|
||||||
|
### `NetworkManager`
|
||||||
|
This is the entry point. It binds to a specific port and listens for incoming TCP connections.
|
||||||
|
Once a client connects, it creates a `Session` adds it to the `SessionManager` and goes back to waiting. Its not the responsibility of the `NetworkManager` to recieve and send data from and to each connected client.
|
||||||
|
|
||||||
|
### `Session`
|
||||||
|
Every client that connects has its own `Session` instance running on its own thread. This component owns the lifecycle of that connection: it reads incoming data, passes it along for processing, and writes responses back.
|
||||||
|
|
||||||
|
### `SessionId`
|
||||||
|
The `SessionId` is used to uniquely identify each session. Essentially its a wrapper arround the UUID type.
|
||||||
|
|
||||||
|
### `SessionManager`
|
||||||
|
The `SessionManager` stores all currently active sessions. It allows for the retrieval of a specific session by its id.
|
||||||
|
|
||||||
|
### `ProtocolParser`
|
||||||
|
Raw text (encapsulated in a `RawRequest`) coming off the socket isn't immediately useful — the `ProtocolParser` turns it with the help of the `Tokenizer` into a `PrimitiveRequest`.
|
||||||
|
|
||||||
|
### `RawRequest`
|
||||||
|
The `RawRequest` object is created after a message has been recieved.
|
||||||
|
It holds both a reference to the `Session` that recieved the message and the message itself.
|
||||||
|
|
||||||
|
### `PrimitiveRequest`
|
||||||
|
The `PrimitiveRequest` object is created by the `ProtocolParser` after the content of the `RawRequest` has been tokenized by the `Tokenizer`.
|
||||||
|
The command is stored in its own attribute and the arguments are accessible as a dictionary.
|
||||||
|
|
||||||
|
### `Tokenizer`
|
||||||
|
The job of the `tokenizer` is it, to take the raw string and turn it into tokens.
|
||||||
|
For example, the command `CHAT_LOBBY GAME=12 MESSAGE='All-in?'` will be decoded as `"CHAT_LOBBY", "GAME", "=", "12", "MESSAGE", "=", "All-in?"`.
|
||||||
|
It is unaware about the meaning.
|
||||||
|
|
||||||
|
### `CommandParser` (per command)
|
||||||
|
Takes in the `PrimitiveRequest` and checks if all required fields are provided with the correct value.
|
||||||
|
Creates a command specific `Request` object containing command arguments in a structured manner.
|
||||||
|
|
||||||
|
### `Request` (per command)
|
||||||
|
Has fields common along each implementation such as a reference to the `Session` that recieved the request.
|
||||||
|
Furthermore each implementation has fields unique to each command. For example, the `ChatLobbyRequest` has fields for the game and the message.
|
||||||
|
|
||||||
|
### `CommandRouter`
|
||||||
|
By identifying the type of class of the `Request`, the `CommandRouter` routes the request to the matching `CommandHandler`.
|
||||||
|
|
||||||
|
### `CommandHandler` (per command)
|
||||||
|
Each supported command has its own handler — a small, focused class that contains the logic for that specific command. They have access to the domain, containing inner parts of the game itself.
|
||||||
|
|
||||||
|
### `Response` (per response)
|
||||||
|
The interface has two sub-interfaces for either failed `ErrorResponse` or successfull `SuccessResponse` execution of commands.
|
||||||
|
|
||||||
|
Each step has exactly one responsibility. Increasing the ability to test and extend different components later on.
|
||||||
|
|
||||||
|
## Concurrency - Handling Multiple Clients
|
||||||
|
|
||||||
|
Every incoming connection spawns a new **Thread**. This means multiple clients can be served simultaneously without blocking each other.
|
||||||
|
|
||||||
|
As each `Session` runs entirely on its own thread, there's no shared mutable state between sessions, with exception of the `SessionManager` and other key components explained later.
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
@startuml
|
||||||
|
skinparam classAttributeIconSize 0
|
||||||
|
skinparam packageStyle rectangle
|
||||||
|
skinparam linetype ortho
|
||||||
|
skinparam backgroundColor transparent
|
||||||
|
|
||||||
|
' Network Layer
|
||||||
|
class NetworkManager {
|
||||||
|
+ start(port: int)
|
||||||
|
}
|
||||||
|
|
||||||
|
class Session {
|
||||||
|
- id: SessionId
|
||||||
|
+ Session(id: SessionId)
|
||||||
|
+ getId(): SessionId
|
||||||
|
+ run()
|
||||||
|
+ send(response: String)
|
||||||
|
}
|
||||||
|
|
||||||
|
class SessionId {
|
||||||
|
- value: UUID
|
||||||
|
+ SessionId()
|
||||||
|
+ SessionId(value: UUID)
|
||||||
|
}
|
||||||
|
|
||||||
|
class SessionManager {
|
||||||
|
- sessions: Map<UUID, Session>
|
||||||
|
+ add(session: Session)
|
||||||
|
+ getById(id: UUID): Session
|
||||||
|
+ remove(id: UUID)
|
||||||
|
}
|
||||||
|
|
||||||
|
NetworkManager --> Session : creates
|
||||||
|
NetworkManager --> SessionManager : adds session to
|
||||||
|
Session --> SessionId : identified by
|
||||||
|
Session --> RawRequest : creates
|
||||||
|
Session ..> Response : sends
|
||||||
|
SessionManager o-- Session : manages
|
||||||
|
|
||||||
|
' Protocol Layer
|
||||||
|
class RawRequest {
|
||||||
|
- session: Session
|
||||||
|
- rawMessage: String
|
||||||
|
}
|
||||||
|
|
||||||
|
class Tokenizer {
|
||||||
|
+ tokenize(input: String): List<String>
|
||||||
|
}
|
||||||
|
|
||||||
|
class ProtocolParser {
|
||||||
|
+ parse(raw: RawRequest): PrimitiveRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
class PrimitiveRequest {
|
||||||
|
- command: String
|
||||||
|
- arguments: Map<String, String>
|
||||||
|
}
|
||||||
|
|
||||||
|
RawRequest --> ProtocolParser : passed to
|
||||||
|
ProtocolParser --> Tokenizer : uses
|
||||||
|
ProtocolParser --> PrimitiveRequest : produces
|
||||||
|
|
||||||
|
' Command Layer
|
||||||
|
interface CommandParser <<per command>> {
|
||||||
|
+ parse(primitive: PrimitiveRequest): Request
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Request <<per command>> {
|
||||||
|
- session: Session
|
||||||
|
}
|
||||||
|
|
||||||
|
class CommandRouter {
|
||||||
|
+ route(request: Request)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CommandHandler <<per command>> {
|
||||||
|
+ handle(request: Request)
|
||||||
|
}
|
||||||
|
|
||||||
|
PrimitiveRequest --> CommandParser : passed to
|
||||||
|
CommandParser --> Request : produces
|
||||||
|
CommandRouter --> CommandHandler : dispatches to
|
||||||
|
CommandRouter ..> Request : receives
|
||||||
|
|
||||||
|
' Response Layer
|
||||||
|
interface Response {
|
||||||
|
+ encode(): String
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SuccessResponse
|
||||||
|
|
||||||
|
interface ErrorResponse
|
||||||
|
|
||||||
|
Response <|-- SuccessResponse
|
||||||
|
Response <|-- ErrorResponse
|
||||||
|
@enduml
|
||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,29 @@
|
|||||||
|
# Skript to convert findings of checkstyle into code quality report for gitlab
|
||||||
|
import xml.etree.ElementTree as ET, json, hashlib
|
||||||
|
|
||||||
|
findings = []
|
||||||
|
for source in ["main", "test"]:
|
||||||
|
try:
|
||||||
|
tree = ET.parse(f"build/reports/checkstyle/{source}.xml")
|
||||||
|
for file in tree.findall("file"):
|
||||||
|
path = file.get("name")
|
||||||
|
for error in file.findall("error"):
|
||||||
|
line = int(error.get("line", 1))
|
||||||
|
msg = error.get("message", "")
|
||||||
|
fingerprint = hashlib.md5(f"{path}{line}{msg}".encode()).hexdigest()
|
||||||
|
findings.append({
|
||||||
|
"type": "issue",
|
||||||
|
"check_name": error.get("source", "checkstyle"),
|
||||||
|
"description": msg,
|
||||||
|
"severity": "minor",
|
||||||
|
"fingerprint": fingerprint,
|
||||||
|
"location": {
|
||||||
|
"path": "src/" + path.split("/src/")[1] if "/src/" in path else path,
|
||||||
|
"lines": {"begin": line}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
with open("gl-code-quality-report.json", "w") as f:
|
||||||
|
json.dump(findings, f)
|
||||||
@@ -2,9 +2,16 @@ package ch.unibas.dmi.dbis.cs108.casono;
|
|||||||
|
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.client.ClientApp;
|
import ch.unibas.dmi.dbis.cs108.casono.client.ClientApp;
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.server.ServerApp;
|
import ch.unibas.dmi.dbis.cs108.casono.server.ServerApp;
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
|
||||||
|
/** Main entry point for Casono application. Handles client and server startup. */
|
||||||
public final class Main {
|
public final class Main {
|
||||||
|
/**
|
||||||
|
* Main entry point for Casono.
|
||||||
|
*
|
||||||
|
* @param args Command line arguments
|
||||||
|
*/
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
if (!isValid(args)) {
|
if (!isValid(args)) {
|
||||||
printUsage();
|
printUsage();
|
||||||
@@ -33,7 +40,9 @@ public final class Main {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static void printUsage() {
|
private static void printUsage() {
|
||||||
System.err.println("""
|
Logger logger = LogManager.getLogger(Main.class);
|
||||||
|
logger.fatal(
|
||||||
|
"""
|
||||||
Usage:
|
Usage:
|
||||||
java -jar xyz.jar server <listenPort>
|
java -jar xyz.jar server <listenPort>
|
||||||
java -jar xyz.jar client <serverIp>:<serverPort>
|
java -jar xyz.jar client <serverIp>:<serverPort>
|
||||||
|
|||||||
@@ -1,6 +1,32 @@
|
|||||||
package ch.unibas.dmi.dbis.cs108.casono.client;
|
package ch.unibas.dmi.dbis.cs108.casono.client;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entry point for the Casono client application. Handles client startup and connection parameters.
|
||||||
|
*/
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.client.ui.Launcher;
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entry point for the Casono client application. Handles client startup and connection parameters.
|
||||||
|
*
|
||||||
|
* <p>Standardkonstruktor für die Anwendung.
|
||||||
|
*/
|
||||||
public class ClientApp {
|
public class ClientApp {
|
||||||
|
|
||||||
|
private static final Logger LOGGER = LogManager.getLogger(ClientApp.class);
|
||||||
|
|
||||||
|
/** Standardkonstruktor. */
|
||||||
|
public ClientApp() {
|
||||||
|
// Standardkonstruktor
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the client application with the given address.
|
||||||
|
*
|
||||||
|
* @param arg Address in the format "ip:port".
|
||||||
|
* @throws IllegalArgumentException if the address format is invalid.
|
||||||
|
*/
|
||||||
public static void start(String arg) {
|
public static void start(String arg) {
|
||||||
String[] parts = arg.split(":", 2);
|
String[] parts = arg.split(":", 2);
|
||||||
if (parts.length != 2) {
|
if (parts.length != 2) {
|
||||||
@@ -9,6 +35,7 @@ public class ClientApp {
|
|||||||
String host = parts[0];
|
String host = parts[0];
|
||||||
int port = Integer.parseInt(parts[1]);
|
int port = Integer.parseInt(parts[1]);
|
||||||
|
|
||||||
System.out.println("You've selected the client. It will connect port " + port + " at host " + host);
|
LOGGER.info("You've selected the client. It will connect port {} at host {}", port, host);
|
||||||
|
Launcher.main(new String[] {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.client.ui;
|
||||||
|
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.client.ui.lobbyui.Casinomainui;
|
||||||
|
import javafx.application.Application;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Launcher for the Casono main UI.
|
||||||
|
*
|
||||||
|
* <p>Standardkonstruktor für die Anwendung.
|
||||||
|
*/
|
||||||
|
public class Launcher {
|
||||||
|
|
||||||
|
/** Standardkonstruktor. */
|
||||||
|
public Launcher() {
|
||||||
|
// Standardkonstruktor
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main entry point for launching the UI.
|
||||||
|
*
|
||||||
|
* @param args Command line arguments
|
||||||
|
*/
|
||||||
|
public static void main(String[] args) {
|
||||||
|
Application.launch(Casinomainui.class, args);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.client.ui.chatui;
|
||||||
|
|
||||||
|
import java.time.LocalTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import javafx.fxml.FXML;
|
||||||
|
import javafx.scene.control.Button;
|
||||||
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.control.ScrollPane;
|
||||||
|
import javafx.scene.control.TextField;
|
||||||
|
import javafx.scene.layout.VBox;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controller-Klasse für das Chat-System innerhalb der Spieloberfläche.
|
||||||
|
*
|
||||||
|
* <p>Verwaltet das Anzeigen von Chatnachrichten, das Eingabefeld für eigene Nachrichten sowie den
|
||||||
|
* Senden-Button. Unterstützt drei Arten von Nachrichten: - Player-to-Player (Privat) - Lobby-Chat
|
||||||
|
* (Raum) - Globaler Chat (Serverweit)
|
||||||
|
*
|
||||||
|
* <p>Nachrichten werden in einem {@link VBox}-Container als {@link Label} angezeigt. Eigene
|
||||||
|
* Nachrichten werden über {@link #onSendToNetwork(String)} an das Netzwerkprotokoll weitergeleitet,
|
||||||
|
* während eingehende Nachrichten über {@link #receiveMessage(String, String)} verarbeitet und
|
||||||
|
* angezeigt werden.
|
||||||
|
*
|
||||||
|
* <p>Hinweis: Einige TODOs stehen in der zugehörigen FXML-Datei
|
||||||
|
*/
|
||||||
|
public class ChatController {
|
||||||
|
|
||||||
|
@FXML private VBox chatVBox;
|
||||||
|
|
||||||
|
@FXML private TextField inputField;
|
||||||
|
|
||||||
|
@FXML private Button sendButton;
|
||||||
|
|
||||||
|
@FXML private ScrollPane chatScrollPane;
|
||||||
|
|
||||||
|
private static final int CHAT_PADDING = 20;
|
||||||
|
|
||||||
|
/** Initialisiert den ChatController nach dem Laden der FXML. */
|
||||||
|
public void initialize() {
|
||||||
|
inputField.setOnAction(event -> sendMessage());
|
||||||
|
chatScrollPane.vvalueProperty().bind(chatVBox.heightProperty());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Diese Methode wird vom Senden-Button oder Enter ausgelöst. Sie gibt die eigene Nachricht an
|
||||||
|
* das Netzwerkprotokoll weiter.
|
||||||
|
*/
|
||||||
|
@FXML
|
||||||
|
private void sendMessage() {
|
||||||
|
String message = inputField.getText().trim();
|
||||||
|
if (!message.isEmpty()) {
|
||||||
|
inputField.clear();
|
||||||
|
|
||||||
|
// Hier wird die eigene Nachricht ans Netzwerkprotokoll übergeben
|
||||||
|
onSendToNetwork(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Diese Funktion muss vom Netzwerkprotokoll aufgerufen werden, wenn eine neue Nachricht von
|
||||||
|
* einem anderen Spieler kommt.
|
||||||
|
*
|
||||||
|
* @param player Name des Spielers
|
||||||
|
* @param message Nachricht des Spielers
|
||||||
|
*/
|
||||||
|
public void receiveMessage(String player, String message) {
|
||||||
|
String time = LocalTime.now().format(DateTimeFormatter.ofPattern("HH:mm"));
|
||||||
|
Label label = new Label("[" + time + "] " + player + ": " + message);
|
||||||
|
label.getStyleClass().add("info-text");
|
||||||
|
label.setWrapText(true); // Zeilenumbruch aktivieren
|
||||||
|
label.maxWidthProperty().bind(chatVBox.widthProperty().subtract(CHAT_PADDING));
|
||||||
|
chatVBox.getChildren().add(label);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schnittstelle zum Netzwerkprotokoll. Diese Funktion wird automatisch aufgerufen, wenn der
|
||||||
|
* Benutzer eine eigene Nachricht sendet.
|
||||||
|
*
|
||||||
|
* @param message Nachricht, die der Benutzer abgeschickt hat
|
||||||
|
*/
|
||||||
|
public void onSendToNetwork(String message) {
|
||||||
|
// TODO: Netzwerkcode einfügen
|
||||||
|
receiveMessage("Du", message);
|
||||||
|
}
|
||||||
|
}
|
||||||
+27
@@ -0,0 +1,27 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui;
|
||||||
|
|
||||||
|
import javafx.fxml.FXML;
|
||||||
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.layout.VBox;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controller für die Casino-Spielfläche.
|
||||||
|
*
|
||||||
|
* <p>Verantwortlich für: - die Darstellung des Pokertisches und der Spieleroberfläche, - die
|
||||||
|
* Verarbeitung von Benutzereingaben, - die Schnittstelle zur GameEngine und zum Netzwerkprotokoll.
|
||||||
|
*
|
||||||
|
* <p>Hinweise: - Die Methode `onTableClick()` dient aktuell nur als Test-Logik. Sie ist ggf. nicht
|
||||||
|
* mehr funktionsfähig und wird zukünftig durch die finale Spielinteraktion ersetzt.
|
||||||
|
*/
|
||||||
|
public class CasinoGameController {
|
||||||
|
|
||||||
|
@FXML private Label welcomeText;
|
||||||
|
@FXML private VBox casinoTable;
|
||||||
|
|
||||||
|
// TODO: Test-Logik: wird durch echte Spielinteraktionen ersetzt,
|
||||||
|
// sobald die GameEngine fertig ist
|
||||||
|
@FXML
|
||||||
|
public void onTableClick() {
|
||||||
|
welcomeText.setText("Einsatz akzeptiert!");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import javafx.application.Application;
|
||||||
|
import javafx.fxml.FXMLLoader;
|
||||||
|
import javafx.scene.Scene;
|
||||||
|
import javafx.stage.Stage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hauptklasse für das Casino-Spiel-UI.
|
||||||
|
*
|
||||||
|
* <p>Startet die JavaFX-Anwendung, lädt die grafische Oberfläche aus der FXML-Datei und
|
||||||
|
* initialisiert die Haupt-Stage für das Spiel.
|
||||||
|
*
|
||||||
|
* <p>Aufgaben: - Lädt die FXML-Oberfläche "/ui-structure/Casinogameui.fxml". - Lädt das
|
||||||
|
* Anwendungs-Icon aus "/images/logoinverted.png". - Startet die Anwendung im Vollbildmodus.
|
||||||
|
*/
|
||||||
|
public class CasinoGameUI extends Application {
|
||||||
|
|
||||||
|
private static final int DEFAULT_WIDTH = 1200;
|
||||||
|
private static final int DEFAULT_HEIGHT = 800;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Startet die Haupt-Stage der Anwendung.
|
||||||
|
*
|
||||||
|
* @param stage Die vom System bereitgestellte Haupt-Stage.
|
||||||
|
* @throws IOException Wenn die FXML-Datei oder Ressourcen nicht geladen werden können.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void start(Stage stage) throws IOException {
|
||||||
|
FXMLLoader fxmlLoader =
|
||||||
|
new FXMLLoader(CasinoGameUI.class.getResource("/ui-structure/casinogameui.fxml"));
|
||||||
|
Scene scene = new Scene(fxmlLoader.load(), DEFAULT_WIDTH, DEFAULT_HEIGHT);
|
||||||
|
stage.setTitle("Casono (GAME)");
|
||||||
|
|
||||||
|
String iconPath = getClass().getResource("/images/logoinverted.png").toExternalForm();
|
||||||
|
stage.getIcons().add(new javafx.scene.image.Image(iconPath));
|
||||||
|
stage.setScene(scene);
|
||||||
|
stage.setFullScreen(true);
|
||||||
|
stage.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Startpunkt der Anwendung.
|
||||||
|
*
|
||||||
|
* @param args Befehlszeilenargumente.
|
||||||
|
*/
|
||||||
|
public static void main(String[] args) {
|
||||||
|
launch();
|
||||||
|
}
|
||||||
|
}
|
||||||
+542
@@ -0,0 +1,542 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.gameuicomponents;
|
||||||
|
|
||||||
|
import java.net.CookieHandler;
|
||||||
|
import java.net.CookieManager;
|
||||||
|
import java.net.CookiePolicy;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
import javafx.application.Platform;
|
||||||
|
import javafx.geometry.Insets;
|
||||||
|
import javafx.geometry.Pos;
|
||||||
|
import javafx.scene.Scene;
|
||||||
|
import javafx.scene.control.Alert;
|
||||||
|
import javafx.scene.control.Button;
|
||||||
|
import javafx.scene.control.ButtonType;
|
||||||
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.control.TextField;
|
||||||
|
import javafx.scene.image.Image;
|
||||||
|
import javafx.scene.image.ImageView;
|
||||||
|
import javafx.scene.input.KeyCode;
|
||||||
|
import javafx.scene.layout.HBox;
|
||||||
|
import javafx.scene.layout.Priority;
|
||||||
|
import javafx.scene.layout.StackPane;
|
||||||
|
import javafx.scene.layout.VBox;
|
||||||
|
import javafx.scene.shape.Rectangle;
|
||||||
|
import javafx.scene.web.WebEngine;
|
||||||
|
import javafx.scene.web.WebView;
|
||||||
|
import javafx.stage.Stage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Experimenteller integrierter Browser für Casono.
|
||||||
|
*
|
||||||
|
* <p>Diese Klasse implementiert einen einfachen eingebetteten Webbrowser auf Basis von {@link
|
||||||
|
* javafx.scene.web.WebView}. Der Browser dient primär als Hilfswerkzeug innerhalb des Spiels, um
|
||||||
|
* externe Inhalte wie Webseiten oder Videos anzuzeigen.
|
||||||
|
*
|
||||||
|
* <p>Status Der Browser befindet sich derzeit in einer experimentellen Phase. Einige
|
||||||
|
* Sicherheitsmechanismen basieren auf experimentellen KI-gestützten Empfehlungen und können sich in
|
||||||
|
* zukünftigen Versionen noch ändern.
|
||||||
|
*
|
||||||
|
* <p>Zweck Der Browser wird aktuell experimentell genutzt, um: Pokerregeln direkt im Spiel zu
|
||||||
|
* erklären Hilfeseiten oder Dokumentationen anzuzeigen Videos (z.B. Tutorials oder Erklärungen)
|
||||||
|
* über Plattformen wie YouTube abzuspielen
|
||||||
|
*
|
||||||
|
* <p>Sicherheitsmechanismen Da externe Webseiten geladen werden können, wurden einige grundlegende
|
||||||
|
* Schutzmaßnahmen integriert: - HTTPS-Zwang für Webseiten - Whitelist für bekannte Domains -
|
||||||
|
* Warnung bei unbekannten Webseiten - JavaScript standardmäßig deaktiviert (man kann es jedoch für
|
||||||
|
* Google etc. einschalten) - Popup-Blocker - Automatische Cookie-Löschung beim Schließen
|
||||||
|
*/
|
||||||
|
public class CasinoBrowserController {
|
||||||
|
|
||||||
|
private static final Set<String> TRUSTED_DOMAINS = new HashSet<>();
|
||||||
|
|
||||||
|
private static final CookieManager COOKIE_MANAGER =
|
||||||
|
new CookieManager(null, CookiePolicy.ACCEPT_ORIGINAL_SERVER);
|
||||||
|
|
||||||
|
private static final org.apache.logging.log4j.Logger LOGGER =
|
||||||
|
org.apache.logging.log4j.LogManager.getLogger(CasinoBrowserController.class);
|
||||||
|
|
||||||
|
private static final int LOGO_HEIGHT = 40;
|
||||||
|
private static final int CORNER_RADIUS = 40;
|
||||||
|
private static final int HBOX_SPACIN = 10;
|
||||||
|
private static final int VBOX_SPACING = 10;
|
||||||
|
private static final int PADDING_SIZE = 10;
|
||||||
|
private static final int WINDOW_WIDTH = 1200;
|
||||||
|
private static final int WINDOW_HEIGHT = 800;
|
||||||
|
private static final int ALERT_WIDTH = 600;
|
||||||
|
private static final int ALERT_HEIGHT = 300;
|
||||||
|
private static final String LOGO_PATH = "/images/logoinverted.png";
|
||||||
|
private static final String LOGO_PATH_MAIN = "/images/logo.png";
|
||||||
|
|
||||||
|
static {
|
||||||
|
CookieHandler.setDefault(COOKIE_MANAGER);
|
||||||
|
|
||||||
|
TRUSTED_DOMAINS.add("wikipedia.org");
|
||||||
|
TRUSTED_DOMAINS.add("github.com");
|
||||||
|
TRUSTED_DOMAINS.add("oracle.com");
|
||||||
|
TRUSTED_DOMAINS.add("stackoverflow.com");
|
||||||
|
TRUSTED_DOMAINS.add("docs.oracle.com");
|
||||||
|
TRUSTED_DOMAINS.add("developer.mozilla.org");
|
||||||
|
TRUSTED_DOMAINS.add("maven.apache.org");
|
||||||
|
TRUSTED_DOMAINS.add("gradle.org");
|
||||||
|
TRUSTED_DOMAINS.add("spring.io");
|
||||||
|
TRUSTED_DOMAINS.add("jetbrains.com");
|
||||||
|
TRUSTED_DOMAINS.add("google.com");
|
||||||
|
TRUSTED_DOMAINS.add("bing.com");
|
||||||
|
TRUSTED_DOMAINS.add("duckduckgo.com");
|
||||||
|
TRUSTED_DOMAINS.add("metager.de");
|
||||||
|
TRUSTED_DOMAINS.add("unibas.ch");
|
||||||
|
TRUSTED_DOMAINS.add("mojeek.com");
|
||||||
|
TRUSTED_DOMAINS.add("searx.be ");
|
||||||
|
TRUSTED_DOMAINS.add("startpage.com");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean javascriptEnabled = false;
|
||||||
|
|
||||||
|
/** Löscht alle gespeicherten Cookies der aktuellen Browser-Sitzung. */
|
||||||
|
private static void clearCookies() {
|
||||||
|
try {
|
||||||
|
COOKIE_MANAGER.getCookieStore().removeAll();
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Öffnet ein neues Browserfenster und lädt eine angegebene Webseite.
|
||||||
|
*
|
||||||
|
* <p>Falls die Webseite nicht zur Liste vertrauenswürdiger Domains gehört, wird der Benutzer
|
||||||
|
* gefragt, ob die Seite dennoch geladen werden soll.
|
||||||
|
*
|
||||||
|
* @param url die Startadresse der Webseite, die geladen werden soll
|
||||||
|
*/
|
||||||
|
public static void open(String url) {
|
||||||
|
Platform.runLater(
|
||||||
|
() -> {
|
||||||
|
Stage stage = new Stage();
|
||||||
|
|
||||||
|
WebView webView = new WebView();
|
||||||
|
WebEngine engine = webView.getEngine();
|
||||||
|
|
||||||
|
engine.setJavaScriptEnabled(false);
|
||||||
|
|
||||||
|
configurePopupBlocker(engine);
|
||||||
|
|
||||||
|
webView.getStyleClass().add("web-view");
|
||||||
|
|
||||||
|
StackPane webContainer = createWebContainer(webView);
|
||||||
|
|
||||||
|
Rectangle clip = new Rectangle();
|
||||||
|
clip.setArcWidth(CORNER_RADIUS);
|
||||||
|
clip.setArcHeight(CORNER_RADIUS);
|
||||||
|
webView.setClip(clip);
|
||||||
|
|
||||||
|
webView.widthProperty()
|
||||||
|
.addListener((o, a, b) -> clip.setWidth(b.doubleValue()));
|
||||||
|
webView.heightProperty()
|
||||||
|
.addListener((o, a, b) -> clip.setHeight(b.doubleValue()));
|
||||||
|
|
||||||
|
ImageView browserLogo = loadLogos(stage);
|
||||||
|
|
||||||
|
TextField urlField = createUrlField(url);
|
||||||
|
Label securityLabel = createSecurityLabel();
|
||||||
|
|
||||||
|
Button jsToggle = createJsToggle(engine);
|
||||||
|
Button backBtn = createBackButton(engine);
|
||||||
|
Button fwdBtn = createForwardButton(engine);
|
||||||
|
Button reloadBtn = createReloadButton(engine);
|
||||||
|
Button closeBtn = createCloseButton(stage, webView);
|
||||||
|
configureUrlEvents(engine, urlField, securityLabel);
|
||||||
|
|
||||||
|
HBox taskbar =
|
||||||
|
new HBox(
|
||||||
|
HBOX_SPACIN,
|
||||||
|
browserLogo,
|
||||||
|
backBtn,
|
||||||
|
fwdBtn,
|
||||||
|
reloadBtn,
|
||||||
|
urlField,
|
||||||
|
jsToggle,
|
||||||
|
securityLabel,
|
||||||
|
closeBtn);
|
||||||
|
|
||||||
|
taskbar.getStyleClass().add("taskbar-browser");
|
||||||
|
taskbar.setAlignment(Pos.CENTER_LEFT);
|
||||||
|
|
||||||
|
Scene scene = createScene(taskbar, webContainer);
|
||||||
|
|
||||||
|
configureKeyEvents(scene, engine);
|
||||||
|
|
||||||
|
stage.setScene(scene);
|
||||||
|
stage.setTitle("Casono Browser");
|
||||||
|
loadUrlSafely(engine, url, securityLabel);
|
||||||
|
stage.show();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WebView Container
|
||||||
|
*
|
||||||
|
* @param webView WebView Inhalt
|
||||||
|
* @return StackPane Container
|
||||||
|
*/
|
||||||
|
public static StackPane createWebContainer(WebView webView) {
|
||||||
|
StackPane webContainer = new StackPane(webView);
|
||||||
|
webContainer.getStyleClass().add("browser-web-view");
|
||||||
|
VBox.setVgrow(webContainer, Priority.ALWAYS);
|
||||||
|
|
||||||
|
return webContainer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Popup Blocker
|
||||||
|
*
|
||||||
|
* @param engine WebEngine nutzen
|
||||||
|
*/
|
||||||
|
public static void configurePopupBlocker(WebEngine engine) {
|
||||||
|
engine.setCreatePopupHandler(
|
||||||
|
config -> {
|
||||||
|
Alert alert = new Alert(Alert.AlertType.WARNING);
|
||||||
|
alert.setTitle("Popup blockiert");
|
||||||
|
alert.setHeaderText(null);
|
||||||
|
alert.setContentText("Popup wurde aus Sicherheitsgründen blockiert.");
|
||||||
|
|
||||||
|
try {
|
||||||
|
var stream = CasinoBrowserController.class.getResourceAsStream(LOGO_PATH);
|
||||||
|
Image logo = new Image(stream);
|
||||||
|
|
||||||
|
// Variable 'streamM' abgekürzt, um das 100-Zeichen-Limit (LineLength)
|
||||||
|
// einzuhalten
|
||||||
|
var streamM =
|
||||||
|
CasinoBrowserController.class.getResourceAsStream(LOGO_PATH_MAIN);
|
||||||
|
Image logomain = new Image(streamM);
|
||||||
|
|
||||||
|
if (logo != null) {
|
||||||
|
Stage alertStage = (Stage) alert.getDialogPane().getScene().getWindow();
|
||||||
|
if (alertStage != null) {
|
||||||
|
alertStage.getIcons().add(logo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (logomain != null) {
|
||||||
|
ImageView logoView = new ImageView(logomain);
|
||||||
|
logoView.setFitHeight(LOGO_HEIGHT);
|
||||||
|
logoView.setPreserveRatio(true);
|
||||||
|
alert.setGraphic(logoView);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOGGER.error("Logo konnte nicht geladen werden");
|
||||||
|
}
|
||||||
|
|
||||||
|
alert.showAndWait();
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logos laden
|
||||||
|
*
|
||||||
|
* @param stage Fenster Stage
|
||||||
|
* @return ImageView Logo
|
||||||
|
*/
|
||||||
|
private static ImageView loadLogos(Stage stage) {
|
||||||
|
ImageView browserLogo = new ImageView();
|
||||||
|
|
||||||
|
try {
|
||||||
|
var stream = CasinoBrowserController.class.getResourceAsStream(LOGO_PATH);
|
||||||
|
Image logo = new Image(stream);
|
||||||
|
|
||||||
|
// Variable 'streamM' abgekürzt, um das 100-Zeichen-Limit (LineLength)
|
||||||
|
// einzuhalten
|
||||||
|
var streamM = CasinoBrowserController.class.getResourceAsStream(LOGO_PATH_MAIN);
|
||||||
|
Image logomain = new Image(streamM);
|
||||||
|
|
||||||
|
if (logo != null) {
|
||||||
|
stage.getIcons().add(logo);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (logomain != null) {
|
||||||
|
browserLogo.setImage(logomain);
|
||||||
|
browserLogo.setFitHeight(LOGO_HEIGHT);
|
||||||
|
browserLogo.setPreserveRatio(true);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOGGER.error("Logo konnte nicht geladen werden");
|
||||||
|
}
|
||||||
|
|
||||||
|
return browserLogo;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* URL Feld
|
||||||
|
*
|
||||||
|
* @param url Start URL
|
||||||
|
* @return TextField Eingabe
|
||||||
|
*/
|
||||||
|
public static TextField createUrlField(String url) {
|
||||||
|
TextField urlField = new TextField(url);
|
||||||
|
urlField.getStyleClass().add("gray-input-field");
|
||||||
|
HBox.setHgrow(urlField, Priority.ALWAYS);
|
||||||
|
|
||||||
|
return urlField;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sicherheits Label
|
||||||
|
*
|
||||||
|
* @return Label Anzeige
|
||||||
|
*/
|
||||||
|
public static Label createSecurityLabel() {
|
||||||
|
Label securityLabel = new Label("SICHER");
|
||||||
|
securityLabel.getStyleClass().add("security-label");
|
||||||
|
|
||||||
|
return securityLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JS Toggle
|
||||||
|
*
|
||||||
|
* @param engine WebEngine nutzen
|
||||||
|
* @return Button Umschalten
|
||||||
|
*/
|
||||||
|
public static Button createJsToggle(WebEngine engine) {
|
||||||
|
Button jsToggle = new Button("JS EINSCHALTEN");
|
||||||
|
jsToggle.getStyleClass().add("red-button");
|
||||||
|
|
||||||
|
jsToggle.setOnAction(
|
||||||
|
e -> {
|
||||||
|
javascriptEnabled = !javascriptEnabled;
|
||||||
|
engine.setJavaScriptEnabled(javascriptEnabled);
|
||||||
|
|
||||||
|
if (javascriptEnabled) {
|
||||||
|
jsToggle.setText("JS AUSSCHALTEN");
|
||||||
|
jsToggle.getStyleClass().removeAll("red-button");
|
||||||
|
jsToggle.getStyleClass().add("yellow-button");
|
||||||
|
|
||||||
|
} else {
|
||||||
|
jsToggle.setText("JS EINSCHALTEN");
|
||||||
|
jsToggle.getStyleClass().removeAll("yellow-button");
|
||||||
|
jsToggle.getStyleClass().add("red-button");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return jsToggle;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zurück Button
|
||||||
|
*
|
||||||
|
* @param engine WebEngine nutzen
|
||||||
|
* @return Button Zurück
|
||||||
|
*/
|
||||||
|
public static Button createBackButton(WebEngine engine) {
|
||||||
|
Button backBtn = new Button("<");
|
||||||
|
backBtn.getStyleClass().add("gray-button");
|
||||||
|
backBtn.setOnAction(
|
||||||
|
e -> {
|
||||||
|
if (engine.getHistory().getCurrentIndex() > 0) {
|
||||||
|
engine.getHistory().go(-1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return backBtn;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vorwärts Button
|
||||||
|
*
|
||||||
|
* @param engine WebEngine nutzen
|
||||||
|
* @return Button Vorwärts
|
||||||
|
*/
|
||||||
|
public static Button createForwardButton(WebEngine engine) {
|
||||||
|
Button fwdBtn = new Button(">");
|
||||||
|
fwdBtn.getStyleClass().add("gray-button");
|
||||||
|
fwdBtn.setOnAction(
|
||||||
|
e -> {
|
||||||
|
if (engine.getHistory().getCurrentIndex()
|
||||||
|
< engine.getHistory().getEntries().size() - 1) {
|
||||||
|
engine.getHistory().go(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return fwdBtn;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reload Button
|
||||||
|
*
|
||||||
|
* @param engine WebEngine nutzen
|
||||||
|
* @return Button Reload
|
||||||
|
*/
|
||||||
|
public static Button createReloadButton(WebEngine engine) {
|
||||||
|
Button reloadBtn = new Button("⟳");
|
||||||
|
reloadBtn.getStyleClass().add("gray-button");
|
||||||
|
reloadBtn.setOnAction(e -> engine.reload());
|
||||||
|
return reloadBtn;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close Button
|
||||||
|
*
|
||||||
|
* @param stage Fenster Stage
|
||||||
|
* @param webView WebView Inhalt
|
||||||
|
* @return Button Schließen
|
||||||
|
*/
|
||||||
|
public static Button createCloseButton(Stage stage, WebView webView) {
|
||||||
|
Button closeBtn = new Button("X");
|
||||||
|
closeBtn.getStyleClass().add("red-button");
|
||||||
|
closeBtn.setOnAction(
|
||||||
|
e -> {
|
||||||
|
webView.getEngine().load("about:blank");
|
||||||
|
clearCookies();
|
||||||
|
stage.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
return closeBtn;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* URL Events
|
||||||
|
*
|
||||||
|
* @param engine WebEngine nutzen
|
||||||
|
* @param urlField URL Textfeld
|
||||||
|
* @param securityLabel Sicherheits Label
|
||||||
|
*/
|
||||||
|
public static void configureUrlEvents(
|
||||||
|
WebEngine engine, TextField urlField, Label securityLabel) {
|
||||||
|
urlField.setOnAction(e -> loadUrlSafely(engine, urlField.getText(), securityLabel));
|
||||||
|
engine.locationProperty().addListener((obs, o, n) -> urlField.setText(n));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scene erstellen
|
||||||
|
*
|
||||||
|
* @param taskbar Taskbar HBox
|
||||||
|
* @param webContainer Web Container
|
||||||
|
* @return Scene Fenster
|
||||||
|
*/
|
||||||
|
public static Scene createScene(HBox taskbar, StackPane webContainer) {
|
||||||
|
VBox root = new VBox(VBOX_SPACING, taskbar, webContainer);
|
||||||
|
root.getStyleClass().add("browser-root");
|
||||||
|
root.setPadding(new Insets(PADDING_SIZE));
|
||||||
|
|
||||||
|
Scene scene = new Scene(root, WINDOW_WIDTH, WINDOW_HEIGHT);
|
||||||
|
|
||||||
|
var css = CasinoBrowserController.class.getResource("/ui-structure/casinogameui.css");
|
||||||
|
|
||||||
|
if (css != null) {
|
||||||
|
scene.getStylesheets().add(css.toExternalForm());
|
||||||
|
}
|
||||||
|
|
||||||
|
return scene;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Key Events
|
||||||
|
*
|
||||||
|
* @param scene Scene Fenster
|
||||||
|
* @param engine WebEngine nutzen
|
||||||
|
*/
|
||||||
|
public static void configureKeyEvents(Scene scene, WebEngine engine) {
|
||||||
|
scene.setOnKeyPressed(
|
||||||
|
event -> {
|
||||||
|
if (event.getCode() == KeyCode.F5) {
|
||||||
|
engine.reload();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lädt eine URL in den Browser, nachdem grundlegende Sicherheitsprüfungen durchgeführt wurden.
|
||||||
|
*
|
||||||
|
* <p>Vor dem Laden einer Seite werden folgende Prüfungen durchgeführt: - Überprüfung des
|
||||||
|
* Protokolls (nur HTTPS erlaubt) - Überprüfung der Domain gegen eine Whitelist - Schutz vor
|
||||||
|
* Domain-Spoofing (Domain-Vortäuschung)
|
||||||
|
*
|
||||||
|
* <p>Falls eine Domain nicht als vertrauenswürdig eingestuft wird, muss der Benutzer
|
||||||
|
* bestätigen, dass die Seite dennoch geöffnet werden darf.
|
||||||
|
*
|
||||||
|
* @param engine der WebEngine-Renderer des Browsers
|
||||||
|
* @param url die zu ladende Webadresse
|
||||||
|
* @param securityLabel Label zur Anzeige des aktuellen Sicherheitsstatus
|
||||||
|
*/
|
||||||
|
private static void loadUrlSafely(WebEngine engine, String url, Label securityLabel) {
|
||||||
|
try {
|
||||||
|
if (!url.startsWith("http")) {
|
||||||
|
url = "https://" + url;
|
||||||
|
}
|
||||||
|
|
||||||
|
URI uri = new URI(url);
|
||||||
|
|
||||||
|
// HTTPS Pflicht
|
||||||
|
if (!"https".equalsIgnoreCase(uri.getScheme())) {
|
||||||
|
securityLabel.setText("BLOCKIERT");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String host = uri.getHost();
|
||||||
|
if (host == null) {
|
||||||
|
securityLabel.setText("ERROR");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sicherer Domain Check
|
||||||
|
boolean trusted =
|
||||||
|
TRUSTED_DOMAINS.stream()
|
||||||
|
.anyMatch(domain -> host.equals(domain) || host.endsWith("." + domain));
|
||||||
|
|
||||||
|
if (!trusted) {
|
||||||
|
if (!showUnknownWebsiteAlert(host)) {
|
||||||
|
securityLabel.setText("BLOCKIERT");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
securityLabel.setText("UNBEKANNT");
|
||||||
|
} else {
|
||||||
|
securityLabel.setText("SICHER");
|
||||||
|
}
|
||||||
|
engine.load(uri.toString());
|
||||||
|
} catch (Exception e) {
|
||||||
|
securityLabel.setText("ERROR");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zeigt Warnung an.
|
||||||
|
*
|
||||||
|
* @param host Website Host
|
||||||
|
* @return OK gedrückt
|
||||||
|
*/
|
||||||
|
private static boolean showUnknownWebsiteAlert(String host) {
|
||||||
|
Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
|
||||||
|
|
||||||
|
alert.setTitle("Unbekannte Website");
|
||||||
|
alert.setHeaderText("Diese Website ist nicht bekannt");
|
||||||
|
alert.setContentText(
|
||||||
|
host
|
||||||
|
+ "\n\nDiese Seite ist nicht vom "
|
||||||
|
+ "Casono Browser verifiziert.\nMöchten Sie sie trotzdem öffnen?");
|
||||||
|
|
||||||
|
var stream = CasinoBrowserController.class.getResourceAsStream(LOGO_PATH);
|
||||||
|
Image logo = new Image(stream);
|
||||||
|
if (logo != null) {
|
||||||
|
Stage stage = (Stage) alert.getDialogPane().getScene().getWindow();
|
||||||
|
if (stage != null) {
|
||||||
|
stage.getIcons().add(logo);
|
||||||
|
}
|
||||||
|
ImageView logomain = new ImageView(logo);
|
||||||
|
logomain.setFitHeight(LOGO_HEIGHT);
|
||||||
|
logomain.setPreserveRatio(true);
|
||||||
|
alert.setGraphic(logomain);
|
||||||
|
}
|
||||||
|
|
||||||
|
alert.getDialogPane().setPrefSize(ALERT_WIDTH, ALERT_HEIGHT);
|
||||||
|
Optional<ButtonType> result = alert.showAndWait();
|
||||||
|
|
||||||
|
if (result.isEmpty() || result.get() != ButtonType.OK) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
+138
@@ -0,0 +1,138 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.gameuicomponents;
|
||||||
|
|
||||||
|
import javafx.application.Platform;
|
||||||
|
import javafx.fxml.FXML;
|
||||||
|
import javafx.scene.control.TextField;
|
||||||
|
import javafx.scene.input.KeyCode;
|
||||||
|
import javafx.scene.input.KeyEvent;
|
||||||
|
import javafx.scene.input.MouseEvent;
|
||||||
|
import javafx.scene.layout.HBox;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controller für die interaktive Taskleiste innerhalb der Poker-UI.
|
||||||
|
*
|
||||||
|
* <p>Verantwortlich für: - Drag-and-Drop-Verschieben der Taskleiste, - Eingabe und Verwaltung von
|
||||||
|
* Spieleinsätzen, - Steuerung allgemeiner Menüfunktionen wie Exit.
|
||||||
|
*/
|
||||||
|
public class TaskbarController {
|
||||||
|
|
||||||
|
private static final org.apache.logging.log4j.Logger LOGGER =
|
||||||
|
org.apache.logging.log4j.LogManager.getLogger(CasinoBrowserController.class);
|
||||||
|
|
||||||
|
@FXML private HBox taskbar;
|
||||||
|
@FXML private TextField taskbarInput;
|
||||||
|
|
||||||
|
private double xOffset = 0;
|
||||||
|
private double yOffset = 0;
|
||||||
|
private static final double TASKBAR_SCALE = 0.9;
|
||||||
|
private static final int MIN_CREDITS = 5;
|
||||||
|
private static final int MAX_CREDITS = 100000;
|
||||||
|
private static final int CREDIT_STEP = 5;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wird aufgerufen, wenn die Taskleiste mit der Maus gedrückt wird. Speichert die relative
|
||||||
|
* Position, um später korrekt zu verschieben.
|
||||||
|
*
|
||||||
|
* @param event Das Mausereignis
|
||||||
|
*/
|
||||||
|
@FXML
|
||||||
|
private void onTaskbarPressed(MouseEvent event) {
|
||||||
|
xOffset = event.getSceneX() - taskbar.getLayoutX();
|
||||||
|
yOffset = event.getSceneY() - taskbar.getLayoutY();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wird aufgerufen, während die Taskleiste mit der Maus gezogen wird. Aktualisiert die Position
|
||||||
|
* und skaliert die Taskleiste leicht zur visuellen Rückmeldung.
|
||||||
|
*
|
||||||
|
* <p>TODO: Es muss noch gefixt werden, dass die Taskleiste nicht aus dem Fenster verschwinden
|
||||||
|
* kann.
|
||||||
|
*
|
||||||
|
* @param event Das Mausereignis
|
||||||
|
*/
|
||||||
|
@FXML
|
||||||
|
private void onTaskbarDragged(MouseEvent event) {
|
||||||
|
taskbar.setLayoutX(event.getSceneX() - xOffset);
|
||||||
|
taskbar.setLayoutY(event.getSceneY() - yOffset);
|
||||||
|
|
||||||
|
taskbar.setScaleX(TASKBAR_SCALE);
|
||||||
|
taskbar.setScaleY(TASKBAR_SCALE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wird aufgerufen, wenn die Maus über der Taskleiste losgelassen wird. Setzt die Skalierung der
|
||||||
|
* Taskleiste wieder auf Normalgröße.
|
||||||
|
*
|
||||||
|
* @param event Das Mausereignis
|
||||||
|
*/
|
||||||
|
@FXML
|
||||||
|
private void onTaskbarReleased(MouseEvent event) {
|
||||||
|
taskbar.setScaleX(1.0);
|
||||||
|
taskbar.setScaleY(1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wird aufgerufen, wenn im Textfeld die Enter-Taste gedrückt wird.
|
||||||
|
*
|
||||||
|
* @param event Das Tastaturereignis
|
||||||
|
*/
|
||||||
|
@FXML
|
||||||
|
private void onInputSubmitted(KeyEvent event) {
|
||||||
|
if (event.getCode() == KeyCode.ENTER) {
|
||||||
|
processBet();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wird aufgerufen, wenn der Submit-Button in der Taskleiste gedrückt wird. Löst die
|
||||||
|
* Verarbeitung des Einsatzes aus.
|
||||||
|
*/
|
||||||
|
@FXML
|
||||||
|
private void onInputSubmittedAction() {
|
||||||
|
processBet();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wird aufgerufen, wenn der Exit-Button in der Taskleiste gedrückt wird.
|
||||||
|
*
|
||||||
|
* <p>TODO: Logik implementieren, um zur Lobby zurückzukehren, ohne die gesamte Anwendung zu
|
||||||
|
* schließen (kein System.exit/Platform.exit).
|
||||||
|
*/
|
||||||
|
@FXML
|
||||||
|
private void onExitButtonClick() {
|
||||||
|
Platform.exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verarbeitet den im Textfeld eingegebenen Einsatz. Es werden ausschließlich ganzzahlige Werte
|
||||||
|
* im Bereich von 5 bis 100.000 Credits akzeptiert, die einem Vielfachen von 5 entsprechen
|
||||||
|
* (5er-Schritte). Der Einsatz wird aktuell nur auf der Konsole ausgegeben.
|
||||||
|
*/
|
||||||
|
private void processBet() {
|
||||||
|
String input = taskbarInput.getText();
|
||||||
|
try {
|
||||||
|
int credits = Integer.parseInt(input.trim());
|
||||||
|
|
||||||
|
if (credits >= MIN_CREDITS && credits <= MAX_CREDITS && credits % CREDIT_STEP == 0) {
|
||||||
|
// TODO: Credits müssen an die GameEngine gesendet werden
|
||||||
|
LOGGER.info("Einsatz gesetzt: {} Casono Credits", credits);
|
||||||
|
taskbarInput.clear();
|
||||||
|
} else {
|
||||||
|
LOGGER.info("Fehler: Nur 5er-Schritte (5, 10, ... 100.000) erlaubt!");
|
||||||
|
}
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
LOGGER.info("Fehler: Bitte nur eine Zahl eingeben!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Öffnet den integrierten Casono Webbrowser.
|
||||||
|
*
|
||||||
|
* <p>TODO: Ersetze die Start-URL durch die offizielle Projekt-Website (z.B. Tipps & Tricks
|
||||||
|
* Seite), sobald die Inhalte für Strategien und Support bereitstehen.
|
||||||
|
*/
|
||||||
|
@FXML
|
||||||
|
private void onBrowserButtonClick() {
|
||||||
|
CasinoBrowserController.open("wikipedia.org");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.client.ui.lobbyui;
|
||||||
|
|
||||||
|
/** Main UI application for Casono. Loads the main FXML layout and sets up the stage. */
|
||||||
|
import java.io.IOException;
|
||||||
|
import javafx.application.Application;
|
||||||
|
import javafx.fxml.FXMLLoader;
|
||||||
|
import javafx.scene.Scene;
|
||||||
|
import javafx.stage.Stage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JavaFX Application class for the Casono main UI.
|
||||||
|
*
|
||||||
|
* <p>Standardkonstruktor für die Anwendung.
|
||||||
|
*/
|
||||||
|
public class Casinomainui extends Application {
|
||||||
|
|
||||||
|
/** Standardkonstruktor. */
|
||||||
|
public Casinomainui() {
|
||||||
|
// Standardkonstruktor
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final int SCENE_WIDTH = 1200;
|
||||||
|
private static final int SCENE_HEIGHT = 800;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
/**
|
||||||
|
* Starts the JavaFX application and loads the main UI.
|
||||||
|
*
|
||||||
|
* @param stage The primary stage for this application.
|
||||||
|
* @throws IOException If loading the FXML fails.
|
||||||
|
*/
|
||||||
|
public void start(Stage stage) throws IOException {
|
||||||
|
FXMLLoader fxmlLoader =
|
||||||
|
new FXMLLoader(getClass().getResource("/ui-structure/Casinomainui.fxml"));
|
||||||
|
Scene scene = new Scene(fxmlLoader.load(), SCENE_WIDTH, SCENE_HEIGHT);
|
||||||
|
stage.setTitle("Casono");
|
||||||
|
javafx.scene.image.Image icon =
|
||||||
|
new javafx.scene.image.Image(
|
||||||
|
getClass().getResource("/images/logoinverted.png").toExternalForm());
|
||||||
|
stage.getIcons().add(icon);
|
||||||
|
stage.setScene(scene);
|
||||||
|
stage.setFullScreen(true);
|
||||||
|
stage.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main entry point for launching the application.
|
||||||
|
*
|
||||||
|
* @param args Command line arguments
|
||||||
|
*/
|
||||||
|
public static void main(String[] args) {
|
||||||
|
launch();
|
||||||
|
}
|
||||||
|
}
|
||||||
+73
@@ -0,0 +1,73 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.client.ui.lobbyui;
|
||||||
|
|
||||||
|
import javafx.application.Platform;
|
||||||
|
import javafx.fxml.FXML;
|
||||||
|
import javafx.scene.control.Button;
|
||||||
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.image.Image;
|
||||||
|
import javafx.scene.image.ImageView;
|
||||||
|
import javafx.scene.layout.AnchorPane;
|
||||||
|
import javafx.scene.layout.VBox;
|
||||||
|
import javafx.scene.shape.Rectangle;
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
|
||||||
|
/** Controller for the Casono main UI lobby. Handles UI initialization and user actions. */
|
||||||
|
public class CasinomainuiController {
|
||||||
|
private static final Logger LOGGER = LogManager.getLogger(CasinomainuiController.class);
|
||||||
|
|
||||||
|
@FXML private AnchorPane rootPane;
|
||||||
|
@FXML private Label titleLabel;
|
||||||
|
@FXML private Label subtitleLabel;
|
||||||
|
@FXML private ImageView logoView;
|
||||||
|
@FXML private Rectangle greenBox;
|
||||||
|
@FXML private Button exitbutton;
|
||||||
|
@FXML private VBox casinoTable;
|
||||||
|
|
||||||
|
private LobbyButtonTranslationManager translationManager;
|
||||||
|
private LobbyButtonGridManager gridManager;
|
||||||
|
private int nextButtonId = 1;
|
||||||
|
|
||||||
|
public CasinomainuiController() {
|
||||||
|
// Default constructor
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Initializes the UI components and sets default values. */
|
||||||
|
@FXML
|
||||||
|
public void initialize() {
|
||||||
|
titleLabel.setText("Casono");
|
||||||
|
subtitleLabel.setText("Texas Hold'em Poker");
|
||||||
|
logoView.setImage(new Image(getClass().getResource("/images/logo.png").toExternalForm()));
|
||||||
|
|
||||||
|
translationManager = new LobbyButtonTranslationManager();
|
||||||
|
gridManager =
|
||||||
|
new LobbyButtonGridManager(new javafx.scene.layout.GridPane(), translationManager);
|
||||||
|
casinoTable.getChildren().clear();
|
||||||
|
casinoTable.getChildren().add(gridManager.getGridPane());
|
||||||
|
gridManager.renderLobbyButtons();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Handles the exit button action to close the application. */
|
||||||
|
@FXML
|
||||||
|
public void handleexitbutton() {
|
||||||
|
Platform.exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Handles creation of a new lobby button. */
|
||||||
|
@FXML
|
||||||
|
public void handleCreateLobbyButton() {
|
||||||
|
if (translationManager.isFull()) {
|
||||||
|
LOGGER.warn("Grid voll! Keine weiteren Lobbys moeglich.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int buttonId = nextButtonId++;
|
||||||
|
int lobbyId = gridManager.createLobby();
|
||||||
|
try {
|
||||||
|
translationManager.addLobbyButton(buttonId, lobbyId);
|
||||||
|
LOGGER.info("ButtonID: {}, LobbyID: {}", buttonId, lobbyId);
|
||||||
|
gridManager.renderLobbyButtons();
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOGGER.error("Fehler beim Hinzufügen: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+114
@@ -0,0 +1,114 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.client.ui.lobbyui;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages the grid for lobby buttons and rendering. Uses LobbyButtonTranslationManager for mapping
|
||||||
|
* ButtonID to LobbyID.
|
||||||
|
*/
|
||||||
|
import java.util.Map;
|
||||||
|
import javafx.scene.control.Button;
|
||||||
|
import javafx.scene.image.Image;
|
||||||
|
import javafx.scene.image.ImageView;
|
||||||
|
import javafx.scene.layout.GridPane;
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages the grid for lobby buttons and rendering. Uses LobbyButtonTranslationManager for mapping
|
||||||
|
* ButtonID to LobbyID.
|
||||||
|
*/
|
||||||
|
public class LobbyButtonGridManager {
|
||||||
|
private static final Logger LOGGER = LogManager.getLogger(LobbyButtonGridManager.class);
|
||||||
|
|
||||||
|
/** GridPane for the button grid. */
|
||||||
|
private final GridPane gridPane;
|
||||||
|
|
||||||
|
/** Manager for mapping ButtonID to LobbyID. */
|
||||||
|
private final LobbyButtonTranslationManager translationManager;
|
||||||
|
|
||||||
|
/** Number of rows in the grid. */
|
||||||
|
private static final int ROWS = 2;
|
||||||
|
|
||||||
|
/** Number of columns in the grid. */
|
||||||
|
private static final int COLS = 4;
|
||||||
|
|
||||||
|
/** Path to the button image. */
|
||||||
|
private static final String BUTTON_IMAGE_PATH = "/images/logo.png";
|
||||||
|
|
||||||
|
/** Max random lobby id. */
|
||||||
|
private static final int MAX_RANDOM_LOBBY_ID = 10000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor for the GridManager.
|
||||||
|
*
|
||||||
|
* @param gridPane the GridPane for rendering
|
||||||
|
* @param translationManager the manager for mapping ButtonID to LobbyID
|
||||||
|
*/
|
||||||
|
public LobbyButtonGridManager(
|
||||||
|
GridPane gridPane, LobbyButtonTranslationManager translationManager) {
|
||||||
|
this.gridPane = gridPane;
|
||||||
|
this.translationManager = translationManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders all lobby buttons in the grid. Creates a button for each mapping with image and event
|
||||||
|
* handler.
|
||||||
|
*/
|
||||||
|
public void renderLobbyButtons() {
|
||||||
|
gridPane.getChildren().clear();
|
||||||
|
int index = 0;
|
||||||
|
Map<Integer, Integer> mapping = translationManager.getButtonIdToLobbyId();
|
||||||
|
if (mapping.isEmpty()) {
|
||||||
|
// No buttons to render
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (Map.Entry<Integer, Integer> entry : mapping.entrySet()) {
|
||||||
|
int buttonId = entry.getKey();
|
||||||
|
Button btn = new Button();
|
||||||
|
btn.setId("lobbyBtn-" + buttonId);
|
||||||
|
btn.setGraphic(
|
||||||
|
new ImageView(new Image(getClass().getResourceAsStream(BUTTON_IMAGE_PATH))));
|
||||||
|
btn.setOnAction(
|
||||||
|
e -> {
|
||||||
|
Integer lobbyId = translationManager.getLobbyIdForButton(buttonId);
|
||||||
|
if (lobbyId != null) {
|
||||||
|
joinLobby(lobbyId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
int row = index / COLS;
|
||||||
|
int col = index % COLS;
|
||||||
|
gridPane.add(btn, col, row);
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Placeholder for lobby creation logic. Returns a generated lobbyId.
|
||||||
|
*
|
||||||
|
* @return The generated lobbyId
|
||||||
|
*/
|
||||||
|
public int createLobby() {
|
||||||
|
// TODO: Replace with actual lobby creation logic
|
||||||
|
int lobbyId = (int) (Math.random() * MAX_RANDOM_LOBBY_ID + 1);
|
||||||
|
LOGGER.info("Lobby created: {}", lobbyId);
|
||||||
|
return lobbyId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Placeholder for joining a lobby.
|
||||||
|
*
|
||||||
|
* @param lobbyId The lobbyId to join
|
||||||
|
*/
|
||||||
|
public void joinLobby(int lobbyId) {
|
||||||
|
// TODO: Replace with actual join logic
|
||||||
|
LOGGER.info("Joining lobby: {}", lobbyId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Getter for the GridPane.
|
||||||
|
*
|
||||||
|
* @return The GridPane for the button grid
|
||||||
|
*/
|
||||||
|
public javafx.scene.layout.GridPane getGridPane() {
|
||||||
|
return gridPane;
|
||||||
|
}
|
||||||
|
}
|
||||||
+72
@@ -0,0 +1,72 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.client.ui.lobbyui;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verwaltet das Mapping zwischen Button-IDs und Lobby-IDs rein im Speicher. Keine Dateioperationen,
|
||||||
|
* nur Laufzeitdatenstruktur.
|
||||||
|
*/
|
||||||
|
public class LobbyButtonTranslationManager {
|
||||||
|
/** Maximale Anzahl an Buttons/Lobbys */
|
||||||
|
private static final int MAX_BUTTONS = 8;
|
||||||
|
|
||||||
|
/** Zuordnung ButtonID → LobbyID */
|
||||||
|
private final Map<Integer, Integer> buttonIdToLobbyId = new HashMap<>();
|
||||||
|
|
||||||
|
/** Konstruktor: initialisiert die Zuordnung leer. */
|
||||||
|
public LobbyButtonTranslationManager() {
|
||||||
|
// Zuordnung bleibt leer beim Start
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prüft, ob das Grid voll ist (MAX_BUTTONS erreicht).
|
||||||
|
*
|
||||||
|
* @return true, wenn Grid voll; sonst false
|
||||||
|
*/
|
||||||
|
public boolean isFull() {
|
||||||
|
return buttonIdToLobbyId.size() >= MAX_BUTTONS;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fügt eine Zuordnung ButtonID → LobbyID hinzu.
|
||||||
|
*
|
||||||
|
* @param buttonId Die ID des Buttons
|
||||||
|
* @param lobbyId Die ID der Lobby
|
||||||
|
* @throws Exception wenn das Grid voll ist
|
||||||
|
*/
|
||||||
|
public void addLobbyButton(int buttonId, int lobbyId) throws Exception {
|
||||||
|
if (isFull()) {
|
||||||
|
throw new Exception("Grid is full!");
|
||||||
|
}
|
||||||
|
buttonIdToLobbyId.put(buttonId, lobbyId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entfernt eine Zuordnung für die gegebene ButtonID.
|
||||||
|
*
|
||||||
|
* @param buttonId Die ID des zu entfernenden Buttons
|
||||||
|
*/
|
||||||
|
public void removeLobbyButton(int buttonId) {
|
||||||
|
buttonIdToLobbyId.remove(buttonId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gibt die LobbyID für eine gegebene ButtonID zurück.
|
||||||
|
*
|
||||||
|
* @param buttonId Die ButtonID
|
||||||
|
* @return Die zugehoerige LobbyID oder null, falls nicht vorhanden
|
||||||
|
*/
|
||||||
|
public Integer getLobbyIdForButton(int buttonId) {
|
||||||
|
return buttonIdToLobbyId.get(buttonId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gibt die gesamte Zuordnung ButtonID → LobbyID zurück.
|
||||||
|
*
|
||||||
|
* @return Map aller Zuordnungen
|
||||||
|
*/
|
||||||
|
public Map<Integer, Integer> getButtonIdToLobbyId() {
|
||||||
|
return buttonIdToLobbyId;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,63 @@
|
|||||||
package ch.unibas.dmi.dbis.cs108.casono.server;
|
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;
|
||||||
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
|
||||||
|
/** Application class for starting the server. */
|
||||||
public class ServerApp {
|
public class ServerApp {
|
||||||
|
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) {
|
public static void start(String arg) {
|
||||||
int port = Integer.parseInt(arg);
|
int port = Integer.parseInt(arg);
|
||||||
System.out.println("You've selected the server. It will accept connections at port " + port);
|
|
||||||
|
Logger logger = LogManager.getLogger(ServerApp.class);
|
||||||
|
logger.info("Starting server at port {}", port);
|
||||||
|
|
||||||
|
EventBus eventBus = new 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(
|
||||||
|
DisconnectEvent.class, event -> userRegistry.onDisconnect(event.sessionId()));
|
||||||
|
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
|
||||||
|
scheduler.scheduleAtFixedRate(
|
||||||
|
new UserCleanupJob(
|
||||||
|
userRegistry, Duration.ofSeconds(USER_CLEANUP_JOB_RECONNECT_THRESHOLD)),
|
||||||
|
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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.domain.message;
|
||||||
|
|
||||||
|
/** Represents a message sent by someone */
|
||||||
|
public record Message(String sender, String content) {}
|
||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.domain.message;
|
||||||
|
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserRegistry;
|
||||||
|
|
||||||
|
public class MessageManager {
|
||||||
|
private final UserRegistry userRegistry;
|
||||||
|
|
||||||
|
public MessageManager(UserRegistry userRegistry) {
|
||||||
|
this.userRegistry = userRegistry;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void broadcast(Message message) {
|
||||||
|
userRegistry.getAllUsers().forEach(user -> user.enqueueMessage(message));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.domain.user;
|
||||||
|
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.domain.message.Message;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionId;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.ArrayDeque;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Queue;
|
||||||
|
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||||
|
|
||||||
|
/** Represents an authenticated user on the server. */
|
||||||
|
public class User {
|
||||||
|
private final UserId id;
|
||||||
|
private final String name;
|
||||||
|
private SessionId sessionId;
|
||||||
|
private Instant disconnectedAt;
|
||||||
|
private final Queue<Message> messages;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new User with the given ID, name and session.
|
||||||
|
*
|
||||||
|
* @param id the unique identifier for this user
|
||||||
|
* @param name the display name of this user
|
||||||
|
* @param sessionId the session currently associated with this user
|
||||||
|
*/
|
||||||
|
public User(UserId id, String name, SessionId sessionId) {
|
||||||
|
this.id = id;
|
||||||
|
this.name = name;
|
||||||
|
this.sessionId = sessionId;
|
||||||
|
this.disconnectedAt = null;
|
||||||
|
this.messages = new ConcurrentLinkedQueue<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the ID of this user.
|
||||||
|
*
|
||||||
|
* @return the user ID
|
||||||
|
*/
|
||||||
|
public UserId getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the display name of this user.
|
||||||
|
*
|
||||||
|
* @return the user name
|
||||||
|
*/
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the session currently associated with this user, if any.
|
||||||
|
*
|
||||||
|
* @return an Optional containing the session ID, or empty if disconnected
|
||||||
|
*/
|
||||||
|
public Optional<SessionId> getSessionId() {
|
||||||
|
return Optional.ofNullable(sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the time at which this user disconnected, if applicable.
|
||||||
|
*
|
||||||
|
* @return an Optional containing the disconnect timestamp, or empty if connected
|
||||||
|
*/
|
||||||
|
public Optional<Instant> getDisconnectedAt() {
|
||||||
|
return Optional.ofNullable(disconnectedAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Associates this user with a new session, clearing the disconnect timestamp.
|
||||||
|
*
|
||||||
|
* @param sessionId the new session ID
|
||||||
|
*/
|
||||||
|
public void reassignSession(SessionId sessionId) {
|
||||||
|
this.sessionId = sessionId;
|
||||||
|
this.disconnectedAt = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Marks this user as disconnected by clearing the session and recording the timestamp. */
|
||||||
|
public void markDisconnected() {
|
||||||
|
this.sessionId = null;
|
||||||
|
this.disconnectedAt = Instant.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void enqueueMessage(Message message) {
|
||||||
|
messages.add(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized List<Message> dequeueAllMessages(Message message) {
|
||||||
|
List<Message> allMessages = new ArrayDeque<>(messages).stream().toList();
|
||||||
|
messages.clear();
|
||||||
|
return allMessages;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.domain.user;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.Instant;
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Periodically periodicly run job to remove disconnected users who have exceeded the reconnect
|
||||||
|
* threshold.
|
||||||
|
*/
|
||||||
|
public class UserCleanupJob implements Runnable {
|
||||||
|
private final Logger logger;
|
||||||
|
private final UserRegistry registry;
|
||||||
|
private final Duration reconnectThreshold;
|
||||||
|
|
||||||
|
public UserCleanupJob(UserRegistry registry, Duration reconnectThreshold) {
|
||||||
|
this.logger = LogManager.getLogger(UserCleanupJob.class);
|
||||||
|
this.registry = registry;
|
||||||
|
this.reconnectThreshold = reconnectThreshold;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
logger.debug("Job started.");
|
||||||
|
Instant threshold = Instant.now().minus(reconnectThreshold);
|
||||||
|
|
||||||
|
for (User user : registry.getAllUsers()) {
|
||||||
|
if (user.getDisconnectedAt().isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Instant disconnectedAt = user.getDisconnectedAt().get();
|
||||||
|
if (disconnectedAt.isBefore(threshold)) {
|
||||||
|
if (registry.removeIfStillDisconnected(user.getId())) {
|
||||||
|
logger.info(
|
||||||
|
"Removed expired user {} ({})", user.getName(), user.getId().value());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug("Job finished.");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.domain.user;
|
||||||
|
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionId;
|
||||||
|
|
||||||
|
/** Creates new users, resolving name conflicts automatically. */
|
||||||
|
public class UserFactory {
|
||||||
|
private final UserRegistry registry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new UserFactory backed by the given registry.
|
||||||
|
*
|
||||||
|
* @param registry the registry to register new users to
|
||||||
|
*/
|
||||||
|
public UserFactory(UserRegistry registry) {
|
||||||
|
this.registry = registry;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates and registers a new user with the given name and session. If the name is already
|
||||||
|
* taken, a numeric suffix is appended and incremented until a free name is found (e.g.
|
||||||
|
* Lars_001, Lars_002, ...).
|
||||||
|
*
|
||||||
|
* @param desiredName the preferred display name
|
||||||
|
* @param sessionId the session to associate with the new user
|
||||||
|
* @return the newly created and registered user
|
||||||
|
*/
|
||||||
|
public User create(String desiredName, SessionId sessionId) {
|
||||||
|
var result = registry.registerIfAvailable(desiredName, sessionId);
|
||||||
|
if (result.isPresent()) {
|
||||||
|
return result.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
int suffix = 1;
|
||||||
|
while (true) {
|
||||||
|
String candidate = desiredName + "_" + String.format("%03d", suffix);
|
||||||
|
result = registry.registerIfAvailable(candidate, sessionId);
|
||||||
|
if (result.isPresent()) {
|
||||||
|
return result.get();
|
||||||
|
}
|
||||||
|
suffix++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.domain.user;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/** Represents a unique identifier for a user. */
|
||||||
|
public class UserId {
|
||||||
|
private final UUID value;
|
||||||
|
|
||||||
|
/** Creates a new UserId with a randomly generated UUID. */
|
||||||
|
public UserId() {
|
||||||
|
this.value = UUID.randomUUID();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new UserId with the specified UUID.
|
||||||
|
*
|
||||||
|
* @param value the UUID to use for this UserId
|
||||||
|
*/
|
||||||
|
public UserId(UUID value) {
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the UUID value of this UserId.
|
||||||
|
*
|
||||||
|
* @return the UUID value
|
||||||
|
*/
|
||||||
|
public UUID value() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.domain.user;
|
||||||
|
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionId;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
/** Manages all active users on the server. */
|
||||||
|
public class UserRegistry {
|
||||||
|
private final ConcurrentHashMap<UserId, User> byId = new ConcurrentHashMap<>();
|
||||||
|
private final ConcurrentHashMap<String, User> byName = new ConcurrentHashMap<>();
|
||||||
|
private final ConcurrentHashMap<SessionId, User> bySessionId = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to register a user under the given name atomically. Returns the registered user, or
|
||||||
|
* empty if the name is already taken.
|
||||||
|
*
|
||||||
|
* @param name the desired display name
|
||||||
|
* @param sessionId the session to associate with the new user
|
||||||
|
* @return an Optional containing the new user, or empty if the name was taken
|
||||||
|
*/
|
||||||
|
public synchronized Optional<User> registerIfAvailable(String name, SessionId sessionId) {
|
||||||
|
if (byName.containsKey(name)) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
User user = new User(new UserId(), name, sessionId);
|
||||||
|
byId.put(user.getId(), user);
|
||||||
|
byName.put(user.getName(), user);
|
||||||
|
bySessionId.put(sessionId, user);
|
||||||
|
return Optional.of(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the user with the given ID, but only if they are still disconnected. This prevents
|
||||||
|
* removing a user who has reconnected between the cleanup job's check and its removal call.
|
||||||
|
*
|
||||||
|
* @param userId the ID of the user to remove
|
||||||
|
*/
|
||||||
|
public synchronized boolean removeIfStillDisconnected(UserId userId) {
|
||||||
|
User user = byId.get(userId);
|
||||||
|
if (user == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.getSessionId().isPresent()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
byId.remove(user.getId());
|
||||||
|
byName.remove(user.getName());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marks the user associated with the given session as disconnected, clearing the session
|
||||||
|
* association and recording the disconnect timestamp.
|
||||||
|
*
|
||||||
|
* @param sessionId the session ID of the disconnected client
|
||||||
|
*/
|
||||||
|
// TODO: Add to EventRegistry with DisconnectEvent
|
||||||
|
public synchronized void onDisconnect(SessionId sessionId) {
|
||||||
|
User user = bySessionId.remove(sessionId);
|
||||||
|
if (user == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
user.markDisconnected();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reassociates a user with a new session, effectively restoring them after a reconnect.
|
||||||
|
*
|
||||||
|
* @param userId the ID of the user to reconnect
|
||||||
|
* @param sessionId the new session ID
|
||||||
|
* @return an Optional containing the user, or empty if the user was not found
|
||||||
|
*/
|
||||||
|
public synchronized Optional<User> reassignSession(UserId userId, SessionId sessionId) {
|
||||||
|
User user = byId.get(userId);
|
||||||
|
if (user == null) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
user.reassignSession(sessionId);
|
||||||
|
bySessionId.put(sessionId, user);
|
||||||
|
return Optional.of(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Looks up a user by their session ID.
|
||||||
|
*
|
||||||
|
* @param sessionId the session ID to look up
|
||||||
|
* @return an Optional containing the user, or empty if no user is associated with this session
|
||||||
|
*/
|
||||||
|
public Optional<User> findBySessionId(SessionId sessionId) {
|
||||||
|
return Optional.ofNullable(bySessionId.get(sessionId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all currently registered users.
|
||||||
|
*
|
||||||
|
* @return a collection of all users
|
||||||
|
*/
|
||||||
|
public Collection<User> getAllUsers() {
|
||||||
|
return byId.values();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.network;
|
||||||
|
|
||||||
|
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;
|
||||||
|
import java.net.ServerSocket;
|
||||||
|
import java.net.Socket;
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
|
||||||
|
/** Creates and manages the server socket. Accepts new incoming connections and creates sessions. */
|
||||||
|
public class NetworkManager implements Runnable {
|
||||||
|
private Integer port;
|
||||||
|
private Logger logger;
|
||||||
|
private Thread thread;
|
||||||
|
private Boolean running;
|
||||||
|
private SessionManager sessionManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Starts the internal thread to accept new connections. */
|
||||||
|
public void start() {
|
||||||
|
logger.debug("Starting at port {}", port);
|
||||||
|
thread.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Runs the network manager loop, accepting connections. */
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
try (ServerSocket serverSocket = new ServerSocket(port)) {
|
||||||
|
while (running) {
|
||||||
|
Socket clientSocket = serverSocket.accept();
|
||||||
|
|
||||||
|
logger.debug("Accepted connection from {}", clientSocket.getRemoteSocketAddress());
|
||||||
|
|
||||||
|
sessionManager.create(new TcpTransport(clientSocket));
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.fatal(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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+6
@@ -0,0 +1,6 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.network.events;
|
||||||
|
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionId;
|
||||||
|
|
||||||
|
/** Represents a disconnect event for a session. */
|
||||||
|
public record DisconnectEvent(SessionId sessionId) implements Event {}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.network.events;
|
||||||
|
|
||||||
|
/** Marker interface for events in the event bus system. */
|
||||||
|
interface Event {}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.network.events;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
/** A simple event bus for publishing and subscribing to events. */
|
||||||
|
public class EventBus {
|
||||||
|
private final Map<Class<?>, List<Consumer<Object>>> handlers = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribes a handler to a specific event type.
|
||||||
|
*
|
||||||
|
* @param eventType the class of the event to subscribe to
|
||||||
|
* @param handler the consumer to handle the event
|
||||||
|
*/
|
||||||
|
@SuppressWarnings(
|
||||||
|
"unchecked") // This cast is safe, because handlers only get passed the type they
|
||||||
|
// subscribed to
|
||||||
|
public <T extends Event> void subscribe(Class<T> eventType, Consumer<T> handler) {
|
||||||
|
handlers.computeIfAbsent(eventType, k -> new CopyOnWriteArrayList<>())
|
||||||
|
.add((Consumer<Object>) (Consumer<?>) handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Publishes an event to all subscribed handlers.
|
||||||
|
*
|
||||||
|
* @param event the event to publish
|
||||||
|
*/
|
||||||
|
public <T extends Event> void publish(T event) {
|
||||||
|
Objects.requireNonNull(event, "event must not be null");
|
||||||
|
List<Consumer<Object>> subscribers = handlers.get(event.getClass());
|
||||||
|
if (subscribers != null) {
|
||||||
|
subscribers.forEach(h -> h.accept(event));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.network.parser;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parser to convert the PrimitiveRequest to a Request and performing checks for required fields and
|
||||||
|
* data types
|
||||||
|
*/
|
||||||
|
public interface CommandParser {
|
||||||
|
/**
|
||||||
|
* Parses the provided PrimitiveRequest into a command-specific request
|
||||||
|
*
|
||||||
|
* @param primitiveRequest
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
Request parse(PrimitiveRequest primitiveRequest);
|
||||||
|
}
|
||||||
+36
@@ -0,0 +1,36 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.network.parser;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/** Dispatcher all CommandParser are registered in */
|
||||||
|
public class CommandParserDispatcher {
|
||||||
|
private final Map<String, CommandParser> parsers = new HashMap<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a new CommandParser
|
||||||
|
*
|
||||||
|
* @param command
|
||||||
|
* @param parser the parser class
|
||||||
|
*/
|
||||||
|
public void register(String command, CommandParser parser) {
|
||||||
|
parsers.put(command, parser);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the PrimitiveRequest into a Request using the appropriate CommandParser
|
||||||
|
*
|
||||||
|
* @param primitiveRequest the PrimitiveRequest to parse
|
||||||
|
* @return the parsed Request object
|
||||||
|
*/
|
||||||
|
public Request parse(PrimitiveRequest primitiveRequest) {
|
||||||
|
String command = primitiveRequest.command();
|
||||||
|
CommandParser parser = parsers.get(command);
|
||||||
|
|
||||||
|
if (parser == null) {
|
||||||
|
throw new UnknownCommandException(command);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parser.parse(primitiveRequest);
|
||||||
|
}
|
||||||
|
}
|
||||||
+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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.network.parser;
|
||||||
|
|
||||||
|
/** Used in the PrimitiveRequest class to store the key of a parameter with its respective value */
|
||||||
|
public record Parameter(String key, String value) {}
|
||||||
+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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+7
@@ -0,0 +1,7 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.network.parser;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** Created by the ProtocolParser to allow easy access to the request contents */
|
||||||
|
public record PrimitiveRequest(
|
||||||
|
RequestContext context, String command, List<Parameter> parameters) {}
|
||||||
+109
@@ -0,0 +1,109 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.network.parser;
|
||||||
|
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.tokenizer.RawToken;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.tokenizer.Token;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.tokenizer.TokenClassifier;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.tokenizer.TokenType;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.tokenizer.Tokenizer;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.NoSuchElementException;
|
||||||
|
|
||||||
|
/** ProtocolParser, used to parse RawPacket into a PrimitiveRequest */
|
||||||
|
public class ProtocolParser {
|
||||||
|
/**
|
||||||
|
* Parses the payload of the provided RawPacket
|
||||||
|
*
|
||||||
|
* @param packet the RawPacket containing the recieved data
|
||||||
|
* @return created PrimitiveRequest
|
||||||
|
*/
|
||||||
|
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 RawRequest(command, parameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads the first token, which is ensured to be the command of the request
|
||||||
|
*
|
||||||
|
* @param iterator
|
||||||
|
* @return read command
|
||||||
|
*/
|
||||||
|
private static String readCommand(Iterator<Token> iterator) {
|
||||||
|
Token token = iterator.next();
|
||||||
|
if (token.type() != TokenType.COMMAND) {
|
||||||
|
throw new ProtocolParserException("Expected COMMAND got " + token.type());
|
||||||
|
}
|
||||||
|
|
||||||
|
return token.value();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Iterates over the remaining tokens, calling helperfunctions to validate token type
|
||||||
|
*
|
||||||
|
* @param iterator
|
||||||
|
* @return list containing all parsed parameters
|
||||||
|
*/
|
||||||
|
private static List<Parameter> readParameters(Iterator<Token> iterator) {
|
||||||
|
List<Parameter> parameters = new ArrayList<>();
|
||||||
|
|
||||||
|
try {
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
Token nextToken = iterator.next();
|
||||||
|
|
||||||
|
if (nextToken.type() == TokenType.EOF) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
String key = readKey(nextToken);
|
||||||
|
readSeperator(iterator.next());
|
||||||
|
String value = readValue(iterator.next());
|
||||||
|
|
||||||
|
parameters.add(new Parameter(key, value));
|
||||||
|
}
|
||||||
|
} catch (NoSuchElementException e) {
|
||||||
|
throw new ProtocolParserException("Ran out of tokens while reading parameter");
|
||||||
|
}
|
||||||
|
|
||||||
|
return parameters;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param token the current token
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
private static String readKey(Token token) {
|
||||||
|
if (token.type() != TokenType.KEY) {
|
||||||
|
throw new ProtocolParserException("Expected KEY got " + token.type());
|
||||||
|
}
|
||||||
|
|
||||||
|
return token.value();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param token the current token
|
||||||
|
*/
|
||||||
|
private static void readSeperator(Token token) {
|
||||||
|
if (token.type() != TokenType.SEPARATOR) {
|
||||||
|
throw new ProtocolParserException("Expected SEPARATOR got " + token.type());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param token the current token
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
private static String readValue(Token token) {
|
||||||
|
if (token.type() != TokenType.VALUE) {
|
||||||
|
throw new ProtocolParserException("Expected VALUE got " + token.type());
|
||||||
|
}
|
||||||
|
|
||||||
|
return token.value();
|
||||||
|
}
|
||||||
|
}
|
||||||
+7
@@ -0,0 +1,7 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.network.parser;
|
||||||
|
|
||||||
|
public class ProtocolParserException extends RuntimeException {
|
||||||
|
public ProtocolParserException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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) {}
|
||||||
@@ -0,0 +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 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;
|
||||||
|
}
|
||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.network.parser;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception thrown when the CommandParserDispatcher has no registered handler for the provided
|
||||||
|
* request
|
||||||
|
*/
|
||||||
|
public class UnknownCommandException extends RuntimeException {
|
||||||
|
public UnknownCommandException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
+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";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
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.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.IOException;
|
||||||
|
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 {
|
||||||
|
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.
|
||||||
|
*
|
||||||
|
* @param transport the transport layer for communication
|
||||||
|
* @param eventBus the event bus for publishing events
|
||||||
|
* @throws IOException if an I/O error occurs during initialization
|
||||||
|
*/
|
||||||
|
public Session(
|
||||||
|
TransportLayer transport,
|
||||||
|
EventBus eventBus,
|
||||||
|
CommandParserDispatcher dispatcher,
|
||||||
|
CommandRouter router) {
|
||||||
|
this.id = new SessionId();
|
||||||
|
this.lastActivity = Instant.now();
|
||||||
|
this.transport = transport;
|
||||||
|
this.dispatcher = dispatcher;
|
||||||
|
this.router = router;
|
||||||
|
this.responseQueue = new ArrayBlockingQueue<>(RESPOND_QUEUE_SIZE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the ID of this session.
|
||||||
|
*
|
||||||
|
* @return the session ID
|
||||||
|
*/
|
||||||
|
public SessionId getId() {
|
||||||
|
return this.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the timestamp of the last inbound activity for this session.
|
||||||
|
*
|
||||||
|
* @return an {@link Instant} representing the time of the last inbound activity
|
||||||
|
*/
|
||||||
|
public Instant getLastInboundActivity() {
|
||||||
|
return lastActivity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Updates the timestamp of the last inbound activity for this session. */
|
||||||
|
public void updateLastInboundActivity() {
|
||||||
|
this.lastActivity = Instant.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the TransportLayer of this session
|
||||||
|
*
|
||||||
|
* @return the transport layer of the session
|
||||||
|
*/
|
||||||
|
public TransportLayer getTransport() {
|
||||||
|
return transport;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the BlockingQueue of this session
|
||||||
|
*
|
||||||
|
* @return the queue containing outgoing responses
|
||||||
|
*/
|
||||||
|
public BlockingQueue<PrimitiveResponse> getResponseQueue() {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+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) {}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.network.sessions;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/** Represents a unique identifier for a session. */
|
||||||
|
public class SessionId {
|
||||||
|
private final UUID value;
|
||||||
|
|
||||||
|
/** Creates a new SessionId with a randomly generated UUID. */
|
||||||
|
public SessionId() {
|
||||||
|
this.value = UUID.randomUUID();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new SessionId with the specified UUID.
|
||||||
|
*
|
||||||
|
* @param UUID to use for this SessionId
|
||||||
|
*/
|
||||||
|
public SessionId(UUID value) {
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the UUID value of this SessionId.
|
||||||
|
*
|
||||||
|
* @return the UUID value
|
||||||
|
*/
|
||||||
|
public UUID value() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
+118
@@ -0,0 +1,118 @@
|
|||||||
|
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, SessionHandle> sessions;
|
||||||
|
private final EventBus eventBus;
|
||||||
|
private final Logger logger;
|
||||||
|
private final CommandParserDispatcher dispatcher;
|
||||||
|
private final CommandRouter router;
|
||||||
|
|
||||||
|
/** Constructs a new 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create new Session from provided transport.
|
||||||
|
*
|
||||||
|
* <p>Will create both worker threads and start them.
|
||||||
|
*
|
||||||
|
* @param transport to create session from
|
||||||
|
* @return newly created 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disconnect specified client
|
||||||
|
*
|
||||||
|
* <p>WARNING: Client will be uninformed about disconnect. Use with caution.
|
||||||
|
*
|
||||||
|
* @param id of the client to disconnect
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler for the DisconnectEvent
|
||||||
|
*
|
||||||
|
* @param id of the session that disconnected
|
||||||
|
*/
|
||||||
|
public void onDisconnect(DisconnectEvent event) {
|
||||||
|
logger.debug("Recieved DisconnectEvent event for session {}", event.sessionId().value());
|
||||||
|
|
||||||
|
disconnect(event.sessionId());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a session by its ID.
|
||||||
|
*
|
||||||
|
* @param id the ID of the session to retrieve
|
||||||
|
* @return the session with the specified ID, or null if not found
|
||||||
|
*/
|
||||||
|
public Session getSessionById(SessionId 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +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
|
||||||
|
*/
|
||||||
|
public record RawPacket(int requestId, String payload) {}
|
||||||
+67
@@ -0,0 +1,67 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.network.transport;
|
||||||
|
|
||||||
|
import java.io.DataInputStream;
|
||||||
|
import java.io.DataOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.Socket;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
/** Implements TCP-based transport layer for network communication. */
|
||||||
|
public class TcpTransport implements TransportLayer {
|
||||||
|
private Socket socket;
|
||||||
|
private DataInputStream in;
|
||||||
|
private DataOutputStream out;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new TcpTransport with the given socket.
|
||||||
|
*
|
||||||
|
* @param socket the socket to use for communication
|
||||||
|
* @throws IOException if an I/O error occurs
|
||||||
|
*/
|
||||||
|
public TcpTransport(Socket socket) throws IOException {
|
||||||
|
this.socket = socket;
|
||||||
|
this.in = new DataInputStream(socket.getInputStream());
|
||||||
|
this.out = new DataOutputStream(socket.getOutputStream());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads a string from the socket.
|
||||||
|
*
|
||||||
|
* @return the read RawPacket
|
||||||
|
* @throws IOException if an I/O error occurs
|
||||||
|
*/
|
||||||
|
public RawPacket read() throws IOException {
|
||||||
|
int length = in.readInt();
|
||||||
|
int requestId = in.readInt();
|
||||||
|
byte[] rawPayload = new byte[length];
|
||||||
|
in.readFully(rawPayload);
|
||||||
|
|
||||||
|
String payload = new String(rawPayload, StandardCharsets.UTF_8);
|
||||||
|
return new RawPacket(requestId, payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes a string to the socket.
|
||||||
|
*
|
||||||
|
* @param data the RawPacket's data to write
|
||||||
|
* @throws IOException if an I/O error occurs
|
||||||
|
*/
|
||||||
|
public void write(RawPacket data) throws IOException {
|
||||||
|
int requestId = data.requestId();
|
||||||
|
byte[] rawPayload = data.payload().getBytes(StandardCharsets.UTF_8);
|
||||||
|
|
||||||
|
out.writeInt(requestId);
|
||||||
|
out.writeInt(rawPayload.length);
|
||||||
|
out.write(rawPayload);
|
||||||
|
out.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the socket.
|
||||||
|
*
|
||||||
|
* @throws IOException if an I/O error occurs
|
||||||
|
*/
|
||||||
|
public void close() throws IOException {
|
||||||
|
socket.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.network.transport;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/** Interface for transport layer implementations. */
|
||||||
|
public interface TransportLayer {
|
||||||
|
/**
|
||||||
|
* Reads data from the transport layer.
|
||||||
|
*
|
||||||
|
* @return the read data as a RawPacket
|
||||||
|
* @throws IOException if an I/O error occurs
|
||||||
|
*/
|
||||||
|
RawPacket read() throws IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes data to the transport layer.
|
||||||
|
*
|
||||||
|
* @param data the data to write
|
||||||
|
* @throws IOException if an I/O error occurs
|
||||||
|
*/
|
||||||
|
void write(RawPacket data) throws IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the transport layer.
|
||||||
|
*
|
||||||
|
* @throws IOException if an I/O error occurs
|
||||||
|
*/
|
||||||
|
void close() throws IOException;
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.tokenizer;
|
||||||
|
|
||||||
|
/** Represents a raw (unclassified) token in the tokenizer. */
|
||||||
|
public record RawToken(RawTokenType type, String value, int line, int column) {}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.tokenizer;
|
||||||
|
|
||||||
|
public enum RawTokenType {
|
||||||
|
WORD,
|
||||||
|
STRING,
|
||||||
|
SEPARATOR,
|
||||||
|
NEWLINE,
|
||||||
|
EOF
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.tokenizer;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** Internal state for the tokenizer. */
|
||||||
|
class State {
|
||||||
|
final String input;
|
||||||
|
int pos;
|
||||||
|
int line;
|
||||||
|
int column;
|
||||||
|
final List<RawToken> tokens;
|
||||||
|
|
||||||
|
State(String input) {
|
||||||
|
this.input = input;
|
||||||
|
this.pos = 0;
|
||||||
|
this.line = 1;
|
||||||
|
this.column = 1;
|
||||||
|
this.tokens = new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
char current() {
|
||||||
|
return input.charAt(pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
char peek() {
|
||||||
|
if (pos + 1 >= input.length()) {
|
||||||
|
return '\0';
|
||||||
|
}
|
||||||
|
return input.charAt(pos + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void advance() {
|
||||||
|
pos++;
|
||||||
|
column++;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isEof() {
|
||||||
|
return pos >= input.length();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.tokenizer;
|
||||||
|
|
||||||
|
/** Represents a token in the tokenizer. */
|
||||||
|
public record Token(TokenType type, String value, int line, int column) {}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.tokenizer;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class TokenClassifier {
|
||||||
|
public static List<Token> classify(List<RawToken> rawTokens) {
|
||||||
|
validateFirstToken(rawTokens);
|
||||||
|
|
||||||
|
List<Token> tokens = new ArrayList<>();
|
||||||
|
|
||||||
|
for (int i = 0; i < rawTokens.size(); i++) {
|
||||||
|
RawToken raw = rawTokens.get(i);
|
||||||
|
|
||||||
|
switch (raw.type()) {
|
||||||
|
case SEPARATOR -> {
|
||||||
|
validateSeparator(rawTokens, i);
|
||||||
|
tokens.add(
|
||||||
|
new Token(TokenType.SEPARATOR, raw.value(), raw.line(), raw.column()));
|
||||||
|
}
|
||||||
|
case WORD -> {
|
||||||
|
TokenType type = resolveWordType(rawTokens, i);
|
||||||
|
tokens.add(new Token(type, raw.value(), raw.line(), raw.column()));
|
||||||
|
}
|
||||||
|
case STRING -> {
|
||||||
|
validateString(rawTokens, i);
|
||||||
|
tokens.add(new Token(TokenType.VALUE, raw.value(), raw.line(), raw.column()));
|
||||||
|
}
|
||||||
|
|
||||||
|
case NEWLINE -> {
|
||||||
|
throw new TokenizerException(
|
||||||
|
"Unexpected newline in request", raw.line(), raw.column());
|
||||||
|
}
|
||||||
|
|
||||||
|
case EOF -> {
|
||||||
|
tokens.add(new Token(TokenType.EOF, raw.value(), raw.line(), raw.column()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void validateFirstToken(List<RawToken> rawTokens) {
|
||||||
|
if (rawTokens.isEmpty() || rawTokens.get(0).type() != RawTokenType.WORD) {
|
||||||
|
throw new TokenizerException("Expected COMMAND as first token", 1, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void validateSeparator(List<RawToken> rawTokens, int index) {
|
||||||
|
boolean missingKey = index == 0 || rawTokens.get(index - 1).type() != RawTokenType.WORD;
|
||||||
|
|
||||||
|
boolean missingValue =
|
||||||
|
index + 1 >= rawTokens.size()
|
||||||
|
|| (rawTokens.get(index + 1).type() != RawTokenType.WORD
|
||||||
|
&& rawTokens.get(index + 1).type() != RawTokenType.STRING);
|
||||||
|
|
||||||
|
boolean nextWordIsKey =
|
||||||
|
!missingValue
|
||||||
|
&& rawTokens.get(index + 1).type() == RawTokenType.WORD
|
||||||
|
&& index + 2 < rawTokens.size()
|
||||||
|
&& rawTokens.get(index + 2).type() == RawTokenType.SEPARATOR;
|
||||||
|
|
||||||
|
RawToken separator = rawTokens.get(index);
|
||||||
|
|
||||||
|
if (missingKey) {
|
||||||
|
throw new TokenizerException(
|
||||||
|
"Expected KEY before '='", separator.line(), separator.column());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (missingValue || nextWordIsKey) {
|
||||||
|
throw new TokenizerException(
|
||||||
|
"Expected VALUE after '='", separator.line(), separator.column());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void validateString(List<RawToken> rawTokens, int index) {
|
||||||
|
boolean afterSeparator =
|
||||||
|
index > 0 && rawTokens.get(index - 1).type() == RawTokenType.SEPARATOR;
|
||||||
|
|
||||||
|
if (!afterSeparator) {
|
||||||
|
RawToken token = rawTokens.get(index);
|
||||||
|
throw new TokenizerException("Unexpected string literal", token.line(), token.column());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TokenType resolveWordType(List<RawToken> rawTokens, int index) {
|
||||||
|
boolean isFirst = index == 0;
|
||||||
|
boolean afterNewline = index > 0 && rawTokens.get(index - 1).type() == RawTokenType.NEWLINE;
|
||||||
|
boolean afterSeparator =
|
||||||
|
index > 0 && rawTokens.get(index - 1).type() == RawTokenType.SEPARATOR;
|
||||||
|
|
||||||
|
if (isFirst || afterNewline) {
|
||||||
|
return TokenType.COMMAND;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (afterSeparator) {
|
||||||
|
return TokenType.VALUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
return TokenType.KEY;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.tokenizer;
|
||||||
|
|
||||||
|
/** Enumeration of token types used in the tokenizer. */
|
||||||
|
public enum TokenType {
|
||||||
|
COMMAND,
|
||||||
|
KEY,
|
||||||
|
VALUE,
|
||||||
|
SEPARATOR,
|
||||||
|
EOF
|
||||||
|
}
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.tokenizer;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A static utility class for tokenizing input strings into a sequence of tokens.
|
||||||
|
*
|
||||||
|
* <p>Strings can be enclosed in single quotes and may contain escaped single quotes using a
|
||||||
|
* backslash.
|
||||||
|
*/
|
||||||
|
public class Tokenizer {
|
||||||
|
/**
|
||||||
|
* Tokenizes the given input string into a list of tokens.
|
||||||
|
*
|
||||||
|
* @param input the string to tokenize
|
||||||
|
* @return a list of tokens representing the input
|
||||||
|
* @throws TokenizerException if the input contains unexpected characters, unterminated strings,
|
||||||
|
* or other syntax errors
|
||||||
|
*/
|
||||||
|
public static List<RawToken> tokenize(String input) {
|
||||||
|
State state = new State(input);
|
||||||
|
|
||||||
|
while (!state.isEof()) {
|
||||||
|
char c = state.current();
|
||||||
|
|
||||||
|
if (c == ' ' || c == '\t') {
|
||||||
|
state.advance();
|
||||||
|
|
||||||
|
} else if (c == '\n') {
|
||||||
|
readNewline(state);
|
||||||
|
|
||||||
|
} else if (c == '=') {
|
||||||
|
readSeparator(state);
|
||||||
|
|
||||||
|
} else if (c == '\'') {
|
||||||
|
readString(state);
|
||||||
|
|
||||||
|
} else if (Character.isLetterOrDigit(c) || c == '_') {
|
||||||
|
readWord(state);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
throw new TokenizerException(
|
||||||
|
"Unexpected character '" + c + "'", state.line, state.column);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state.tokens.add(new RawToken(RawTokenType.EOF, null, state.line, state.column));
|
||||||
|
return state.tokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads a newline character and creates a NEWLINE token.
|
||||||
|
*
|
||||||
|
* <p>Updates the line counter and resets the column to 1.
|
||||||
|
*
|
||||||
|
* @param state the current parsing state
|
||||||
|
*/
|
||||||
|
private static void readNewline(State state) {
|
||||||
|
state.tokens.add(new RawToken(RawTokenType.NEWLINE, "", state.line, state.column));
|
||||||
|
state.advance();
|
||||||
|
state.line++;
|
||||||
|
state.column = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads a separator character ('=') and creates a SEPARATOR token.
|
||||||
|
*
|
||||||
|
* @param state the current parsing state
|
||||||
|
*/
|
||||||
|
private static void readSeparator(State state) {
|
||||||
|
state.tokens.add(new RawToken(RawTokenType.SEPARATOR, "=", state.line, state.column));
|
||||||
|
state.advance();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads a word (sequence of alphanumeric characters and underscores) and creates an appropriate
|
||||||
|
* token (COMMAND, KEY, or VALUE) based on the parsing context.
|
||||||
|
*
|
||||||
|
* <p>The token type is determined by the {@link #resolveWordType(State)} method.
|
||||||
|
*
|
||||||
|
* @param state the current parsing state
|
||||||
|
*/
|
||||||
|
private static void readWord(State state) {
|
||||||
|
int startColumn = state.column;
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
|
||||||
|
while (!state.isEof()
|
||||||
|
&& (Character.isLetterOrDigit(state.current()) || state.current() == '_')) {
|
||||||
|
sb.append(state.current());
|
||||||
|
state.advance();
|
||||||
|
}
|
||||||
|
|
||||||
|
state.tokens.add(new RawToken(RawTokenType.WORD, sb.toString(), state.line, startColumn));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads a string literal enclosed in single quotes and creates a VALUE token.
|
||||||
|
*
|
||||||
|
* <p>Supports escaped single quotes using backslash notation (\'). Newlines within strings are
|
||||||
|
* properly tracked for line counting.
|
||||||
|
*
|
||||||
|
* @param state the current parsing state
|
||||||
|
* @throws TokenizerException if the string literal is not terminated before end of input
|
||||||
|
*/
|
||||||
|
private static void readString(State state) {
|
||||||
|
int startColumn = state.column;
|
||||||
|
state.advance();
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
if (state.isEof()) {
|
||||||
|
throw new TokenizerException(
|
||||||
|
"Unterminated string literal", state.line, startColumn);
|
||||||
|
}
|
||||||
|
|
||||||
|
char c = state.current();
|
||||||
|
|
||||||
|
if (c == '\\' && state.peek() == '\'') {
|
||||||
|
sb.append('\'');
|
||||||
|
state.advance();
|
||||||
|
state.advance();
|
||||||
|
|
||||||
|
} else if (c == '\'') {
|
||||||
|
state.advance();
|
||||||
|
break;
|
||||||
|
|
||||||
|
} else {
|
||||||
|
if (c == '\n') {
|
||||||
|
state.line++;
|
||||||
|
state.column = 1;
|
||||||
|
}
|
||||||
|
sb.append(c);
|
||||||
|
state.advance();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state.tokens.add(new RawToken(RawTokenType.STRING, sb.toString(), state.line, startColumn));
|
||||||
|
}
|
||||||
|
}
|
||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.tokenizer;
|
||||||
|
|
||||||
|
/** Exception thrown during tokenization. */
|
||||||
|
public class TokenizerException extends RuntimeException {
|
||||||
|
private final int line;
|
||||||
|
private final int column;
|
||||||
|
|
||||||
|
public TokenizerException(String message, int line, int column) {
|
||||||
|
super(message);
|
||||||
|
this.line = line;
|
||||||
|
this.column = column;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getLine() {
|
||||||
|
return line;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getColumn() {
|
||||||
|
return column;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
package ch.unibas.dmi.dbis.cs108.example;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A simple HelloWorld class.
|
|
||||||
*/
|
|
||||||
public class HelloWorld {
|
|
||||||
|
|
||||||
public static void main(String[] args) {
|
|
||||||
System.out.println("Hello World");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
package ch.unibas.dmi.dbis.cs108.example.gui.javafx;
|
|
||||||
|
|
||||||
import javafx.application.Application;
|
|
||||||
import javafx.scene.Scene;
|
|
||||||
import javafx.scene.control.Label;
|
|
||||||
import javafx.scene.layout.StackPane;
|
|
||||||
import javafx.stage.Stage;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is an example JavaFX-Application.
|
|
||||||
*/
|
|
||||||
public class GUI extends Application {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Launching this method will not work on some platforms.
|
|
||||||
* What you should do is to create a separate main class and launch the GUI class from there as is done in {@link Main}
|
|
||||||
*/
|
|
||||||
public static void main(String[] args) {
|
|
||||||
launch(args);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void start(Stage stage) {
|
|
||||||
String javaVersion = System.getProperty("java.version");
|
|
||||||
String javafxVersion = System.getProperty("javafx.version");
|
|
||||||
Label l = new Label("Hello, JavaFX " + javafxVersion + ", running on Java " + javaVersion + ".");
|
|
||||||
Scene scene = new Scene(new StackPane(l), 640, 480);
|
|
||||||
stage.setScene(scene);
|
|
||||||
stage.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
package ch.unibas.dmi.dbis.cs108.example.gui.javafx;
|
|
||||||
|
|
||||||
import javafx.application.Application;
|
|
||||||
|
|
||||||
public class Main {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is simply a wrapper to launch the {@link GUI} class.
|
|
||||||
* The reason this class exists is documented in {@link GUI#main(String[])}
|
|
||||||
*/
|
|
||||||
public static void main(String[] args) {
|
|
||||||
Application.launch(GUI.class, args);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
@@ -0,0 +1,25 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Configuration status="warn">
|
||||||
|
|
||||||
|
<Appenders>
|
||||||
|
<Console name="Console" target="SYSTEM_OUT">
|
||||||
|
<PatternLayout disableAnsi="false" pattern=
|
||||||
|
"%style{%d{HH:mm:ss.SSS}}{bright,black} %highlight{%-8level}{
|
||||||
|
FATAL=bright red,
|
||||||
|
ERROR=red,
|
||||||
|
WARN=yellow,
|
||||||
|
INFO=blue,
|
||||||
|
DEBUG=cyan,
|
||||||
|
TRACE=white
|
||||||
|
} %style{%-20logger{20}}{magenta} %msg%n"
|
||||||
|
/>
|
||||||
|
</Console>
|
||||||
|
</Appenders>
|
||||||
|
|
||||||
|
<Loggers>
|
||||||
|
<Root level="debug">
|
||||||
|
<AppenderRef ref="Console"/>
|
||||||
|
</Root>
|
||||||
|
</Loggers>
|
||||||
|
|
||||||
|
</Configuration>
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<?import javafx.scene.control.*?>
|
||||||
|
<?import javafx.scene.layout.*?>
|
||||||
|
<?import javafx.geometry.Insets?>
|
||||||
|
<?import javafx.scene.image.*?>
|
||||||
|
|
||||||
|
<!-- Hauptcontainer: Verknüpft die UI mit dem CasinoGameController und lädt casinogameui.css -->
|
||||||
|
<AnchorPane xmlns="http://javafx.com/javafx/21"
|
||||||
|
xmlns:fx="http://javafx.com/fxml/1"
|
||||||
|
fx:controller="ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.CasinoGameController"
|
||||||
|
styleClass="background"
|
||||||
|
stylesheets="@casinogameui.css">
|
||||||
|
|
||||||
|
<GridPane prefWidth="1200" prefHeight="800" AnchorPane.topAnchor="0" AnchorPane.bottomAnchor="0" AnchorPane.leftAnchor="0" AnchorPane.rightAnchor="0">
|
||||||
|
<columnConstraints>
|
||||||
|
<!-- Tisch-Box: 70% -->
|
||||||
|
<ColumnConstraints percentWidth="70.0" hgrow="ALWAYS" />
|
||||||
|
<!-- Leere-Box: 5% -->
|
||||||
|
<ColumnConstraints percentWidth="5.0" hgrow="ALWAYS" />
|
||||||
|
<!-- Chat-Box: 20% -->
|
||||||
|
<ColumnConstraints percentWidth="25.0" hgrow="ALWAYS" />
|
||||||
|
</columnConstraints>
|
||||||
|
|
||||||
|
<!-- Sorgt dafür, dass diese Zeile den gesamten verfügbaren vertikalen Platz nutzt -->
|
||||||
|
<rowConstraints>
|
||||||
|
<RowConstraints vgrow="ALWAYS" />
|
||||||
|
</rowConstraints>
|
||||||
|
|
||||||
|
<children>
|
||||||
|
<!-- Tisch-Box in Spalte 0 -->
|
||||||
|
<StackPane GridPane.columnIndex="0">
|
||||||
|
<!-- Innenabstand: Hält den Inhalt 10 Pixel von oben/unten und 20 Pixel von den Seiten fern -->
|
||||||
|
<padding>
|
||||||
|
<Insets top="20" right="10" bottom="120" left="20"/>
|
||||||
|
</padding>
|
||||||
|
|
||||||
|
<!-- Zentraler Casinotisch: Zentriert den Inhalt und nutzt durch die maximale Größe (1.7976931348623157E308) das gesamte Fenster -->
|
||||||
|
<VBox fx:id="casinoTable"
|
||||||
|
alignment="CENTER"
|
||||||
|
styleClass="casino-table"
|
||||||
|
spacing="20"
|
||||||
|
onMouseClicked="#onTableClick"
|
||||||
|
maxWidth="Infinity"
|
||||||
|
maxHeight="Infinity">
|
||||||
|
|
||||||
|
<!-- TODO: Platzhalter für die Poker-Spielfläche: Hier werden später dynamisch Karten, Chips und Einsätze der Gegner angezeigt -->
|
||||||
|
<HBox alignment="CENTER" spacing="15">
|
||||||
|
<VBox styleClass="dealer-box" prefWidth="80" prefHeight="120" />
|
||||||
|
<VBox styleClass="dealer-box" prefWidth="80" prefHeight="120" />
|
||||||
|
<VBox styleClass="dealer-box" prefWidth="80" prefHeight="120" />
|
||||||
|
</HBox>
|
||||||
|
|
||||||
|
<VBox alignment="CENTER" spacing="10">
|
||||||
|
<!-- Label mit Logo -->
|
||||||
|
<Label text="CAS0NO" styleClass="table-title">
|
||||||
|
<graphic>
|
||||||
|
<ImageView fitHeight="30.0" preserveRatio="true">
|
||||||
|
<image>
|
||||||
|
<Image url="@/images/logo.png" />
|
||||||
|
</image>
|
||||||
|
</ImageView>
|
||||||
|
</graphic>
|
||||||
|
</Label>
|
||||||
|
|
||||||
|
<!-- TODO: Platzhalter: Wird ersetzt, sobald die GameEngine fertig ist und echte Einsätze verarbeiten kann -->
|
||||||
|
<Label fx:id="welcomeText" text="Setzen Sie Ihren Einsatz" styleClass="table-title" />
|
||||||
|
</VBox>
|
||||||
|
</VBox>
|
||||||
|
</StackPane>
|
||||||
|
|
||||||
|
<!-- Leere-Box in Spalte 1: Schafft Platz zwischen dem Tisch-Box und der Chat-Box -->
|
||||||
|
<VBox GridPane.columnIndex="1"
|
||||||
|
alignment="CENTER"
|
||||||
|
minWidth="100"
|
||||||
|
minHeight="100">
|
||||||
|
<!-- Bleibt leer -->
|
||||||
|
</VBox>
|
||||||
|
|
||||||
|
<!-- TODO: Platzhalter: Chat-Box in Spalte 2: -->
|
||||||
|
<fx:include source="components/chatbox.fxml" GridPane.columnIndex="2"/>
|
||||||
|
|
||||||
|
</children>
|
||||||
|
</GridPane>
|
||||||
|
|
||||||
|
<!-- verschiebbare Taskbar -->
|
||||||
|
<AnchorPane>
|
||||||
|
<fx:include fx:id="taskbarInclude" source="gameuicomponents/taskbar.fxml"/>
|
||||||
|
</AnchorPane>
|
||||||
|
|
||||||
|
<!-- Gegner-Status 1: Eines von drei Panels, das Icon, Name und Kontostand des Mitspielers anzeigt -->
|
||||||
|
<GridPane AnchorPane.leftAnchor="0.0"
|
||||||
|
AnchorPane.rightAnchor="0.0"
|
||||||
|
AnchorPane.topAnchor="50">
|
||||||
|
|
||||||
|
<columnConstraints>
|
||||||
|
<ColumnConstraints percentWidth="5"/>
|
||||||
|
<ColumnConstraints percentWidth="16"/>
|
||||||
|
</columnConstraints>
|
||||||
|
|
||||||
|
<children>
|
||||||
|
<fx:include source="gameuicomponents/playerstatus.fxml" GridPane.columnIndex="1"/>
|
||||||
|
</children>
|
||||||
|
</GridPane>
|
||||||
|
|
||||||
|
<!-- Gegner-Status 2: Eines von drei Panels, das Icon, Name und Kontostand des Mitspielers anzeigt -->
|
||||||
|
<GridPane AnchorPane.leftAnchor="0.0"
|
||||||
|
AnchorPane.rightAnchor="0.0"
|
||||||
|
AnchorPane.topAnchor="20">
|
||||||
|
|
||||||
|
<columnConstraints>
|
||||||
|
<ColumnConstraints percentWidth="30"/>
|
||||||
|
<ColumnConstraints percentWidth="16"/>
|
||||||
|
</columnConstraints>
|
||||||
|
|
||||||
|
<children>
|
||||||
|
<fx:include source="gameuicomponents/playerstatus.fxml" GridPane.columnIndex="1"/>
|
||||||
|
</children>
|
||||||
|
</GridPane>
|
||||||
|
|
||||||
|
<!-- Gegner-Status 3: Eines von drei Panels, das Icon, Name und Kontostand des Mitspielers anzeigt -->
|
||||||
|
<GridPane AnchorPane.leftAnchor="0.0"
|
||||||
|
AnchorPane.rightAnchor="0.0"
|
||||||
|
AnchorPane.topAnchor="50">
|
||||||
|
|
||||||
|
<columnConstraints>
|
||||||
|
<ColumnConstraints percentWidth="55"/>
|
||||||
|
<ColumnConstraints percentWidth="16"/>
|
||||||
|
</columnConstraints>
|
||||||
|
|
||||||
|
<children>
|
||||||
|
<fx:include source="gameuicomponents/playerstatus.fxml" GridPane.columnIndex="1"/>
|
||||||
|
</children>
|
||||||
|
</GridPane>
|
||||||
|
</AnchorPane>
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
/* Pixel-Art Font (WICHTIG: Nutze eine Monospace-Schrift für Pixel-Look) */
|
||||||
|
.root {
|
||||||
|
-fx-background-color: #000000;
|
||||||
|
-fx-font-family: "Monospaced", "Courier New";
|
||||||
|
}
|
||||||
|
|
||||||
|
.anchor-pane-bg {
|
||||||
|
-fx-background-color: #ffffff;
|
||||||
|
/* Optional: Bild entfernen oder als Overlay nutzen */
|
||||||
|
-fx-background-image: url("/images/background.png");
|
||||||
|
-fx-background-size: cover;
|
||||||
|
-fx-background-position: center center;
|
||||||
|
-fx-background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* DIE SCHWEBENDE INFO-BOX (PIXEL-STYLE) */
|
||||||
|
.floating-chat-box {
|
||||||
|
-fx-background-color: #0d9e3b;
|
||||||
|
/* Pixel-Ränder: Keine Rundung (0px) oder nur sehr kleine Stufen */
|
||||||
|
-fx-background-radius: 58;
|
||||||
|
-fx-border-radius: 50;
|
||||||
|
-fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10;
|
||||||
|
-fx-border-width: 12;
|
||||||
|
-fx-padding: 20;
|
||||||
|
/* Harter Schatten statt weicher Glow */
|
||||||
|
-fx-effect: dropshadow(one-pass-box, rgba(0,0,0,1), 0, 0, 15, 15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-header {
|
||||||
|
-fx-text-fill: #ffffff;
|
||||||
|
-fx-font-size: 22px;
|
||||||
|
-fx-font-weight: bold;
|
||||||
|
-fx-padding: 0 0 10 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-text {
|
||||||
|
-fx-text-fill: #ffffff;
|
||||||
|
-fx-font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* DER OVALE PIXEL-TISCH */
|
||||||
|
.casino-table {
|
||||||
|
-fx-background-color: #0d9e3b; /* Feste Farbe statt Gradient für Pixel-Look */
|
||||||
|
/* Oval durch festen Radius - Pixel-Art-Ovale sind stufig */
|
||||||
|
-fx-background-radius: 2000;
|
||||||
|
-fx-border-radius: 2000;
|
||||||
|
|
||||||
|
/* Dicker, eckiger Holzrand */
|
||||||
|
-fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10;
|
||||||
|
-fx-border-width: 12;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.table-title {
|
||||||
|
-fx-text-fill: #ffffff;
|
||||||
|
-fx-font-size: 28px;
|
||||||
|
-fx-font-weight: bold;
|
||||||
|
-fx-effect: dropshadow(one-pass-box, #000, 0, 0, 3, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* Hauptcontainer der Statusbox */
|
||||||
|
.player-status-pane {
|
||||||
|
-fx-background-color: rgb(129, 13, 158);
|
||||||
|
-fx-background-radius: 15;
|
||||||
|
-fx-border-radius: 15;
|
||||||
|
|
||||||
|
/* -fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10;
|
||||||
|
-fx-border-width: 2;
|
||||||
|
-fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.5), 10, 0, 0, 4);*/
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-status-pane:hover {
|
||||||
|
-fx-translate-y: -3;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Die zwei inneren Boxen */
|
||||||
|
.status-inner-box-top {
|
||||||
|
-fx-background-color: rgb(13, 93, 158);
|
||||||
|
-fx-background-radius: 19;
|
||||||
|
-fx-border-radius: 15;
|
||||||
|
-fx-padding: 5 10;
|
||||||
|
-fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10;
|
||||||
|
-fx-border-width: 3;
|
||||||
|
-fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.8), 0, 0, 3, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-inner-box-midle {
|
||||||
|
-fx-background-color: rgba(13, 158, 59, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-inner-box-bottom {
|
||||||
|
-fx-background-color: rgb(158, 13, 102);
|
||||||
|
-fx-background-radius: 19;
|
||||||
|
-fx-border-radius: 15;
|
||||||
|
-fx-padding: 5 10;
|
||||||
|
-fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10;
|
||||||
|
-fx-border-width: 3;
|
||||||
|
-fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.8), 0, 0, 3, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-title {
|
||||||
|
/* Titel-Text anpassen */
|
||||||
|
-fx-text-fill: #ffffff;
|
||||||
|
-fx-font-size: 32px;
|
||||||
|
-fx-font-weight: bold;
|
||||||
|
-fx-effect: dropshadow(one-pass-box, #000, 0, 0, 3, 3);
|
||||||
|
-fx-padding: 10 0 0 0; /* Abstand nach oben */
|
||||||
|
}
|
||||||
|
|
||||||
|
.green-box {
|
||||||
|
/* Grüne Box anpassen */
|
||||||
|
-fx-fill: #4CAF50;
|
||||||
|
-fx-stroke: #FFFFFF;
|
||||||
|
-fx-stroke-width: 3;
|
||||||
|
-fx-effect: dropshadow(one-pass-box, #000, 0, 0, 10, 10);
|
||||||
|
-fx-padding: 0 0 40 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* EXIT BUTTON */
|
||||||
|
.button-exit {
|
||||||
|
-fx-background-radius: 12;
|
||||||
|
-fx-border-radius: 12;
|
||||||
|
-fx-font-family: "Courier New";
|
||||||
|
-fx-font-weight: bold;
|
||||||
|
-fx-border-width: 2;
|
||||||
|
-fx-padding: 6 15;
|
||||||
|
-fx-cursor: hand;
|
||||||
|
-fx-background-color: #333333;
|
||||||
|
-fx-border-color: #666666;
|
||||||
|
-fx-text-fill: #CCCCCC;
|
||||||
|
}
|
||||||
|
.button-exit:hover {
|
||||||
|
-fx-translate-y: -3;
|
||||||
|
-fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.8), 0, 0, 3, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* LOBBY ERSTELLEN BUTTON */
|
||||||
|
.button-create-lobby {
|
||||||
|
-fx-background-radius: 12;
|
||||||
|
-fx-border-radius: 12;
|
||||||
|
-fx-font-family: "Courier New";
|
||||||
|
-fx-font-weight: bold;
|
||||||
|
-fx-border-width: 2;
|
||||||
|
-fx-padding: 6 15;
|
||||||
|
-fx-cursor: hand;
|
||||||
|
-fx-background-color: #1a7f2e;
|
||||||
|
-fx-border-color: #2ecc71;
|
||||||
|
-fx-text-fill: #ffffff;
|
||||||
|
}
|
||||||
|
.button-create-lobby:hover {
|
||||||
|
-fx-translate-y: -3;
|
||||||
|
-fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.8), 0, 0, 3, 3);
|
||||||
|
-fx-background-color: #27ae60;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<?import javafx.scene.image.ImageView?>
|
||||||
|
<?import javafx.scene.control.*?>
|
||||||
|
<?import javafx.scene.layout.*?>
|
||||||
|
<?import javafx.geometry.Insets?>
|
||||||
|
|
||||||
|
<AnchorPane xmlns="http://javafx.com"
|
||||||
|
xmlns:fx="http://javafx.com"
|
||||||
|
fx:controller="ch.unibas.dmi.dbis.cs108.casono.client.ui.lobbyui.CasinomainuiController"
|
||||||
|
styleClass="anchor-pane-bg"
|
||||||
|
stylesheets="@Casinomainui.css">
|
||||||
|
|
||||||
|
<GridPane prefWidth="1200" prefHeight="800" AnchorPane.topAnchor="0" AnchorPane.bottomAnchor="0" AnchorPane.leftAnchor="0" AnchorPane.rightAnchor="0">
|
||||||
|
<columnConstraints>
|
||||||
|
<!-- Tisch bekommt 75% vom Fenster -->
|
||||||
|
<ColumnConstraints percentWidth="70.0" hgrow="ALWAYS" />
|
||||||
|
<!-- Info-Box bekommt 25% vom Fenster inkl. 5 prozent buffer -->
|
||||||
|
<ColumnConstraints percentWidth="5.0" hgrow="ALWAYS" />
|
||||||
|
<ColumnConstraints percentWidth="25.0" hgrow="ALWAYS" />
|
||||||
|
</columnConstraints>
|
||||||
|
|
||||||
|
<rowConstraints>
|
||||||
|
<RowConstraints vgrow="ALWAYS" />
|
||||||
|
</rowConstraints>
|
||||||
|
|
||||||
|
<children>
|
||||||
|
<!-- LINKER BEREICH (Tisch) -->
|
||||||
|
<GridPane GridPane.columnIndex="0" style="-fx-background-color: transparent;" alignment="CENTER">
|
||||||
|
<rowConstraints>
|
||||||
|
<RowConstraints percentHeight="25.0" vgrow="ALWAYS" />
|
||||||
|
<RowConstraints percentHeight="75.0" vgrow="ALWAYS" />
|
||||||
|
</rowConstraints>
|
||||||
|
|
||||||
|
<columnConstraints>
|
||||||
|
<ColumnConstraints hgrow="ALWAYS" />
|
||||||
|
</columnConstraints>
|
||||||
|
|
||||||
|
<children>
|
||||||
|
<HBox alignment="CENTER" spacing="20"
|
||||||
|
maxWidth="Infinity"
|
||||||
|
GridPane.rowIndex="0"
|
||||||
|
GridPane.columnIndex="0"
|
||||||
|
GridPane.halignment="CENTER">
|
||||||
|
<ImageView fx:id="logoView"
|
||||||
|
fitHeight="96"
|
||||||
|
fitWidth="96"
|
||||||
|
preserveRatio="true" />
|
||||||
|
<VBox alignment="CENTER" spacing="5">
|
||||||
|
<Label text="placeholder text wird in controller ueberschriebeen"
|
||||||
|
styleClass="table-title"
|
||||||
|
fx:id="titleLabel"
|
||||||
|
alignment="CENTER" />
|
||||||
|
<Label text="placeholder text wird in controller ueberschriebeen"
|
||||||
|
styleClass="table-title"
|
||||||
|
fx:id="subtitleLabel"
|
||||||
|
alignment="CENTER" />
|
||||||
|
</VBox>
|
||||||
|
</HBox>
|
||||||
|
<Button text="EXIT"
|
||||||
|
styleClass="button-exit"
|
||||||
|
fx:id="exitbutton"
|
||||||
|
onAction="#handleexitbutton"
|
||||||
|
GridPane.rowIndex="0"
|
||||||
|
GridPane.columnIndex="0"
|
||||||
|
GridPane.halignment="LEFT"
|
||||||
|
GridPane.valignment="TOP">
|
||||||
|
<GridPane.margin>
|
||||||
|
<Insets top="20" left="20" />
|
||||||
|
</GridPane.margin>
|
||||||
|
</Button>
|
||||||
|
<Button text="Lobby erstellen"
|
||||||
|
styleClass="button-create-lobby"
|
||||||
|
fx:id="createLobbyButton"
|
||||||
|
onAction="#handleCreateLobbyButton"
|
||||||
|
GridPane.rowIndex="0"
|
||||||
|
GridPane.columnIndex="0"
|
||||||
|
GridPane.halignment="LEFT"
|
||||||
|
GridPane.valignment="TOP">
|
||||||
|
<GridPane.margin>
|
||||||
|
<Insets top="60" left="20" />
|
||||||
|
</GridPane.margin>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<VBox fx:id="casinoTable"
|
||||||
|
alignment="CENTER"
|
||||||
|
styleClass="casino-table"
|
||||||
|
spacing="20"
|
||||||
|
maxWidth="Infinity"
|
||||||
|
maxHeight="Infinity"
|
||||||
|
GridPane.rowIndex="1"
|
||||||
|
GridPane.columnIndex="0"
|
||||||
|
GridPane.halignment="CENTER">
|
||||||
|
<GridPane.margin>
|
||||||
|
<Insets left="20" bottom="20" />
|
||||||
|
</GridPane.margin>
|
||||||
|
</VBox>
|
||||||
|
</children>
|
||||||
|
</GridPane>
|
||||||
|
|
||||||
|
|
||||||
|
<VBox GridPane.columnIndex="1"
|
||||||
|
alignment="CENTER"
|
||||||
|
minWidth="100"
|
||||||
|
minHeight="100">
|
||||||
|
<!-- Bleibt leer -->
|
||||||
|
</VBox>
|
||||||
|
|
||||||
|
<!-- RECHTER BEREICH (Info-Box) -->
|
||||||
|
<VBox GridPane.columnIndex="2" alignment="CENTER">
|
||||||
|
<padding>
|
||||||
|
<Insets top="30" right="30" bottom="30" left="10"/>
|
||||||
|
</padding>
|
||||||
|
<VBox VBox.vgrow="ALWAYS" styleClass="floating-chat-box" spacing="10" maxWidth="Infinity">
|
||||||
|
<Label text="== INFO ==" styleClass="chat-header" alignment="CENTER" maxWidth="Infinity"/>
|
||||||
|
<Region minHeight="4" style="-fx-background-color: #ffffff;" />
|
||||||
|
<VBox spacing="15" VBox.vgrow="ALWAYS" styleClass="info-content">
|
||||||
|
<Label text="> CREDITS: 1000" styleClass="info-text"/>
|
||||||
|
<Label text="> JACKPOT: 9999" styleClass="info-text"/>
|
||||||
|
<Label text="> SYSTEM: OK" styleClass="info-text"/>
|
||||||
|
</VBox>
|
||||||
|
</VBox>
|
||||||
|
</VBox>
|
||||||
|
</children>
|
||||||
|
</GridPane>
|
||||||
|
</AnchorPane>
|
||||||
@@ -0,0 +1,251 @@
|
|||||||
|
.root {
|
||||||
|
-fx-background-color: #000000;
|
||||||
|
-fx-font-family: "Monospaced", "Courier New"; /* Schriftart */
|
||||||
|
}
|
||||||
|
|
||||||
|
.background {
|
||||||
|
-fx-background-image: url("/images/background.png");
|
||||||
|
-fx-background-size: cover; /* Bild füllt das ganze Fenster aus */
|
||||||
|
/* Zentriert das Bild und verhindert, dass es sich kachelartig wiederholt */
|
||||||
|
-fx-background-position: center center;
|
||||||
|
-fx-background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-text {
|
||||||
|
-fx-text-fill: #ffffff;
|
||||||
|
-fx-font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.casino-table {
|
||||||
|
-fx-background-color: #0d9e3b;
|
||||||
|
-fx-background-radius: 210;
|
||||||
|
-fx-border-radius: 200;
|
||||||
|
-fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10;
|
||||||
|
-fx-border-width: 12;
|
||||||
|
/*-fx-min-height: 60vh;*/
|
||||||
|
/*-fx-max-height: 60vh;*/
|
||||||
|
/*-fx-min-width: 90vh;*/
|
||||||
|
/*-fx-max-width: 90vh;*/
|
||||||
|
-fx-effect: dropshadow(one-pass-box, rgba(0,0,0,0.8), 0, 0, 15, 15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dealer-box {
|
||||||
|
-fx-background-color: #ffffff;
|
||||||
|
-fx-border-color: #000000;
|
||||||
|
-fx-border-width: 3;
|
||||||
|
-fx-background-radius: 0;
|
||||||
|
-fx-border-radius: 0;
|
||||||
|
-fx-min-width: 70;
|
||||||
|
-fx-min-height: 100;
|
||||||
|
-fx-effect: dropshadow(one-pass-box, rgba(0,0,0,0.8), 0, 0, 4, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-title {
|
||||||
|
-fx-text-fill: #ffffff;
|
||||||
|
-fx-font-size: 28px;
|
||||||
|
-fx-font-weight: bold;
|
||||||
|
-fx-effect: dropshadow(one-pass-box, #000000, 0, 0, 3, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.taskbar {
|
||||||
|
-fx-background-color: rgba(13, 158, 59);
|
||||||
|
-fx-background-radius: 23;
|
||||||
|
-fx-border-radius: 20;
|
||||||
|
-fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10;
|
||||||
|
-fx-border-width: 3;
|
||||||
|
-fx-padding: 10 25;
|
||||||
|
-fx-cursor: move;
|
||||||
|
-fx-effect: dropshadow(one-pass-box, rgba(0,0,0,0.8), 0, 0, 5, 5);
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.taskbar:hover {
|
||||||
|
-fx-border-color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.black-input-field {
|
||||||
|
-fx-background-color: #1a1a1a;
|
||||||
|
-fx-text-fill: #ffffff;
|
||||||
|
-fx-font-family: "Courier New";
|
||||||
|
-fx-font-weight: bold;
|
||||||
|
-fx-background-radius: 8;
|
||||||
|
-fx-border-radius: 8;
|
||||||
|
-fx-border-color: #444444;
|
||||||
|
-fx-border-width: 2;
|
||||||
|
-fx-padding: 5 12;
|
||||||
|
}
|
||||||
|
|
||||||
|
.black-input-field:hover {
|
||||||
|
-fx-translate-y: -3;
|
||||||
|
-fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.8), 0, 0, 3, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.black-input-field:focused {
|
||||||
|
-fx-border-color: #ffffff;
|
||||||
|
-fx-background-color: #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gray-input-field {
|
||||||
|
-fx-background-color: #333333;
|
||||||
|
-fx-text-fill: #ffffff;
|
||||||
|
-fx-font-family: "Courier New";
|
||||||
|
-fx-font-weight: bold;
|
||||||
|
-fx-background-radius: 8;
|
||||||
|
-fx-border-radius: 8;
|
||||||
|
-fx-border-color: #666666;
|
||||||
|
-fx-color-color: #cccccc;
|
||||||
|
-fx-border-width: 2;
|
||||||
|
-fx-padding: 5 12;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gray-input-field:hover {
|
||||||
|
-fx-translate-y: -3;
|
||||||
|
-fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.8), 0, 0, 3, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gray-input-field:focused {
|
||||||
|
-fx-border-color: #ffffff;
|
||||||
|
-fx-background-color: #333333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yellow-button {
|
||||||
|
-fx-background-color: #b8860b;
|
||||||
|
-fx-border-color: #ffd700;
|
||||||
|
-fx-text-fill: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.red-button {
|
||||||
|
-fx-background-color: #8b0000;
|
||||||
|
-fx-border-color: #ff4444;
|
||||||
|
-fx-text-fill: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gray-button {
|
||||||
|
-fx-background-color: #333333;
|
||||||
|
-fx-border-color: #666666;
|
||||||
|
-fx-text-fill: #cccccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gray-button, .yellow-button, .red-button {
|
||||||
|
-fx-background-radius: 12;
|
||||||
|
-fx-border-radius: 12;
|
||||||
|
-fx-font-family: "Courier New";
|
||||||
|
-fx-font-weight: bold;
|
||||||
|
-fx-border-width: 2;
|
||||||
|
-fx-padding: 6 15;
|
||||||
|
-fx-cursor: hand;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gray-button:hover, .yellow-button:hover, .red-button:hover {
|
||||||
|
-fx-translate-y: -3;
|
||||||
|
-fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.8), 0, 0, 3, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-status-pane {
|
||||||
|
-fx-background-color: rgba(13, 158, 59, 0);
|
||||||
|
-fx-background-radius: 15;
|
||||||
|
-fx-border-radius: 15;
|
||||||
|
/* -fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10;
|
||||||
|
-fx-border-width: 2;
|
||||||
|
-fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.5), 10, 0, 0, 4);*/
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-status-pane:hover {
|
||||||
|
-fx-translate-y: -3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-inner-box-top {
|
||||||
|
-fx-background-color: rgba(13, 158, 59);
|
||||||
|
-fx-background-radius: 19;
|
||||||
|
-fx-border-radius: 15;
|
||||||
|
-fx-padding: 5 10;
|
||||||
|
-fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10;
|
||||||
|
-fx-border-width: 3;
|
||||||
|
-fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.8), 0, 0, 3, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-inner-box-midle {
|
||||||
|
-fx-background-color: rgba(13, 158, 59, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-inner-box-bottom {
|
||||||
|
-fx-background-color: rgba(13, 158, 59);
|
||||||
|
-fx-background-radius: 19;
|
||||||
|
-fx-border-radius: 15;
|
||||||
|
-fx-padding: 5 10;
|
||||||
|
-fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10;
|
||||||
|
-fx-border-width: 3;
|
||||||
|
-fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.8), 0, 0, 3, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-label-small {
|
||||||
|
-fx-text-fill: #ffffff;
|
||||||
|
-fx-font-family: "Monospaced"; /* Schriftart */
|
||||||
|
-fx-font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-value-text {
|
||||||
|
-fx-text-fill: #ffffff;
|
||||||
|
-fx-font-family: "Monospaced"; /* Schriftart */
|
||||||
|
-fx-font-size: 15px;
|
||||||
|
-fx-font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-value-money {
|
||||||
|
-fx-text-fill: #ffffff;
|
||||||
|
-fx-font-family: "Monospaced"; /* Schriftart */
|
||||||
|
-fx-font-size: 15px;
|
||||||
|
-fx-font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-circle {
|
||||||
|
-fx-background-color: #000000;
|
||||||
|
-fx-background-radius: 50;
|
||||||
|
-fx-border-color: #ffffff;
|
||||||
|
-fx-border-width: 3;
|
||||||
|
-fx-border-radius: 50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.browser-root {
|
||||||
|
-fx-background-image: url("/images/background.png");
|
||||||
|
-fx-background-size: cover;
|
||||||
|
-fx-background-position: center;
|
||||||
|
-fx-background-color: #0b2d15;
|
||||||
|
}
|
||||||
|
|
||||||
|
.browser-web-view {
|
||||||
|
-fx-background-color: rgb(13, 158, 59);
|
||||||
|
-fx-background-radius: 23;
|
||||||
|
-fx-border-radius: 20;
|
||||||
|
-fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10;
|
||||||
|
-fx-border-width: 3;
|
||||||
|
-fx-padding: 0;
|
||||||
|
-fx-cursor: default;
|
||||||
|
-fx-effect: dropshadow(one-pass-box, rgba(0,0,0,0.8), 0, 0, 5, 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.web-view {
|
||||||
|
-fx-background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.taskbar-browser {
|
||||||
|
-fx-background-color: rgb(13, 158, 59);
|
||||||
|
-fx-background-radius: 23;
|
||||||
|
-fx-border-radius: 20;
|
||||||
|
-fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10;
|
||||||
|
-fx-border-width: 3;
|
||||||
|
-fx-padding: 10 25;
|
||||||
|
-fx-cursor: default;
|
||||||
|
-fx-effect: dropshadow(one-pass-box, rgba(0,0,0,0.8), 0, 0, 5, 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*.taskbar-browser:hover {*/
|
||||||
|
/* -fx-border-color: #ffffff;*/
|
||||||
|
/*}*/
|
||||||
|
|
||||||
|
.security-label {
|
||||||
|
-fx-text-fill: #000000;
|
||||||
|
-fx-font-weight: bold;
|
||||||
|
-fx-font-size: 16px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<?import javafx.scene.control.*?>
|
||||||
|
<?import javafx.scene.layout.*?>
|
||||||
|
<?import javafx.geometry.Insets?>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Dieses Fenster wird später mit den Serveranfragen verknüpft,
|
||||||
|
um Nachrichten von Mitspielern und Systemmeldungen in Echtzeit anzuzeigen.
|
||||||
|
|
||||||
|
Der Chat unterscheidet intern zwischen:
|
||||||
|
- Player-to-Player (Privater 2-Personen-Chat)
|
||||||
|
- Lobby-Chat (Aktueller Raum)
|
||||||
|
- Globaler Chat (Gesamter Server)
|
||||||
|
|
||||||
|
Features, die später implementiert werden sollen:
|
||||||
|
- Schicke Chat-Bubbles mit Namen und Uhrzeit.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<VBox xmlns="http://javafx.com/javafx/21"
|
||||||
|
xmlns:fx="http://javafx.com/fxml/1"
|
||||||
|
fx:controller="ch.unibas.dmi.dbis.cs108.casono.client.ui.chatui.ChatController"
|
||||||
|
alignment="CENTER"
|
||||||
|
stylesheets="@chatui.css">
|
||||||
|
|
||||||
|
<!-- Innenabstand: Schafft oben, rechts und unten 30 Pixel Platz, links nur 10 Pixel (asymmetrisch) -->
|
||||||
|
<padding>
|
||||||
|
<Insets top="30" right="30" bottom="30" left="10"/>
|
||||||
|
</padding>
|
||||||
|
|
||||||
|
<children>
|
||||||
|
<VBox VBox.vgrow="ALWAYS"
|
||||||
|
styleClass="chat-box"
|
||||||
|
spacing="10"
|
||||||
|
maxWidth="Infinity">
|
||||||
|
|
||||||
|
<children>
|
||||||
|
<Label text="== CHAT ==" styleClass="chat-header" alignment="CENTER" maxWidth="Infinity"/>
|
||||||
|
<Region minHeight="4" styleClass="chat-separator"/>
|
||||||
|
|
||||||
|
<!-- Chatnachrichten werden hier hinzugefügt -->
|
||||||
|
<ScrollPane fx:id="chatScrollPane" fitToWidth="true" vbarPolicy="AS_NEEDED" hbarPolicy="NEVER" VBox.vgrow="ALWAYS" styleClass="chat-scroll-pane">
|
||||||
|
<content>
|
||||||
|
<VBox fx:id="chatVBox" spacing="10" styleClass="chat-VBox"/>
|
||||||
|
</content>
|
||||||
|
</ScrollPane>
|
||||||
|
|
||||||
|
<!-- Eingabebereich -->
|
||||||
|
<HBox spacing="10">
|
||||||
|
<!-- TODO: Größe des TextFields dynamisch anpassen -->
|
||||||
|
<TextField fx:id="inputField" HBox.hgrow="ALWAYS" promptText="Nachricht eingeben..." styleClass="gray-input-field" onAction="#sendMessage"/>
|
||||||
|
<Button fx:id="sendButton" text="SENDEN" onAction="#sendMessage" styleClass="yellow-button"/>
|
||||||
|
</HBox>
|
||||||
|
</children>
|
||||||
|
</VBox>
|
||||||
|
</children>
|
||||||
|
</VBox>
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
.chat-box {
|
||||||
|
-fx-background-color: #0d9e3b;
|
||||||
|
-fx-background-radius: 58;
|
||||||
|
-fx-border-radius: 50;
|
||||||
|
-fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10;
|
||||||
|
-fx-border-width: 12;
|
||||||
|
-fx-padding: 20;
|
||||||
|
-fx-effect: dropshadow(one-pass-box, rgba(0,0,0,1), 0, 0, 15, 15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-header {
|
||||||
|
-fx-text-fill: #ffffff;
|
||||||
|
-fx-font-size: 22px;
|
||||||
|
-fx-font-weight: bold;
|
||||||
|
-fx-padding: 0 0 10 0;
|
||||||
|
-fx-effect: dropshadow(one-pass-box, #000000, 0, 0, 3, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-separator {
|
||||||
|
-fx-background-color: #ffffff;
|
||||||
|
-fx-min-height: 4px;
|
||||||
|
-fx-effect: dropshadow(one-pass-box, #000000, 0, 0, 3, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.black-input-field {
|
||||||
|
-fx-background-color: #1a1a1a;
|
||||||
|
-fx-text-fill: #ffffff;
|
||||||
|
-fx-font-family: "Courier New";
|
||||||
|
-fx-font-weight: bold;
|
||||||
|
-fx-background-radius: 8;
|
||||||
|
-fx-border-radius: 8;
|
||||||
|
-fx-border-color: #444444;
|
||||||
|
-fx-border-width: 2;
|
||||||
|
-fx-padding: 5 12;
|
||||||
|
}
|
||||||
|
|
||||||
|
.black-input-field:hover {
|
||||||
|
-fx-translate-y: -3;
|
||||||
|
-fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.8), 0, 0, 3, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.black-input-field:focused {
|
||||||
|
-fx-border-color: #ffffff;
|
||||||
|
-fx-background-color: #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gray-input-field {
|
||||||
|
-fx-background-color: #333333;
|
||||||
|
-fx-text-fill: #ffffff;
|
||||||
|
-fx-font-family: "Courier New";
|
||||||
|
-fx-font-weight: bold;
|
||||||
|
-fx-background-radius: 8;
|
||||||
|
-fx-border-radius: 8;
|
||||||
|
-fx-border-color: #666666;
|
||||||
|
-fx-color-color: #cccccc;
|
||||||
|
-fx-border-width: 2;
|
||||||
|
-fx-padding: 5 12;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gray-input-field:hover {
|
||||||
|
-fx-translate-y: -3;
|
||||||
|
-fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.8), 0, 0, 3, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gray-input-field:focused {
|
||||||
|
-fx-border-color: #ffffff;
|
||||||
|
-fx-background-color: #333333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yellow-button {
|
||||||
|
-fx-background-color: #b8860b;
|
||||||
|
-fx-border-color: #ffd700;
|
||||||
|
-fx-text-fill: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.red-button {
|
||||||
|
-fx-background-color: #8b0000;
|
||||||
|
-fx-border-color: #ff4444;
|
||||||
|
-fx-text-fill: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gray-button {
|
||||||
|
-fx-background-color: #333333;
|
||||||
|
-fx-border-color: #666666;
|
||||||
|
-fx-text-fill: #cccccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gray-button, .yellow-button, .red-button {
|
||||||
|
-fx-background-radius: 12;
|
||||||
|
-fx-border-radius: 12;
|
||||||
|
-fx-font-family: "Courier New";
|
||||||
|
-fx-font-weight: bold;
|
||||||
|
-fx-border-width: 2;
|
||||||
|
-fx-padding: 6 15;
|
||||||
|
-fx-cursor: hand;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gray-button:hover, .yellow-button:hover, .red-button:hover {
|
||||||
|
-fx-translate-y: -3;
|
||||||
|
-fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.8), 0, 0, 3, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-scroll-pane {
|
||||||
|
-fx-background-color: transparent;
|
||||||
|
-fx-background: transparent;
|
||||||
|
-fx-border-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-VBox {
|
||||||
|
-fx-background-color: transparent;
|
||||||
|
-fx-background: transparent;
|
||||||
|
}
|
||||||
|
.scroll-pane {
|
||||||
|
-fx-background-color: transparent;
|
||||||
|
-fx-border-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll-pane .scroll-bar:vertical {
|
||||||
|
-fx-background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll-pane .scroll-bar .track {
|
||||||
|
-fx-background-color: #5c3d10;
|
||||||
|
-fx-background-insets: 0;
|
||||||
|
-fx-background-radius: 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll-pane .scroll-bar .thumb {
|
||||||
|
-fx-background-color: #3d260a;
|
||||||
|
-fx-background-insets: 0;
|
||||||
|
-fx-background-radius: 5;
|
||||||
|
-fx-pref-width: 6;
|
||||||
|
-fx-pref-height: 6;
|
||||||
|
-fx-padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll-pane .scroll-bar:horizontal {
|
||||||
|
-fx-background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll-pane .scroll-bar:horizontal .track {
|
||||||
|
-fx-background-color: #5c3d10;
|
||||||
|
-fx-background-radius: 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll-pane .scroll-bar:horizontal .thumb {
|
||||||
|
-fx-background-color: #3d260a;
|
||||||
|
-fx-background-radius: 5;
|
||||||
|
-fx-pref-height: 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll-pane .scroll-bar:horizontal .thumb:hover {
|
||||||
|
-fx-pref-height: 10;
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user