Compare commits

...

303 Commits

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

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

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

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

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

Closes #15

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

Closes #17

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

Closes #19

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

Closes #18

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

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

Closes #13

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

Closes #12

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

Closes #8

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

Closes #11

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

Closes #9

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

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

Closes #7

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

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

Closes #2

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

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

See merge request cs108-fs26/Gruppe-13!36
2026-03-16 13:04:24 +00:00
Mathis Ginkel 98df1dc227 add: first implementation of login 2026-03-15 22:26:19 +01:00
Mathis Ginkel d6468c9595 add: first version of client to client chat 2026-03-15 22:26:11 +01:00
ginkelmath 3809b7e0e2 Fix: update clientservice 2026-03-15 22:17:18 +01:00
ginkelmath 892f0553f6 Change protocol implementation 2026-03-15 22:17:18 +01:00
ginkelmath e533262c38 Add: First version of a Client-side Networking Service 2026-03-15 22:17:18 +01:00
Lars Simon Winzer 0d8f1ac4f7 Merge branch 'feat/messages-serverside' into 'main'
Add MessageManager to broadcast Message storing content and sender to all connected users

See merge request cs108-fs26/Gruppe-13!35
2026-03-15 20:44:08 +01:00
Lars Simon Winzer 54fd43142b Add: MessagesManager to easily distribute messages to all users (Broadcast) 2026-03-15 20:33:55 +01:00
Lars Simon Winzer 5bd88445d2 Add: Modify User to store pending incoming messages 2026-03-15 20:33:24 +01:00
Lars Simon Winzer e6c9c12e3b Add: Message send by user 2026-03-15 20:24:17 +01:00
Julian Kropff be124f4a05 Merge branch 'docs/meeting-notes' into 'main'
Add: meeting notes for 14.03.2026

See merge request cs108-fs26/Gruppe-13!34
2026-03-15 19:14:15 +00:00
Julian Kropff 85d604b978 Add: meeting notes for 14.03.2026 2026-03-15 20:03:56 +01:00
Lars Simon Winzer 47544c4f1b Merge branch 'feat/user-serverside' into 'main'
Add UserRegistry to store all existing users and UserCleanupJob to periodicly remove expired user

See merge request cs108-fs26/Gruppe-13!33
2026-03-15 20:00:58 +01:00
Lars Simon Winzer 46b1388cad Add: Create UserRegistry and schedule UserCleanupJob 2026-03-15 19:51:03 +01:00
Lars Simon Winzer 68827e177e Add: Move logger creation in CleanupJob to constructor 2026-03-15 19:49:23 +01:00
Lars Simon Winzer 0a5eb5d11c Chore: JavaDoc Docstrings 2026-03-15 19:48:55 +01:00
Lars Simon Winzer 8fae4789ad Add: Make UserRegistry use multiple Maps to accelerate lookups 2026-03-15 19:37:43 +01:00
Lars Simon Winzer 8dd99726ce Add: UserCleanupJob as runnable to remove all expired users 2026-03-15 19:29:25 +01:00
Lars Simon Winzer 1223e1a4a9 Add: Extend UserRegistry to remove Disconnected User, reassign User to new session and get all users 2026-03-15 19:27:41 +01:00
Lars Simon Winzer 9d9b6ae2c7 Add: Handler for DisconnectEvent to SessionRegistry 2026-03-15 18:57:38 +01:00
Lars Simon Winzer a7cdf995c4 Add: Timestamp to User to store time of disconnect 2026-03-15 18:44:03 +01:00
Lars Simon Winzer 82582dae83 Add: UserFactory to create new users and resolve name conflicts 2026-03-15 18:41:57 +01:00
Lars Simon Winzer 7c5a4675dd Add: UserRegistry to store all active users and grand synchronized access 2026-03-15 18:35:25 +01:00
Lars Simon Winzer 2979c91ee0 Add: User to link network to domain 2026-03-15 18:25:48 +01:00
Lars Simon Winzer 8263a1355a Add: UserId to uniquely identify user 2026-03-15 18:22:11 +01:00
Lars Simon Winzer 118869f461 Merge branch 'feat/command-parser' into 'main'
Add CommandParserDispatcher to dispatch PrimitiveRequest to matching CommandHandler

See merge request cs108-fs26/Gruppe-13!32
2026-03-15 17:21:08 +01:00
Lars Simon Winzer f7f570638b Style: Correct JavaDoc comments and remove trailing whitespaces 2026-03-15 17:06:19 +01:00
Lars Simon Winzer 80c7e1c25d Add: CommandParserDispatcher to dispatch PrimitiveRequest to match CommandParser 2026-03-15 17:04:41 +01:00
Lars Simon Winzer 135e6a5327 Add: Exception to be thrown if no matching CommandParser is found 2026-03-15 16:53:29 +01:00
Lars Simon Winzer 4e853cf35f Add: Interfaces used by the CommandParserDispatcher 2026-03-15 16:53:03 +01:00
Lars Simon Winzer f6c889c250 Merge branch 'feat/protocol-parser' into 'main'
Add ProtocolParser to parse RawPacket into PrimitiveRequest using the Tokenizer

See merge request cs108-fs26/Gruppe-13!31
2026-03-15 16:19:04 +01:00
Lars Simon Winzer 8f7c92684b Add: Use ProtocolParser in Session 2026-03-15 16:12:14 +01:00
Lars Simon Winzer a5f2ac635d Add: RequestId to PrimitiveRequest 2026-03-15 16:08:48 +01:00
Lars Simon Winzer 7ca0bcc3e0 Add: Implement ProtocolParser to parse RawPacket into PrimitiveRequest 2026-03-15 16:06:04 +01:00
Lars Simon Winzer aa508dfd6c Add: Exception thrown while parsing tokens 2026-03-15 16:00:00 +01:00
Lars Simon Winzer 9c3f62db24 Add: PrimitiveRequest created by ProtocolParser containing a list of Parameter objects 2026-03-15 15:18:38 +01:00
Lars Simon Winzer 99a7154701 Merge branch 'main' into 'feat/protocol-parser' 2026-03-15 15:01:21 +01:00
Lars Simon Winzer 2d2bdddfa8 Merge branch 'feat/server-transport-rawpacket' into 'main'
Encapsulate read data by transport layer into RawPacket

See merge request cs108-fs26/Gruppe-13!30
2026-03-15 15:00:50 +01:00
Lars Simon Winzer 0bfcbe95b0 Style: Line length of JavaDoc comment 2026-03-15 14:55:43 +01:00
Lars Simon Winzer 94a2e1f977 Add: Make interface and implementation use RawPacket 2026-03-15 14:55:29 +01:00
Lars Simon Winzer 1bfbb5c75d Add: RawPacket record to store raw data about the incomming request 2026-03-15 14:49:39 +01:00
Lars Simon Winzer 61f0a23bfa Merge branch 'feat/tokenizer' into 'main'
Add server-side tokenizer

See merge request cs108-fs26/Gruppe-13!29
2026-03-15 13:52:51 +01:00
Lars Simon Winzer 08dc94b4b0 Style: Comply with Spotless
Created issue #2 regarding this issue
2026-03-15 13:38:59 +01:00
Lars Simon Winzer ca11c135f1 Style: Bracket after 'if' and record in one-line 2026-03-15 13:28:06 +01:00
Lars Simon Winzer 60f59e03d4 Add: TokenClassifier to perform more extensive syntax verification and classify tokens 2026-03-15 13:26:59 +01:00
Lars Simon Winzer 1e794337b5 Remove: 'NEWLINE' member from TokenType enum as commands dont span over multiple lines 2026-03-15 13:08:23 +01:00
Lars Simon Winzer 3509a967a6 Add: Tokenizer does no longer classify tokens and instead returns 'RawToken's 2026-03-15 12:21:56 +01:00
Lars Simon Winzer 4c0e6aeb36 Delete: Falsly added file for rudimentary quick testing 2026-03-15 12:20:37 +01:00
Lars Simon Winzer 3e138ab84e Add: State to use RawToken 2026-03-15 12:11:59 +01:00
Lars Simon Winzer 45fd696fec Add: RawToken and RawTokenType for token representation 2026-03-15 12:08:32 +01:00
Lars Simon Winzer b898357d76 Merge branch 'main' into 'feat/tokenizer' 2026-03-14 18:24:03 +01:00
Jona Walpert 830c04a706 Merge branch 'chore/conform-ui-to-checkstyle' into 'main'
Chore/conform ui to checkstyle

See merge request cs108-fs26/Gruppe-13!28
2026-03-14 17:21:28 +00:00
Lars Simon Winzer d242aeef7c Merge branch 'main' into 'chore/conform-ui-to-checkstyle' 2026-03-14 17:58:13 +01:00
Lars Simon Winzer 71d6ac9ecc Merge branch 'issue/1-increase-allowed-method-length' into 'main'
Increase allowed MethodLength checked by Checkstyle to 60

Closes #1

See merge request cs108-fs26/Gruppe-13!27
2026-03-14 17:56:15 +01:00
Lars Simon Winzer 8415e3f155 Add: Increase allowed MethodLength to 60 2026-03-14 17:53:19 +01:00
Jona Walpert a15a0a0044 Style: apply Checkstyle fixes on client side 2026-03-14 17:35:47 +01:00
Jona Walpert e3e1da0082 Merge branch 'main' into 'chore/conform-ui-to-checkstyle' 2026-03-14 17:16:57 +01:00
Jona Walpert d1bf8e4c9a Style: Use logger instead of System.out.println 2026-03-14 17:14:41 +01:00
Lars Simon Winzer f3c34dfa7f Move: Tokenizer related tests into server server directory 2026-03-14 17:14:37 +01:00
Lars Simon Winzer 2af3fe7c35 Move: Tokenizer related files into server directory 2026-03-14 17:13:18 +01:00
Julian Kropff d01bbdec7b Merge branch 'feat/game-ui' into 'main'
Feat/game-ui

See merge request cs108-fs26/Gruppe-13!26
2026-03-14 16:05:03 +00:00
Julian Kropff 58c473fc9e Merge remote-tracking branch 'origin/main' into feat/game-ui 2026-03-14 17:00:28 +01:00
Julian Kropff c411e9c560 style: apply Checkstyle fixes 2026-03-14 16:51:18 +01:00
Julian Kropff 1c5c4c8a34 style: apply Checkstyle fixes 2026-03-14 16:48:20 +01:00
Julian Kropff 9376b1d406 style: apply Checkstyle fixes 2026-03-14 16:48:06 +01:00
Julian Kropff cb98c70562 style: apply Checkstyle fixes 2026-03-14 16:46:44 +01:00
Julian Kropff d8bfc1c8e9 style: apply Checkstyle fixes 2026-03-14 16:46:28 +01:00
Lars Simon Winzer 14c18df0eb Test: Tokenizer exception 2026-03-14 16:37:11 +01:00
Lars Simon Winzer 208a284204 Test: Tokenizer state 2026-03-14 16:33:34 +01:00
Lars Simon Winzer ef2a00268f Add: Tokenizer to tokenize provided string into tokens and perform syntax check 2026-03-14 16:13:35 +01:00
Lars Simon Winzer d403908341 Add: TokenizerException thrown during tokenization 2026-03-14 16:12:50 +01:00
Lars Simon Winzer fa54fd75c1 Add: Tokenizer state and token representation 2026-03-14 15:32:40 +01:00
Jona Walpert 50d75e2f03 Style: Checkstyle changes to LobbyUI 2026-03-14 14:48:37 +01:00
Lars Simon Winzer 3dbd3a6c15 Merge branch 'chore/conform-server-to-checkstyle' into 'main'
Edit server-side code to conform to checkstyle rules

See merge request cs108-fs26/Gruppe-13!25
2026-03-14 14:26:49 +01:00
Lars Simon Winzer 1aafc0bc4b Style: Use logger instead of System.out.println 2026-03-14 14:20:01 +01:00
Julian Kropff 2f7040485a Merge branch 'feat/game-ui' into 'main'
Feat/game-ui

See merge request cs108-fs26/Gruppe-13!24
2026-03-14 13:10:23 +00:00
jk 736cf230fb Merge branch 'main' into 'feat/game-ui' 2026-03-14 14:02:13 +01:00
Lars Simon Winzer 3ce0a44a18 Style: Trailing whitespace 2026-03-14 13:46:11 +01:00
Lars Simon Winzer 59494ea2d4 Style: Line length 2026-03-14 13:45:29 +01:00
Lars Simon Winzer 34c89b671e Style: Unnecessary spaces in JavaDoc parameter and return value labels 2026-03-14 13:44:58 +01:00
Jona Walpert bb0f805b4e Merge branch 'chore/remove-demo-code' into 'main'
chore: remove demo code from project

See merge request cs108-fs26/Gruppe-13!23
2026-03-14 12:39:19 +00:00
Lars Simon Winzer 573c5fd36d Style: Singleline JavaDoc comments 2026-03-14 13:37:04 +01:00
Jona Walpert 09ebacc1e4 chore: remove demo code from project 2026-03-14 13:36:27 +01:00
Lars Simon Winzer 7be2229280 Style: Sort imports 2026-03-14 13:33:46 +01:00
Julian Kropff 3a8dc98e91 style: update chat ui design 2026-03-14 13:31:45 +01:00
Lars Simon Winzer b731f48ba7 Merge branch 'chore/fix-spotless-indentation' into 'main'
Edit spotless gradle task to indent 4 spaces instead of 2

See merge request cs108-fs26/Gruppe-13!22
2026-03-14 13:28:34 +01:00
Lars Simon Winzer 9a88cfc945 Add: trim trailing whitespace and end with newline for spotless task 2026-03-14 13:24:48 +01:00
Lars Simon Winzer e22446bffc Fix: Use 'Android Open Source Project' indentation (4 spaces) instead of 2 2026-03-14 13:24:05 +01:00
Julian Kropff 59a4648ad6 fix: reorganize folder structure for chat components 2026-03-14 13:01:41 +01:00
Julian Kropff ec4d8bfb07 fix: update chatbox fxml 2026-03-14 11:54:14 +01:00
jk 1fe6d9ce5d add: casino browser controller 2026-03-14 11:43:01 +01:00
jk 8f5d90c538 style: update casino game ui 2026-03-14 11:41:05 +01:00
jk 8610f77386 fix: update casino game ui fxml 2026-03-14 11:38:54 +01:00
jk c38f177a16 fix: update chatbox fxml 2026-03-14 11:37:26 +01:00
jk 815caff372 add: chat controller 2026-03-14 11:34:50 +01:00
jk c441318df9 fix: update launcher logic 2026-03-14 11:33:50 +01:00
jk 7d2486d3be fix: update taskbar fxml 2026-03-14 11:31:47 +01:00
jk 18d8b68fc8 fix: update and rename taskbar controller logic 2026-03-14 11:28:02 +01:00
jk 4ecbe21602 fix: update and rename casino game ui logic 2026-03-14 11:26:30 +01:00
jk e718083010 fix: update and rename casino controller logic 2026-03-14 11:24:14 +01:00
jk b810b2187b build: add javafx.graphics and javafx.web dependencies 2026-03-14 11:20:59 +01:00
Lars Simon Winzer 5b6b58f1cc Merge branch 'chore/ci-execute-junit-tests' into 'main'
Add CI job to run unit tests on push and MR

See merge request cs108-fs26/Gruppe-13!21
2026-03-14 10:12:48 +01:00
Lars Simon Winzer 6e35c599da Ci: Add job to run test on push and mr 2026-03-14 10:06:12 +01:00
Lars Simon Winzer 66f840b269 Merge branch 'chore/ci-run-automatically' into 'main'
Let CI jobs run automatically

See merge request cs108-fs26/Gruppe-13!20
2026-03-14 09:40:36 +01:00
Lars Simon Winzer 39b6c5e13e Ci: Run different jobs on push or mr 2026-03-14 09:33:55 +01:00
Lars Simon Winzer 000a5c0d9a Merge branch 'chore/gitlab-ci-code-report' into 'main'
Add CI Job to convert checkstyle report to code quality report displayable in MR

See merge request cs108-fs26/Gruppe-13!19
2026-03-14 09:20:33 +01:00
Lars Simon Winzer 0157102ae5 Ci: Add job to execute python script and upload code quality report 2026-03-14 09:09:29 +01:00
Lars Simon Winzer a7bfb59e2f Add: Python script to convert findings of checkstyle to GitLab code report 2026-03-14 09:01:25 +01:00
Lars Simon Winzer 37faa39f10 Merge branch 'chore/gitlab-linting-formatting-pipeline' into 'main'
Add first (manual) CI jobs

See merge request cs108-fs26/Gruppe-13!18
2026-03-13 23:24:45 +01:00
Lars Simon Winzer 147f84665e Ci: Make compile-check job run independent from other jobs 2026-03-13 23:17:10 +01:00
Lars Simon Winzer 8aa71a4430 Ci: Cache gradle for faster job execution 2026-03-13 23:11:28 +01:00
Lars Simon Winzer cfeea5222d Ci: Add job to check if project compiles 2026-03-13 23:05:23 +01:00
Lars Simon Winzer fb02faffc9 Build: Add spotless plugin and add task 2026-03-13 22:57:34 +01:00
Lars Simon Winzer 815b60232c Add: ImportOrder check to checkstyle config 2026-03-13 22:56:36 +01:00
Lars Simon Winzer 28f8d17237 Ci: Attach checkstyle report as job artifact 2026-03-13 22:36:23 +01:00
Lars Simon Winzer d311553de7 Add: Suppressions config for tests 2026-03-13 22:34:41 +01:00
Lars Simon Winzer e797470ca3 Ci: Bump image for job to 'gradle:9.3.1-jdk25' 2026-03-13 17:35:42 +01:00
Lars Simon Winzer 18f17564d5 Ci: Configure GitLab ci pipeline to execute Checkstyle on manual trigger 2026-03-13 17:29:41 +01:00
Lars Simon Winzer 57010d2872 Add: Configuration file for checkstyle 2026-03-13 17:28:24 +01:00
Lars Simon Winzer 004b2e2313 Build: Add checkstyle plugin and add gradle task 2026-03-13 16:38:56 +01:00
Lars Simon Winzer 9b40396eed Merge branch 'feat/network-manager' into 'main'
Add (v1) NetworkManager, Session and Transport

See merge request cs108-fs26/Gruppe-13!17
2026-03-13 15:49:55 +01:00
Lars Simon Winzer 4bcb9a9874 Style: Newline at end of file 2026-03-13 15:46:12 +01:00
Jona Walpert 9c3114b5f3 Merge branch 'feat/lobby-ui' into 'main'
Merge 'feat/lobby-ui' back into 'main'

See merge request cs108-fs26/Gruppe-13!16
2026-03-13 14:30:32 +00:00
Jona Walpert 1cefe9c9d9 Merge branch 'chore/fix-enable-native-access-javafx' into 'main'
chore: fix JVM native access warnings for JavaFX

See merge request cs108-fs26/Gruppe-13!15
2026-03-13 14:21:53 +00:00
Jona Walpert 0e30e08247 chore: fix JVM native access warnings for JavaFX 2026-03-13 15:16:42 +01:00
Lars Simon Winzer 38133e6761 Docs: Add JavDoc for all classes and methods 2026-03-13 15:16:15 +01:00
Lars Simon Winzer 4231ca3e97 Ammend to 49019075e4 2026-03-13 15:12:06 +01:00
Jona Walpert 75567c59b2 Merge branch 'main' into 'feat/lobby-ui' 2026-03-13 14:55:43 +01:00
Jona Walpert a39775bd04 refactor: Button_ID - Lobby_ID translation is now only stored in hasmap and not in a Json file anymore 2026-03-13 14:54:47 +01:00
Lars Simon Winzer 49019075e4 Move: Network related files into respective folders 2026-03-13 14:42:19 +01:00
Jona Walpert 97a860df9e Merge branch 'chore/gradle-client-run-shortcut' into 'main'
Chore: Add client run task to gradle

See merge request cs108-fs26/Gruppe-13!14
2026-03-13 10:31:42 +00:00
Jona Walpert 0b51f4f45a Chore: Add client run task to gradle 2026-03-13 11:29:21 +01:00
Lars Simon Winzer 711de2e6aa Add: Make use of SessionManager 2026-03-12 20:13:50 +01:00
Lars Simon Winzer 74566f99cf Fix: Direct (unallowed) cast from Consumer<T> to Consumer<Object> 2026-03-12 20:12:52 +01:00
Lars Simon Winzer c301039bfc Add: SessionManager to store all active sessions 2026-03-12 19:34:46 +01:00
Lars Simon Winzer e1029932d8 Fix: 'Potential null pointer access' 2026-03-12 19:27:29 +01:00
Lars Simon Winzer db7892d9c4 Fix: 'Type safety: Unchecked cast from ...' 2026-03-12 19:26:00 +01:00
Lars Simon Winzer 7b32fe1147 Add: Event interface and implement it in DisconnectEvent, update EventBus to only allow Event type 2026-03-12 19:16:59 +01:00
Lars Simon Winzer 54588276d1 Add: Let Session publish DisconnectEvent and EventHandler subscribe and handle it 2026-03-12 19:15:25 +01:00
Lars Simon Winzer fc8a92789a Add: DisconnectEvent as record 2026-03-12 19:13:25 +01:00
Lars Simon Winzer 7fcb47ed6f Add: EventBus class for event handling and subscription management 2026-03-12 19:12:53 +01:00
Lars Simon Winzer d4bf69dc59 Refactor: Session to use TransportLayer for socket communication 2026-03-12 15:55:46 +01:00
Lars Simon Winzer b073760eef Add: TcpTransport class to implement TransportLayer 2026-03-12 15:51:35 +01:00
Lars Simon Winzer 48d77b6cbe Add: TransportLayer interface to abstract direct socket access 2026-03-12 15:47:03 +01:00
Jona Walpert 59765e33bb Test: Unit test for Translating Button IDs into backend Lobby IDs 2026-03-12 15:36:59 +01:00
Jona Walpert fa95ce1418 Test: Unit test for creating lobbies and trying to join them 2026-03-12 15:36:13 +01:00
Jona Walpert d2407d61f4 Add: Lobby display in lobby GUI with placeholder functions. 2026-03-12 15:25:18 +01:00
Jona Walpert d414c27ff2 Chore: Added Json dependency in Gradle 2026-03-12 15:23:40 +01:00
Lars Simon Winzer f4a32b1649 Add: Session Runnable to read incomming data, decode and print it 2026-03-12 13:45:30 +01:00
Lars Simon Winzer 5407e245f7 Add: Modify NetworkManager to create Session uppon recieving a new connection 2026-03-12 13:43:35 +01:00
Jona Walpert 02dc75c331 Add: Javadoc to client side files for lauching (lobby) UI, Main file 2026-03-12 11:31:41 +01:00
Julian Kropff 4989e26ac6 style(game-ui): add Casinogameui stylesheet 2026-03-12 07:31:15 +01:00
Julian Kropff 89d4cad336 feat(game-ui): add Tastbar FXML layout 2026-03-12 07:30:35 +01:00
Julian Kropff c0b6a5424a feat(game-ui): add PlayerStatus FXML layout 2026-03-12 07:28:27 +01:00
Julian Kropff f9b32bc979 feat(game-ui): add Chatbox FXML layout 2026-03-12 07:26:39 +01:00
Julian Kropff 25d145cffd feat(game-ui): add Casinogameui FXML layout 2026-03-12 07:25:09 +01:00
Julian Kropff d47084d6f5 feat(game-ui): add TaskbarController 2026-03-12 07:24:28 +01:00
Julian Kropff c87e6f1463 feat(game-ui): add Casinogamecontroller 2026-03-12 07:22:50 +01:00
Julian Kropff 23faf0f371 feat(game-ui): add Casinogameui base class 2026-03-12 07:21:16 +01:00
Julian Kropff 268cf283bd chore(launcher): start Casinogameui for development 2026-03-12 07:18:18 +01:00
Lars Simon Winzer 5ff4dcec53 Move: Session and SessionId from common to server 2026-03-11 18:53:30 +01:00
Lars Simon Winzer dac05de331 Add: Primitive first version of NetworkManager 2026-03-11 18:23:26 +01:00
Lars Simon Winzer b1ab8f3982 Merge branch 'main' into 'feat/network-manager' 2026-03-11 17:57:29 +01:00
Lars Simon Winzer 82dd5a9b8a Merge branch 'feat/log4j-logging' into 'main'
Add Log4J as Gradle dependency and configures it with the bare minimum

See merge request cs108-fs26/Gruppe-13!13
2026-03-11 17:50:08 +01:00
Lars Simon Winzer 18331a7cc3 Add: Colored log output, based on log level 2026-03-11 17:47:36 +01:00
Lars Simon Winzer a2256b2c1e Add: Jansi as gradle dependency to allow colored console output 2026-03-11 17:44:19 +01:00
Lars Simon Winzer 85c872df23 Add: Konfigure log4j with the bare minimum 2026-03-11 17:41:14 +01:00
Lars Simon Winzer 2b012aa256 Add: log4j as gradle dependency 2026-03-11 17:40:46 +01:00
jk 4879b1b464 Merge remote-tracking branch 'origin/feat/ui-structure' into feat/game-ui 2026-03-11 17:16:24 +01:00
Jona Walpert a5204eaf29 Add: Lobby UI 2026-03-11 15:16:31 +01:00
Jona Walpert 14384dea5b Merge branch 'chore--add-bin-to-gitignore' into 'main'
Add: Added "bin" to gitignore

See merge request cs108-fs26/Gruppe-13!12
2026-03-11 13:21:26 +00:00
Jona Walpert e5e93c1a65 Add: Added "bin" to gitignroe 2026-03-11 14:20:18 +01:00
Jona Walpert f1b6243d6d Merge branch 'chore/build-gradle-javafx-module-changes' into 'main'
Add javafx modules to gradle

See merge request cs108-fs26/Gruppe-13!11
2026-03-11 12:44:24 +00:00
Jona Walpert 7daae485da Chore: Added javafx modules to build.gradle file 2026-03-11 13:22:33 +01:00
Jona Walpert a7e43619f8 Revert "Add: javafx modules in build.gradle"
This reverts commit 34cb17d0e4.
2026-03-11 12:35:14 +01:00
Jona Walpert 34cb17d0e4 Add: javafx modules in build.gradle 2026-03-11 12:29:31 +01:00
Lars Simon Winzer a6afbeeae1 Merge branch 'docs/networking-documentation' into 'main'
Outline of networking components as document, first revision

See merge request cs108-fs26/Gruppe-13!10
2026-03-11 11:59:22 +01:00
Lars Simon Winzer a5272b99ab Add: Include diagramm in document 2026-03-11 11:50:59 +01:00
Lars Simon Winzer fb6f14371f Add: PlantUML diagramm outlining each component 2026-03-11 11:49:46 +01:00
Lars Simon Winzer 32e4b04c87 Add: SessionId to identify each session 2026-03-11 11:43:27 +01:00
Lars Simon Winzer 22990fa35e Add: Document outlining the core components and roles in the server-side networking 2026-03-11 11:37:04 +01:00
Jona Walpert ac9e7eecff Add: Main UI, UI Launcher 2026-03-11 11:36:57 +01:00
Jona Walpert c1a4b436fe Add: Game Logo, Background Image 2026-03-11 11:35:38 +01:00
Lars Simon Winzer 9c291e4e3b Revert "Add: Readme explaining the purpose, format and links to existing blog posts"
This reverts commit 91a513e214.
2026-03-09 15:37:26 +01:00
Lars Simon Winzer 91a513e214 Add: Readme explaining the purpose, format and links to existing blog posts 2026-03-09 15:30:50 +01:00
Lars Simon Winzer 66925bc665 Add: Session interface all sessions need to conform to 2026-03-05 13:46:10 +01:00
Lars Simon Winzer ff38c9d6ef Add: SessionId class to uniquely identify session in SessionRegistry 2026-03-05 13:38:36 +01:00
Lars Simon Winzer e6b5cac53c Add: SessionId class to uniquely identify session in SessionRegistry 2026-03-05 13:28:04 +01:00
108 changed files with 5576 additions and 135 deletions
+5 -1
View File
@@ -118,4 +118,8 @@ $RECYCLE.BIN/
*.drawio.pdf
## IntelliJ
.idea
.idea
## bin
bin/
gradle.properties
+113
View File
@@ -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
+40
View File
@@ -0,0 +1,40 @@
## Bug Report
<!-- The reccommended type is: Issue -->
### Environment
- **Branch & Commit:**
- **Operating system:**
- **How was execution started (Gradle task / IDE debug / IDE run):**
- **Java version:** `25`
- **Gradle version**: `9.3.1`
### Summary
<!-- Short, precise description of the bug -->
### Expected Behavior
<!-- What you expected to happen -->
### Actual Behavior
<!-- What actually happened instead -->
### Steps to Reproduce
<!--
1. Do this first
2. Then do that next
3. Lastly, see the unexprected ...
-->
### Stack Trace / Error Message
```
<!-- Paste stack trace or log output here -->
```
### Possible Cause / Notes
<!-- Optional: your own hypothesis about the root cause -->
### Checklist
- [ ] I reproduced the problem using the steps above
- [ ] I searched documentation docs for relevant information
- [ ] I added relevant labels
/label ~bug
@@ -0,0 +1,18 @@
## Feature Request
<!-- The reccommended type is: Task -->
### Summary
<!-- Brief description of the desired functionality -->
### Required workarround
<!-- Workarround required to achieve x (if applicable) -->
### Description
<!-- Detailed description of the desired behavior -->
### Checklist
- [ ] I reproduced the problem using the steps above
- [ ] I searched documentation docs for relevant information
- [ ] I added relevant labels
/label ~enhancement
+24
View File
@@ -0,0 +1,24 @@
## Task
<!-- The reccommended type is: Task -->
### Summary
<!-- What needs to be implemented? -->
### Context
<!-- Which part of the system does it belong to? -->
<!-- Examples: Networking, Game-Engine, User interface, ... -->
### Current Progress
- [ ]
- [ ]
- [ ]
### Notes
<!-- Optional: Implementation hints, links to prior design discussions, etc. -->
### Checklist
- [ ] I outlined checkpoints describing phases of my work related to this task
- [ ] I added relevant labels
- [ ] I linked to other issues or branches that must be completed first
/label ~task
+3
View File
@@ -0,0 +1,3 @@
{
"java.configuration.updateBuildConfiguration": "automatic"
}
+208
View File
@@ -0,0 +1,208 @@
# Contribution Guidelines
This document describes the conventions and workflows everyone must follow to keep the codebase consistent and the collaboration smooth.
If you notice a violation, speak to the person involved respectfully.
Since this project is part of a course at the University of Basel, the [Code of Conduct](https://www.unibas.ch/de/Universitaet/Administration-Services/Vizerektorat-People-And-Culture/Persoenliche-Integritaet/Code-of-Conduct.html) applies.
## Table of Contents
- [Contribution Guidelines](#contribution-guidelines)
- [Table of Contents](#table-of-contents)
- [Issues \& Tasks](#issues--tasks)
- [Creating an issue](#creating-an-issue)
- [During implementation](#during-implementation)
- [Collaborative work](#collaborative-work)
- [Git Workflow](#git-workflow)
- [Creating a branch](#creating-a-branch)
- [Working on a branch](#working-on-a-branch)
- [Commit Messages](#commit-messages)
- [Rules](#rules)
- [Examples](#examples)
- [Code Style](#code-style)
- [Linter](#linter)
- [Formatter](#formatter)
- [General guidelines](#general-guidelines)
- [CI/CD Pipeline](#cicd-pipeline)
- [Before pushing](#before-pushing)
- [Merge Requests](#merge-requests)
- [Opening a MR](#opening-a-mr)
- [Merging](#merging)
- [After merging](#after-merging)
- [Be human](#be-human)
## Issues & Tasks
Every piece of work - whether a new feature, a bug fix, or a refactoring - must be tracked as an
Issue or Task in GitLab **before** any implementation begins.
### Creating an issue
1. Open a new Issue or Task using the **relevant template** provided in the repository.
2. Fill in **all fields** specified by the template thoughtfully and completely. A well-written issue is the single source of truth for the work being done - treat it accordingly.
3. Work through the **checklist** in the template before marking the issue as ready. Do not skip items.
### During implementation
- If you encounter a problem or an unexpected finding while working on an issue, record it as a **comment** on the issue. This keeps the history intact and visible to the whole team.
- **Do not restructurally edit the original description** to incorporate new information. The description reflects the intent at the time the issue was created; comments document what happened along the way.
### Collaborative work
- When multiple people are working on the same issue, **prefer issue comments over private messages** for coordination. This keeps the current status, decisions, and open questions
centrally visible and searchable.
- Before starting work that overlaps with an existing issue, check its comment thread first to avoid duplicating effort.
## Git Workflow
We use a **feature branch -> main** strategy. The `main` branch is always in a releasable state.
### Creating a branch
We follow the [**Conventional Branch**](https://conventional-branch.github.io/) specification. Branch names follow this pattern:
```
<type>/<short-description>
```
| Type | When to use |
|------------|--------------------------------------------------|
| `feat` | New feature or capability |
| `fix` | Bug fix |
| `refactor` | Restructuring without behaviour change |
| `test` | Adding or fixing tests |
| `ci` | Pipeline, Gradle, or tooling changes |
| `docs` | Documentation only |
**Examples:**
```
feat/reconnect-command
fix/session-writer-flush
refactor/user-registry-cleanup
docs/contributing
```
### Working on a branch
Keep branches short-lived. A branch should represent one cohesive unit of work.
It is permissible to commit changes within a feature branch that cause the program to become non-functional, but these should be fixed as soon as possible. In any case, the code that is merged into `main` must be functional.
And most importantly: **Do not commit directly to `main`**.
## Commit Messages
We follow the [**Conventional Commits**](https://www.conventionalcommits.org/) specification. Every commit message must have the form:
```
<type>: <short summary>
[optional body]
```
### Rules
The summary line must be **<= 72 characters**, written in the **imperative mood** (e.g. "add", not "added" or "adds").
### Examples
```
Reat: Add RECONNECT command handler
The handler re-associates an existing User with a new Session after
a connection drop, preserving in-flight state.
```
```
Fix: Flush output stream before closing
```
```
Ci: Tighten Checkstyle failure policy to allow_failure: false
```
```
Refactor: Replace ArrayList with CopyOnWriteArrayList
```
## Code Style
Code formatting is enforced automatically. **Do not submit a MR with formatting violations.**
### Linter
We use **Checkstyle** as our linter with a custom set of rules tailored to our project.
Checkstyle runs on every pipeline. Fix all violations locally before pushing:
```bash
./gradlew checkstyleMain checkstyleTest
```
### Formatter
We use **Spotless** with the **[Google](https://google.github.io/styleguide/javaguide.html) / [AOSP Java style](https://source.android.com/docs/core/architecture/hidl/code-style?hl=en)**:
- **Indentation:** 4 spaces (no tabs)
- **Line length:** 100 characters
- No decorative blank lines directly after opening braces `{`
- Blank lines are reserved for separating logical sections within a block
Run the formatter before committing:
```bash
./gradlew spotlessApply
```
Check without applying:
```bash
./gradlew spotlessCheck
```
### General guidelines
- Add **JavaDoc** docstrings to classes, interfaces, records and methods.
- Exercise **clean architecture**
- Prefer **stateless components**
- Use **`record` types** for immutable data carriers.
- Log with **Log4J 2** (`log4j-api`). Use the appropriate level (`DEBUG` for pipeline internals, `INFO` for lifecycle events, `WARN`/`ERROR` for recoverable/unrecoverable problems).
## CI/CD Pipeline
The pipeline runs automatically on every push. It has four stages:
```
lint > report > build > test
```
| Stage | Jobs |
|----------|--------------------------------------------|
| `lint` | Spotless check, Checkstyle |
| `report` | Code Quality JSON conversion (only for mr) |
| `build` | `./gradlew assemble` |
| `test` | `./gradlew test` + JUnit result reporting |
### Before pushing
Run the full check suite locally to avoid a broken pipeline:
```bash
./gradlew spotlessCheck checkstyleMain checkstyleTest build test
```
A red pipeline blocks merging. Fix failures before creating your merge request.
## Merge Requests
### Opening a MR
- Target branch is always **`main`**.
- Fill in the MR description: what changed and why. Link the relevant issue (with 'Closing #x') if one exists.
### Merging
A MR can be merged when **all main CI pipeline stages are green** (lint, build, test).
No explicit peer approval is required, but leaving a note or question in the MR thread for non-trivial changes is encouraged.
If you spot a problem in someone else's open MR, comment - **do not push directly to their branch** and try to fix the issue yourself.
### After merging
Delete the feature branch after the MR is merged. GitLab can do this automatically via the
"Delete source branch" checkbox in the MR.
## Be human
We are all human.
We all forget things or make mistakes sometimes.
Its important that we look out for one another and **work together as a team**.
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
View File
@@ -2,6 +2,8 @@ plugins {
id 'application'
id 'java'
id 'org.openjfx.javafxplugin' version '0.1.0'
id 'checkstyle'
id 'com.diffplug.spotless' version '7.0.2'
}
group = 'ch.unibas.dmi.dbis'
@@ -23,18 +25,44 @@ repositories {
javafx {
version = "25.0.2"
modules = ['javafx.controls', 'javafx.fxml', 'javafx.base']
modules = ['javafx.controls', 'javafx.fxml', 'javafx.base', 'javafx.graphics', 'javafx.web']
}
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")
testRuntimeOnly("org.junit.platform:junit-platform-launcher:1.10.0")
implementation 'org.json:json:20240303'
}
test {
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) {
manifest {
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) {
group = 'build'
description = 'Assembles a runnable fat JAR including runtime dependencies.'
+214
View File
@@ -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>
+24
View File
@@ -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>
+13
View File
@@ -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
![PlantUML diagramm of all components outlined in this document](/documents/images/docs/networking/server-architecure/networking_components.svg)
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
+29
View File
@@ -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.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 {
/**
* Main entry point for Casono.
*
* @param args Command line arguments
*/
public static void main(String[] args) {
if (!isValid(args)) {
printUsage();
@@ -33,10 +40,12 @@ public final class Main {
}
private static void printUsage() {
System.err.println("""
Usage:
java -jar xyz.jar server <listenPort>
java -jar xyz.jar client <serverIp>:<serverPort>
""");
Logger logger = LogManager.getLogger(Main.class);
logger.fatal(
"""
Usage:
java -jar xyz.jar server <listenPort>
java -jar xyz.jar client <serverIp>:<serverPort>
""");
}
}
@@ -1,6 +1,32 @@
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 {
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) {
String[] parts = arg.split(":", 2);
if (parts.length != 2) {
@@ -9,6 +35,7 @@ public class ClientApp {
String host = parts[0];
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);
}
}
@@ -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();
}
}
@@ -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;
}
}
@@ -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();
}
}
@@ -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());
}
}
}
@@ -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;
}
}
@@ -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;
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 {
private static final int USER_CLEANUP_JOB_DELAY = 0;
private static final int USER_CLEANUP_JOB_PERIOD = 10;
private static final int USER_CLEANUP_JOB_RECONNECT_THRESHOLD = 10;
private static final int SESSION_DISCONNECT_JOB_DELAY = 0;
private static final int SESSION_DISCONNECT_JOB_PERIOD = 2;
private static final int SESSION_DISCONNECT_JOB_TIMEOUT = 5;
public static void start(String arg) {
int port = Integer.parseInt(arg);
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) {}
@@ -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);
}
}
}
@@ -0,0 +1,7 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.command;
import ch.unibas.dmi.dbis.cs108.casono.server.network.parser.Request;
public interface CommandHandler<T extends Request> {
void execute(T request);
}
@@ -0,0 +1,28 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.command;
import ch.unibas.dmi.dbis.cs108.casono.server.network.parser.Request;
import java.util.HashMap;
import java.util.Map;
public class CommandRouter {
private final Map<Class<? extends Request>, CommandHandler<?>> handlers = new HashMap<>();
public <T extends Request> void register(Class<T> request, CommandHandler<T> handler) {
handlers.put(request, handler);
}
// Safe, because during registration, it's ensured that the provided CommandHandler only
// receives requests it can handle.
@SuppressWarnings("unchecked")
public void execute(Request request) {
CommandHandler<Request> handler =
(CommandHandler<Request>) handlers.get(request.getClass());
if (handler == null) {
String requestName = request.getClass().toString();
throw new UnknownRequestException(
"Unable to execute request " + requestName + ". Type unknown", requestName);
}
handler.execute(request);
}
}
@@ -0,0 +1,14 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.command;
public class UnknownRequestException extends RuntimeException {
private final String requestName;
public UnknownRequestException(String message, String requestName) {
super(message);
this.requestName = requestName;
}
public String getRequestName() {
return requestName;
}
}
@@ -0,0 +1,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));
}
}
}
@@ -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);
}
@@ -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);
}
}
@@ -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) {}
@@ -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;
}
}
@@ -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) {}
@@ -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();
}
}
@@ -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();
}
}
@@ -0,0 +1,14 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.parser;
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionId;
/**
* Immutable context for a network request.
*
* <p>Contains the originating session's identifier and the request's id. Later used to create
* response.
*
* @param sessionId the identifier of the session that initiated the request
* @param requestId the request's numeric id within the session
*/
public record RequestContext(SessionId sessionId, int requestId) {}
@@ -0,0 +1,113 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.parser;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* Provides typed access to a request's parameters by indexing them by key.
*
* <p>Supports required and optional lookups, with optional conversion from {@link String} values to
* domain-specific types via parser functions.
*/
public class RequestParameterAccessor {
private final Map<String, String> index;
/**
* Creates an accessor
*
* @param parameters to use
*/
public RequestParameterAccessor(List<Parameter> parameters) {
this.index =
parameters.stream()
.collect(Collectors.toUnmodifiableMap(Parameter::key, Parameter::value));
}
/**
* Returns the raw value for a required parameter key.
*
* @param key parameter key to look up
* @return raw parameter value
* @throws MissingParameterException if no parameter with the given key exists
*/
public String require(String key) throws MissingParameterException {
String value = index.get(key);
if (value == null) {
throw new MissingParameterException(
"Required parameter with key '" + key + "' is missing.", key);
}
return value;
}
/**
* Returns a parsed value for a required parameter key.
*
* @param key parameter key to look up
* @param parser parser used to convert the raw value
* @param <T> target type returned by the parser
* @return parsed parameter value
* @throws MissingParameterException if no parameter with the given key exists
* @throws ParameterParseException if parsing the raw value fails
*/
public <T> T require(String key, ThrowingParser<T> parser)
throws MissingParameterException, ParameterParseException {
String value = require(key);
try {
return parser.parse(value);
} catch (Exception e) {
throw new ParameterParseException(
"Error while parsing '" + key + "' with specified parser", key, e);
}
}
/**
* Returns the raw value for a parameter key or the provided default value if missing.
*
* @param key parameter key to look up
* @param defaultValue value returned when the key does not exist
* @return found parameter value or {@code defaultValue} if absent
*/
public String optional(String key, String defaultValue) {
String value = index.get(key);
if (value == null) {
return defaultValue;
}
return value;
}
/**
* Returns a parsed value for a parameter key or the provided default value if missing.
*
* @param key parameter key to look up
* @param defaultValue value returned when the key does not exist
* @param parser parser used to convert the raw value
* @param <T> target type returned by the parser
* @return parsed parameter value or {@code defaultValue} if absent
* @throws ParameterParseException if parsing the raw value fails
*/
public <T> T optional(String key, T defaultValue, ThrowingParser<T> parser)
throws ParameterParseException {
String value = index.get(key);
if (value == null) {
return defaultValue;
}
try {
return parser.parse(value);
} catch (Exception e) {
throw new ParameterParseException(
"Error while parsing '" + key + "' with specified parser", key, e);
}
}
/**
* Checks whether a parameter with the given key exists.
*
* @param key parameter key to check
* @return {@code true} if the key exists, otherwise {@code false}
*/
public boolean containsKey(String key) {
return index.containsKey(key);
}
}
@@ -0,0 +1,18 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.parser;
/**
* Functional parser interface used to convert a raw string parameter into a target type.
*
* @param <T> target type produced by the parser
*/
@FunctionalInterface
interface ThrowingParser<T> {
/**
* Parses the provided raw parameter value.
*
* @param value raw parameter value
* @return parsed value
* @throws Exception if the value cannot be parsed
*/
T parse(String value) throws Exception;
}
@@ -0,0 +1,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);
}
}
@@ -0,0 +1,34 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.response;
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionId;
/** Response representing an error outcome for a client's request. */
public class ErrorResponse extends Response {
/**
* Construct an error response with a code and message.
*
* @param sessionId the target session id
* @param requestId the originating request id
* @param errorCode a short error code identifying the failure
* @param errorMessage a human readable error message
*/
public ErrorResponse(
SessionId sessionId, int requestId, String errorCode, String errorMessage) {
super(
sessionId,
requestId,
ResponseBody.builder().param("CODE", errorCode).param("MSG", errorMessage).build());
}
/**
* {@inheritDoc}
*
* <p>This implementation returns the fixed {@code -ERR} prefix.
*
* @return the {@code -ERR} prefix
*/
@Override
public String prefix() {
return "-ERR";
}
}
@@ -0,0 +1,20 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.response;
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionId;
/**
* A simple success response with an empty body.
*
* <p>Use this to acknowledge successful requests that do not carry additional payload data.
*/
public class OkResponse extends SuccessResponse {
/**
* Create a minimal successful response (no body content).
*
* @param sessionId the target session id
* @param requestId the originating request id
*/
public OkResponse(SessionId sessionId, int requestId) {
super(sessionId, requestId, ResponseBody.builder().build());
}
}
@@ -0,0 +1,12 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.response;
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionId;
/**
* Immutable transport record representing an encoded response ready for delivery to a session.
*
* @param sessionId the target session id
* @param requestId the originating request id
* @param payload the serialized response payload
*/
public record PrimitiveResponse(SessionId sessionId, int requestId, String payload) {}
@@ -0,0 +1,58 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.response;
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionId;
/** Abstract base class for all server responses sent to clients. */
public abstract class Response {
private final SessionId sessionId;
private final int requestId;
private final ResponseBody body;
/**
* Create a new {@code Response}.
*
* @param sessionId the id of the session this response targets
* @param requestId the request identifier this response corresponds to
* @param body the structured response body
*/
protected Response(SessionId sessionId, int requestId, ResponseBody body) {
this.sessionId = sessionId;
this.requestId = requestId;
this.body = body;
}
/**
* Returns the protocol prefix for this response (for example {@code "+OK"} or {@code "-ERR"}).
*
* @return the response prefix string used by the encoder
*/
public abstract String prefix();
/**
* Returns the session id that should receive this response.
*
* @return the target {@link SessionId}
*/
public SessionId getSessionId() {
return sessionId;
}
/**
* Returns the request identifier associated with this response.
*
* @return the numeric request id
*/
public int getRequestId() {
return requestId;
}
/**
* Returns the immutable {@link ResponseBody} that carries the structured payload for this
* response.
*
* @return the response body
*/
public ResponseBody getBody() {
return body;
}
}
@@ -0,0 +1,12 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.response;
import java.util.List;
/**
* A block node that contains a tag and a list of child {@link ResponseNode} elements. Blocks can be
* nested to build hierarchical response bodies.
*
* @param tag the block tag
* @param children the child nodes contained in this block
*/
public record ResponseBlock(String tag, List<ResponseNode> children) implements ResponseNode {}
@@ -0,0 +1,40 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.response;
import java.util.List;
/**
* Immutable container for the structured content of a {@link Response}.
*
* <p>A {@code ResponseBody} holds an ordered list of {@link ResponseNode} items (parameters and
* blocks). Use {@link #builder()} to construct instances.
*/
public class ResponseBody {
private final List<ResponseNode> nodes;
/**
* Package-private constructor used by {@link ResponseBodyBuilder}.
*
* @param nodes the list of response nodes to include in this body
*/
ResponseBody(List<ResponseNode> nodes) {
this.nodes = List.copyOf(nodes);
}
/**
* Create a new {@link ResponseBodyBuilder} for assembling a response body.
*
* @return a fresh builder instance
*/
public static ResponseBodyBuilder builder() {
return new ResponseBodyBuilder();
}
/**
* Returns the ordered list of {@link ResponseNode} elements contained in this body.
*
* @return an immutable list of nodes
*/
public List<ResponseNode> nodes() {
return nodes;
}
}
@@ -0,0 +1,51 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.response;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
/**
* Builder for {@link ResponseBody} instances.
*
* <p>Provides methods to append parameter nodes and nested blocks and to produce an immutable
* {@link ResponseBody} via {@link #build()}.
*/
public class ResponseBodyBuilder {
private final List<ResponseNode> nodes = new ArrayList<>();
/**
* Add a key/value parameter to the response body under construction.
*
* @param key the parameter name
* @param value the parameter value (will be converted to string when encoded)
* @return this builder for fluent chaining
*/
public ResponseBodyBuilder param(String key, Object value) {
nodes.add(new ResponseParameter(key, value));
return this;
}
/**
* Add a nested block with the given tag. The provided consumer receives a child builder to
* populate the block content.
*
* @param tag the block tag
* @param content consumer that appends child nodes to the block
* @return this builder for fluent chaining
*/
public ResponseBodyBuilder block(String tag, Consumer<ResponseBodyBuilder> content) {
ResponseBodyBuilder childBuilder = new ResponseBodyBuilder();
content.accept(childBuilder);
nodes.add(new ResponseBlock(tag, childBuilder.build().nodes()));
return this;
}
/**
* Build an immutable {@link ResponseBody} from the accumulated nodes.
*
* @return a new {@link ResponseBody}
*/
public ResponseBody build() {
return new ResponseBody(nodes);
}
}
@@ -0,0 +1,36 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.response;
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.Session;
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionManager;
/**
* Helper that dispatches {@link Response} instances to the corresponding {@link Session} by
* encoding them and enqueuing the resulting {@link PrimitiveResponse} into the session's response
* queue.
*/
public class ResponseDispatcher {
private final SessionManager sessionManager;
/**
* Create a dispatcher bound to a {@link SessionManager}.
*
* @param sessionManager manager used to resolve sessions
*/
public ResponseDispatcher(SessionManager sessionManager) {
this.sessionManager = sessionManager;
}
/**
* Encode the given {@link Response} and enqueue the resulting {@link PrimitiveResponse} into
* the target session's response queue.
*
* @param response the response to dispatch
* @throws InterruptedException if the thread is interrupted while waiting to enqueue the
* primitive response
*/
public void dispatch(Response response) throws InterruptedException {
PrimitiveResponse primitiveResponse = ResponseEncoder.encode(response);
Session session = sessionManager.getSessionById(response.getSessionId());
session.getResponseQueue().put(primitiveResponse);
}
}
@@ -0,0 +1,98 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.response;
/**
* Utility responsible for encoding a {@link Response} into a protocol payload string and wrapping
* it into a {@link PrimitiveResponse} suitable for transmission.
*/
public class ResponseEncoder {
private static final String INDENT = "\t";
private static final String NEWLINE = "\n";
/**
* Encode a {@link Response} into a {@link PrimitiveResponse} containing the serialized payload
* string.
*
* @param response the response to encode
* @return a {@link PrimitiveResponse} with encoded payload
*/
public static PrimitiveResponse encode(Response response) {
StringBuilder sb = new StringBuilder();
sb.append(response.prefix());
for (ResponseNode node : response.getBody().nodes()) {
sb.append(NEWLINE);
encodeNode(node, sb, 1);
}
sb.append(NEWLINE).append("END");
return new PrimitiveResponse(
response.getSessionId(), response.getRequestId(), sb.toString());
}
/**
* Internal helper to encode any {@link ResponseNode}.
*
* @param node node to encode
* @param sb string builder to append to
* @param depth current indentation depth
*/
private static void encodeNode(ResponseNode node, StringBuilder sb, int depth) {
if (node instanceof ResponseParameter param) {
encodeParameter(param, sb, depth);
} else if (node instanceof ResponseBlock block) {
encodeBlock(block, sb, depth);
}
}
/**
* Encode a {@link ResponseParameter} into the string builder.
*
* @param param the parameter to encode
* @param sb the output builder
* @param depth indentation depth
*/
private static void encodeParameter(ResponseParameter param, StringBuilder sb, int depth) {
sb.append(INDENT.repeat(depth));
sb.append(param.key());
sb.append("=");
sb.append(maskIfNeeded(param.value().toString()));
}
/**
* Encode a {@link ResponseBlock}, including its children and terminating with an {@code END}
* marker.
*
* @param block the block to encode
* @param sb the output builder
* @param depth current indentation depth
*/
private static void encodeBlock(ResponseBlock block, StringBuilder sb, int depth) {
sb.append(INDENT.repeat(depth));
sb.append(block.tag());
for (ResponseNode child : block.children()) {
sb.append(NEWLINE);
encodeNode(child, sb, depth + 1);
}
sb.append(NEWLINE);
sb.append(INDENT.repeat(depth));
sb.append("END");
}
/**
* Quote or escape the provided value if it contains whitespace or single quotes so the encoded
* payload remains parseable.
*
* @param value the raw string value
* @return quoted/escaped value
*/
private static String maskIfNeeded(String value) {
if (value.contains(" ") || value.contains("'")) {
String escaped = value.replace("'", "\\'");
return "'" + escaped + "'";
}
return value;
}
}
@@ -0,0 +1,8 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.response;
/**
* Marker interface for elements that may appear in a {@link ResponseBody}.
*
* <p>Implementations include {@link ResponseParameter} and {@link ResponseBlock}.
*/
public interface ResponseNode {}
@@ -0,0 +1,23 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.response;
/**
* A parameter node stored in a {@link ResponseBody}.
*
* <p>Represents a simple key/value pair. Callers can use {@link #rawValue()} to obtain the string
* representation of the stored value.
*
* @param key the parameter name
* @param value the parameter value
*/
public record ResponseParameter(String key, Object value) implements ResponseNode {
/**
* Returns the raw string representation of the stored value. This is a convenience wrapper
* around {@code Object#toString()} and may throw {@link NullPointerException} if the stored
* value is {@code null}.
*
* @return the value as string
*/
public String rawValue() {
return value.toString();
}
}
@@ -0,0 +1,34 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.response;
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionId;
/**
* Abstract {@link Response} specialization indicating a successful outcome.
*
* <p>Implementations of this class use the {@code +OK} prefix. It provides a protected constructor
* so subclasses can supply the response body content.
*/
public abstract class SuccessResponse extends Response {
/**
* Create a successful response with the provided body.
*
* @param sessionId the session id this response targets
* @param requestId the originating request id
* @param body the response body
*/
protected SuccessResponse(SessionId sessionId, int requestId, ResponseBody body) {
super(sessionId, requestId, body);
}
/**
* {@inheritDoc}
*
* <p>This implementation returns the fixed {@code +OK} prefix.
*
* @return the {@code +OK} prefix
*/
@Override
public final String prefix() {
return "+OK";
}
}
@@ -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;
}
}
@@ -0,0 +1,40 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.sessions;
import ch.unibas.dmi.dbis.cs108.casono.server.network.events.DisconnectEvent;
import ch.unibas.dmi.dbis.cs108.casono.server.network.events.EventBus;
import java.time.Duration;
import java.time.Instant;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class SessionDisconnectJob implements Runnable {
private final Logger logger;
private final SessionManager sessionManager;
private final EventBus eventBus;
private final Duration timeoutThreshold;
public SessionDisconnectJob(
SessionManager sessionManager, EventBus eventBus, Duration timeoutThreshold) {
this.logger = LogManager.getLogger(SessionDisconnectJob.class);
this.sessionManager = sessionManager;
this.eventBus = eventBus;
this.timeoutThreshold = timeoutThreshold;
}
@Override
public void run() {
logger.debug("Job started.");
Instant threshold = Instant.now().minus(timeoutThreshold);
for (Session session : sessionManager.getAllSessions()) {
if (session.getLastInboundActivity().isBefore(threshold)) {
eventBus.publish(new DisconnectEvent(session.getId()));
logger.info(
"Initiated disconnect of {}, as it hasn't been active since a while",
session.getId());
}
}
logger.debug("Job finished.");
}
}
@@ -0,0 +1,4 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.sessions;
/** The SessionHandle stores the session and the two worker threads associated with the session */
record SessionHandle(Session session, Thread reader, Thread writer) {}
@@ -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;
}
}
@@ -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());
}
}
@@ -0,0 +1,75 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.sessions;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.CommandRouter;
import ch.unibas.dmi.dbis.cs108.casono.server.network.events.DisconnectEvent;
import ch.unibas.dmi.dbis.cs108.casono.server.network.events.EventBus;
import ch.unibas.dmi.dbis.cs108.casono.server.network.parser.CommandParserDispatcher;
import ch.unibas.dmi.dbis.cs108.casono.server.network.parser.PrimitiveRequest;
import ch.unibas.dmi.dbis.cs108.casono.server.network.parser.ProtocolParser;
import ch.unibas.dmi.dbis.cs108.casono.server.network.parser.ProtocolParserException;
import ch.unibas.dmi.dbis.cs108.casono.server.network.parser.RawRequest;
import ch.unibas.dmi.dbis.cs108.casono.server.network.parser.Request;
import ch.unibas.dmi.dbis.cs108.casono.server.network.parser.RequestContext;
import ch.unibas.dmi.dbis.cs108.casono.server.network.transport.RawPacket;
import ch.unibas.dmi.dbis.cs108.casono.server.network.transport.TransportLayer;
import ch.unibas.dmi.dbis.cs108.casono.server.tokenizer.TokenizerException;
import java.io.EOFException;
import java.io.IOException;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class SessionReader implements Runnable {
private final Session session;
private final TransportLayer transport;
private final EventBus eventBus;
private final CommandParserDispatcher dispatcher;
private final CommandRouter router;
private final Logger logger;
public SessionReader(Session session, EventBus eventBus) {
this.session = session;
this.transport = session.getTransport();
this.eventBus = eventBus;
this.dispatcher = session.getDispatcher();
this.router = session.getRouter();
this.logger =
LogManager.getLogger(
SessionReader.class.toString() + "-" + session.getId().value());
}
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
RawPacket rawPacket = null;
try {
rawPacket = transport.read();
session.updateLastInboundActivity();
logger.debug("Recieved: {}", rawPacket);
RawRequest rawRequest = ProtocolParser.parse(rawPacket.payload());
logger.debug("Parsed request to {}", rawRequest);
RequestContext requestContext =
new RequestContext(session.getId(), rawPacket.requestId());
PrimitiveRequest primitiveRequest =
new PrimitiveRequest(
requestContext, rawRequest.command(), rawRequest.parameters());
logger.debug("Converted to {}", primitiveRequest);
Request request = dispatcher.parse(primitiveRequest);
router.execute(request);
} catch (EOFException e) {
logger.info("Client disconnected");
eventBus.publish(new DisconnectEvent(session.getId()));
break;
} catch (TokenizerException | ProtocolParserException e) {
logger.error("Error occured while parsing request. RawPacket: {}", rawPacket, e);
// TODO: Send error response to client
} catch (IOException e) {
logger.error("Unexpected exception while reading from transport", e);
}
}
}
}
@@ -0,0 +1,42 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.sessions;
import ch.unibas.dmi.dbis.cs108.casono.server.network.response.PrimitiveResponse;
import ch.unibas.dmi.dbis.cs108.casono.server.network.transport.RawPacket;
import ch.unibas.dmi.dbis.cs108.casono.server.network.transport.TransportLayer;
import java.io.IOException;
import java.util.concurrent.BlockingQueue;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class SessionWriter implements Runnable {
private final TransportLayer transport;
private final BlockingQueue<PrimitiveResponse> queue;
private final Logger logger;
public SessionWriter(Session session) {
this.transport = session.getTransport();
this.queue = session.getResponseQueue();
this.logger =
LogManager.getLogger(
SessionReader.class.toString() + "-" + session.getId().value());
}
public void run() {
while (!Thread.currentThread().isInterrupted()) {
RawPacket packet = null;
try {
PrimitiveResponse response = queue.take();
packet = new RawPacket(response.requestId(), response.payload());
transport.write(packet);
} catch (IOException e) {
logger.error(
"Unexpected exception while writing to transport. RawPacket: {}",
packet,
e);
} catch (InterruptedException e) {
logger.warn("Thread got interrupted", e);
break;
}
}
}
}
@@ -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) {}
@@ -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();
}
}
@@ -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));
}
}
@@ -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

+25
View File
@@ -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