Compare commits

..

233 Commits

Author SHA1 Message Date
Jona Walpert b205b25b48 Feat: Adds functionality to periodically refresh lobbies to update lobby GUI screen 2026-04-09 10:43:25 +02:00
Jona Walpert df31c307fe Fix: Comment out faulty Unit Test 2026-04-09 10:42:45 +02:00
Jona Walpert f3ecbd12f5 Feat: Add username text field to main Lobby 2026-04-08 13:34:26 +02:00
Jona Walpert 5d4d2655ef Add: Supply gameUI with client service at game startup 2026-04-08 13:33:31 +02:00
Jona Walpert d855f2d568 Feat: add offline mode to prevent errors when trying to send requests to not connected server 2026-04-08 13:29:39 +02:00
Jona Walpert ca782b389e Docs: Translate Javadoc and comments to English 2026-04-08 13:26:36 +02:00
Jona Walpert 04bf7ba545 Refactor: move login function from coreClient to LobbyClient 2026-04-08 13:22:24 +02:00
Jona Walpert 42b5344059 Test: Add in-process TestServer and update LobbyButtonGridManagerTest
Add TestServer that speaks the RawPacket/TcpTransport protocol for CREATE_LOBBY,
GET_LOBBY_STATUS and JOIN_LOBBY. Update LobbyButtonGridManagerTest to start the
TestServer and exercise a real `ClientService(host, port)`, enabling integration-style
tests of the client->transport->server roundtrip without an external server.

Refs #36
2026-04-02 22:28:12 +02:00
Jona Walpert 76e62b3cff Feat: Accept ClientService and delegate to LobbyClient
Add constructor overload that accepts ClientService and constructs a LobbyClient.
Avoids implicit host/port lookup in UI code; caller controls network config.
Keeps rendering/status behavior unchanged (async updates, deterministic ordering).
2026-04-02 22:26:14 +02:00
Jona Walpert 1070b5d4b9 Chore(startup): Propagate server ip:port to UI via system properties
ClientApp.start parses <ip>:<port>, sets `casono.server.host` / `casono.server.port` and forwards the original launch arg to the JavaFX launcher.
Casinomainui reads JavaFX launch args and exposes the same system properties.
CasinomainuiController reads those properties and constructs `ClientService(host, port)` (no hardcoded defaults)."
2026-04-02 22:12:48 +02:00
Jona Walpert 4daf6ff4a2 Fix: Sort button IDs before iterating map for deterministic rendering
Copy mapping.keySet() to a List and call Collections.sort(...) before iterating.
Ensures stable button placement and reproducible image updates (render + update)
2026-04-02 22:00:18 +02:00
Jona Walpert 3153f7027d fix: temporyy fix for NPE and wrong naming 2026-04-02 18:50:20 +02:00
Jona Walpert b5d19d915b Add: first (non functioning) modified version of lobby button controlelr with abckend conenction 2026-04-02 16:31:27 +02:00
Julian Kropff 09c19c2602 Add: documentation for client network architecture 2026-04-02 16:10:33 +02:00
Julian Kropff 77c7f7c0a5 Add: initial client structure for game and network 2026-04-02 16:08:24 +02:00
Mathis Ginkel 6f80501ea2 Add: CoreClient and ChatClient 2026-04-02 11:16:40 +02:00
Julian Kropff 45cd54b981 Add: Client Network Architecture Documentation 2026-04-02 10:54:47 +02:00
Mathis Ginkel 3427c661cf Merge branch 'feat/client-chat' into feat/client-communication
# Conflicts:
#	src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/chatui/ChatController.java
2026-04-02 10:31:28 +02:00
Mathis Ginkel ff07843e0a Fix: small changes in ClientService 2026-04-02 10:22:51 +02:00
Mathis Ginkel cc33411911 Add: basic chat functionality 2026-04-02 10:20:03 +02:00
Lars Simon Winzer 948484853c Merge branch 'chore/44-publish-outreach-as-gitlab-pages' into 'main'
Tested if gitlab instance has enabled gitlab pages

Closes #44

See merge request cs108-fs26/Gruppe-13!71
2026-04-01 22:34:14 +02:00
Lars Simon Winzer 521982f7fc Revert "Ci: Add GitLab Pages deployment job for outreach website"
This reverts commit 0e9cf6ae74.
2026-04-01 22:27:25 +02:00
Lars Simon Winzer 0e9cf6ae74 Ci: Add GitLab Pages deployment job for outreach website 2026-04-01 22:17:02 +02:00
Jona Walpert 120b55f5cc Merge branch 'feat/ui-images' into 'main'
Feat/ui images

See merge request cs108-fs26/Gruppe-13!70
2026-04-01 16:24:48 +00:00
Jona Walpert 5dd620116a Chore: Move old poker chip images to old_images folder 2026-04-01 18:21:17 +02:00
Jona Walpert f4c20e1725 Chore: Rename chip images to lobby button naming scheme 2026-04-01 18:18:25 +02:00
Julian Kropff 0c6595a58b Add: red poker chip images 2026-04-01 17:41:05 +02:00
Julian Kropff 936463c8f1 Add: green poker chip images 2026-04-01 17:40:55 +02:00
Jona Walpert 4c203b279c Merge branch 'chore/42-translate-multiple-client-components-to-english' into 'main'
Chore/42 translate multiple client components to english

Closes #42

See merge request cs108-fs26/Gruppe-13!69
2026-04-01 12:26:50 +00:00
Jona Walpert 983f2cca5e Style: fix checkstyle errors 2026-04-01 14:24:34 +02:00
Jona Walpert 56ba5b81ae Docs: translate client side components to english. This includes comments and javaDoc 2026-04-01 13:48:24 +02:00
Lars Simon Winzer 20e8491cff Merge branch 'feat/35-make-dispatch-method-of-responsedispatcher-unchecked' into 'main'
Refactor dispatch method of ResponseDispatcher to be unchecked

Closes #35

See merge request cs108-fs26/Gruppe-13!67
2026-04-01 13:04:14 +02:00
Lars Simon Winzer 3b7b53d973 Style: Apply Spotless 2026-04-01 12:49:56 +02:00
Lars Simon Winzer df0f8ca44c Add: Handle ResponseDispatchException in SessionReader 2026-04-01 12:49:07 +02:00
Lars Simon Winzer ae98334c56 Docs: Modify JavaDoc to match changes 2026-04-01 12:46:51 +02:00
Lars Simon Winzer 4deeace547 Refactor: dispatch method of ResponseDispatcher is now unchecked and throws ResponseDispatchException 2026-04-01 12:46:14 +02:00
Lars Simon Winzer 68e8bc76b2 Add: Wrapper ResponseDispatchException for ResponseDispatcher 2026-04-01 12:44:37 +02:00
Jona Walpert cb65ad2ed0 Merge branch 'feat/ui-switch-lobby-game' into 'main'
Feat/ui switch lobby game

See merge request cs108-fs26/Gruppe-13!66
2026-04-01 10:34:31 +00:00
Jona Walpert 1d322f5cd8 Style: Fix checkstyle for failed pipeline 2026-04-01 12:31:58 +02:00
Jona Walpert 40ea45460e Docs: JavaDoc for client side standard constructors 2026-04-01 11:44:46 +02:00
Jona Walpert 60f326c4d5 Style: line length checkstyle fix 2026-04-01 11:34:55 +02:00
Jona Walpert 2906c59f25 Merge branch 'main' into 'feat/ui-switch-lobby-game' 2026-04-01 11:24:39 +02:00
Lars Simon Winzer 6736345cbe Merge branch 'feat/34-make-response-use-requestcontext' into 'main'
Refactor response classes to use RequestContext instead of SessionId + request id

Closes #34

See merge request cs108-fs26/Gruppe-13!64
2026-03-31 20:50:07 +02:00
Lars Simon Winzer 307c234c91 Docs: Fix javaDoc comments to match changes 2026-03-31 20:47:46 +02:00
Lars Simon Winzer 2ad2981724 Docs: Improve javaDoc for getSessionId method in Response 2026-03-31 20:36:40 +02:00
Lars Simon Winzer 945418798b Refactor: Replace dedicated fields for SessionId and request id with RequestContext 2026-03-31 20:35:29 +02:00
Lars Simon Winzer 34d782e252 Refactor: Use RequestContext in SessionReader when instantiating responses 2026-03-31 20:32:11 +02:00
Lars Simon Winzer 665e006b89 Refactor: Update response classes to use RequestContext instead of SessionId + request id
BREAKING CHANGES
2026-03-31 20:27:36 +02:00
Julian Kropff da23a30ca5 Add: Background image 2026-03-31 19:46:58 +02:00
Julian Kropff d762f7bb2b Add: Background image 2026-03-31 19:46:28 +02:00
Julian Kropff 605260f0d0 Add: Diamond Card images 2026-03-31 19:36:15 +02:00
Julian Kropff 27913854c5 Add: Heart Card images 2026-03-31 19:35:50 +02:00
Lars Simon Winzer 56667bfb12 Merge branch 'feat/33-commandparser-generic-type' into 'main'
Refactor CommandParser interface to use generic type extending Request

Closes #33

See merge request cs108-fs26/Gruppe-13!63
2026-03-31 19:32:33 +02:00
Lars Simon Winzer 3a0248e0c3 Refactor: Update CommandParser interface to use generic type 2026-03-31 19:29:08 +02:00
Julian Kropff ecd66a4ee9 Add: Pik Card images 2026-03-31 19:27:07 +02:00
Julian Kropff ae8eccb158 Add: Cross Card images 2026-03-31 19:20:45 +02:00
Julian Kropff 7a29ce0b3b Add: Diamond Card image 2026-03-31 19:06:24 +02:00
Julian Kropff 865d5bb0dc fix: rename 2026-03-31 18:44:19 +02:00
Julian Kropff 8ee5114d37 Add: poker chip images 2026-03-31 18:42:36 +02:00
Julian Kropff c3bf2d4bb0 fix: rename 2026-03-31 17:46:28 +02:00
Julian Kropff 874df242ab Add: Pik Card image 2026-03-31 17:45:52 +02:00
Julian Kropff 06e49a756c Add: Heart Card image 2026-03-31 17:45:05 +02:00
Julian Kropff b88fdceb9f Add: Cross Card image 2026-03-31 17:44:11 +02:00
Lars Simon Winzer 595800a469 Merge branch 'chore/30-javadoc-ci-job' into 'main'
Add javaDoc gradle task as job to CI pipeline

Closes #30

See merge request cs108-fs26/Gruppe-13!62
2026-03-31 13:06:47 +02:00
Lars Simon Winzer 701fa9b6ed Ci: Add javadoc gradle task as job to CI pipeline 2026-03-31 13:01:30 +02:00
Lars Simon Winzer b0b8e3b1d0 Merge branch 'chore/31-feature-request-issue-template-checklist' into 'main'
Fix minor formatting errors in the issue templates

Closes #32 and #31

See merge request cs108-fs26/Gruppe-13!61
2026-03-31 12:44:17 +02:00
Lars Simon Winzer 3af362a9e5 Chore: Fix typo in Bug Report template checklist 2026-03-31 12:39:19 +02:00
Lars Simon Winzer 65db63d3a1 Chore: Update checklist in Feature Request template 2026-03-31 12:39:03 +02:00
Lars Simon Winzer 9a81c6f1af Merge branch 'chore/29-javadoc-server-errors' into 'main'
Reflect criticial changes to JavaDoc docstrings for server

Closes #29

See merge request cs108-fs26/Gruppe-13!60
2026-03-31 12:29:21 +02:00
Lars Simon Winzer 972e46f4d0 Style: Apply Spotless 2026-03-31 12:27:17 +02:00
Lars Simon Winzer 4e9082f106 Chore: Update Javadoc for ProtocolParser 2026-03-31 12:23:43 +02:00
Lars Simon Winzer e210bd1d49 Chore: Update Javadoc for Session classes 2026-03-31 12:23:30 +02:00
Lars Simon Winzer 238cf937f4 Merge branch 'fix/28-incomplete-transmission-of-response' into 'main'
Fix incorrect order data was written by TcpTransport

Closes #28

See merge request cs108-fs26/Gruppe-13!59
2026-03-31 12:12:57 +02:00
Lars Simon Winzer e07f107384 Fix: Incorrect order data was written out 2026-03-31 12:03:45 +02:00
Lars Simon Winzer 600d10286f Merge branch 'feat/27-finalize-serverside-exception-handling' into 'main'
Finalize exception handling in SessionReader

Closes #27

See merge request cs108-fs26/Gruppe-13!58
2026-03-31 10:11:10 +02:00
Jona Walpert 2cd4324c68 Merge branch 'chore/add-build-cs108-task' into 'main'
Chore: Add build-cs108 Gradle task and javadocJar

See merge request cs108-fs26/Gruppe-13!57
2026-03-30 21:36:45 +00:00
Jona Walpert 45a6b87303 Chore: Add build-cs108 Gradle task and javadocJar 2026-03-30 23:33:48 +02:00
Lars Simon Winzer c6ac3d4aff Add: Handle thrown Exceptions and respond with ErrorResponse 2026-03-30 19:38:39 +02:00
Lars Simon Winzer 73a80deb12 Merge branch 'chore/26-restructure-server-files' into 'main'
Restructure server files and folders

Closes #26

See merge request cs108-fs26/Gruppe-13!56
2026-03-30 18:59:20 +02:00
Lars Simon Winzer 2203f80490 Chore: Reflect path changes for tests 2026-03-30 18:42:19 +02:00
Lars Simon Winzer 059b706485 Chore: Move 'tokenizer' into protocol/parser section 2026-03-30 18:42:05 +02:00
Lars Simon Winzer b588f38cd5 Style: Apply Spotless 2026-03-30 18:27:17 +02:00
Lars Simon Winzer 0930fbe990 Chore: Reflect changes to folder structure in tests 2026-03-30 18:26:45 +02:00
Lars Simon Winzer 86b4a6c191 Chore: Remove leftover TODO 2026-03-30 18:23:41 +02:00
Lars Simon Winzer 6e446b665a Chore: Move 'tokenizer' into protocol section 2026-03-30 18:21:56 +02:00
Lars Simon Winzer 442a846c71 Chore: Move 'response' into protocol section 2026-03-30 18:18:22 +02:00
Lars Simon Winzer e0b791589c Chore: Move 'request' folder into protocol section 2026-03-30 18:17:11 +02:00
Lars Simon Winzer cf0cd2666c Move ResponseDispatcher to 'dispatcher' subfolder in response section 2026-03-30 18:06:43 +02:00
Lars Simon Winzer ba6ff36ee2 Chore: Move ResponseBodyBuilder to 'builder' subfolder in response section 2026-03-30 18:00:34 +02:00
Lars Simon Winzer ed24ba7214 Chore: Move command parsing to 'parsing' subfolder in command section 2026-03-30 17:54:19 +02:00
Lars Simon Winzer 52daf4f2e0 Chore: Move CommandRouter and CommandHandler to execution subfolder in command section 2026-03-30 17:52:10 +02:00
Lars Simon Winzer 050b349638 Chore: Move ProtocolParser to 'protocol' folder for protocol-level processing 2026-03-30 17:50:30 +02:00
Lars Simon Winzer d4b5010158 Chore: Rename Parameter to RequestParameter 2026-03-30 17:45:08 +02:00
Lars Simon Winzer f9519a6eb5 Chore: Move RequestParameterAccessor and related files into subfolder of request 2026-03-30 17:43:31 +02:00
Lars Simon Winzer 8b8ac9ccc0 Chore: Move request related files into own folder 2026-03-30 17:41:55 +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
Mathis Ginkel dd721bda3f Merge remote-tracking branch 'origin/main' 2026-03-28 12:45:53 +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
Mathis Ginkel 68c7003c5d Merge remote-tracking branch 'origin/main' 2026-03-23 21:06:41 +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
Mathis Ginkel b4f34e00f2 Merge remote-tracking branch 'origin/main' 2026-03-21 10:01:34 +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
Mathis Ginkel 26a1ba945a Merge remote-tracking branch 'origin/main' 2026-03-20 13:23:01 +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
Jona Walpert 4dcade3daf Add: Javadoc for client sided classes and methods 2026-03-20 11:41:19 +01:00
Jona Walpert f78e9dfa38 Style: Magic numbers 2026-03-20 11:07:17 +01:00
Jona Walpert af37e33f07 Style: Line length 2026-03-20 10:57:31 +01:00
Julian Kropff 3d8bad2427 Add: first three poker cards 2026-03-19 20:10:19 +01:00
Julian Kropff c88c809a2e Add: Casono logos 2026-03-19 20:09:36 +01:00
Julian Kropff ab851113e0 Add: poker chip images 2026-03-19 20:07:28 +01:00
Julian Kropff 4dd9c56e02 Add: background images 2026-03-19 20:05:05 +01:00
Julian Kropff 0e7507f2c4 Add: loading animation 2026-03-19 20:02:51 +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
Jona Walpert 52bbbd6181 Fix: buttons now change their size relative to screen size 2026-03-19 14:21:43 +01:00
Jona Walpert 1727aec42c Add: chatui added to lobby ui 2026-03-19 14:16:18 +01:00
Julian Kropff 8639fa420f Fix: change file names to conform convention 2026-03-19 13:27:32 +01:00
Jona Walpert 85f89a91f1 Refactor: Update unit tests for LobbyButtonTranslationManager and LobbyButtonGridManager 2026-03-19 12:54:50 +01:00
Jona Walpert 08b85af147 Add: Add: Singleton for LobbyButtonTranslationManager and changing exit button behaviour 2026-03-19 12:54:06 +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 5401ead198 add: Network Protocol docs 2026-03-15 22:31:10 +01: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
229 changed files with 3681 additions and 410 deletions
+1
View File
@@ -122,3 +122,4 @@ $RECYCLE.BIN/
## bin ## bin
bin/ bin/
gradle.properties
+18 -7
View File
@@ -6,11 +6,13 @@ stages:
# Reusable definitions # Reusable definitions
.gradle-cache: &gradle-cache .gradle-cache: &gradle-cache
variables:
GRADLE_USER_HOME: '$CI_PROJECT_DIR/.gradle-home'
cache: cache:
key: '$CI_PROJECT_ID-gradle' key: '$CI_PROJECT_ID-gradle'
paths: paths:
- .gradle/ - .gradle/
- ~/.gradle/caches/ - .gradle-home/
.on-commit: &on-commit .on-commit: &on-commit
rules: rules:
@@ -22,7 +24,7 @@ stages:
.on-mr: &on-mr .on-mr: &on-mr
rules: rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"' - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
allow_failure: true # deactivate after all issues (shown by checkstyle) have been resolved allow_failure: false
# Jobs # Jobs
checkstyle: checkstyle:
@@ -30,7 +32,7 @@ checkstyle:
stage: lint stage: lint
image: gradle:9.3.1-jdk25 image: gradle:9.3.1-jdk25
script: script:
- gradle checkstyleMain checkstyleTest - gradle checkstyleMain checkstyleTest --configuration-cache --configuration-cache-problems=warn
allow_failure: true allow_failure: true
artifacts: artifacts:
when: always when: always
@@ -47,8 +49,8 @@ checkstyle-mr:
stage: lint stage: lint
image: gradle:9.3.1-jdk25 image: gradle:9.3.1-jdk25
script: script:
- gradle checkstyleMain checkstyleTest - gradle checkstyleMain checkstyleTest --configuration-cache --configuration-cache-problems=warn
allow_failure: true allow_failure: false
artifacts: artifacts:
when: always when: always
paths: paths:
@@ -66,6 +68,7 @@ checkstyle-report:
- job: checkstyle-mr - job: checkstyle-mr
artifacts: true artifacts: true
optional: true optional: true
when: always
rules: rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"' - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
script: script:
@@ -81,7 +84,7 @@ compile-check:
stage: build stage: build
image: gradle:9.3.1-jdk25 image: gradle:9.3.1-jdk25
script: script:
- gradle compileTestJava - gradle compileTestJava --configuration-cache --configuration-cache-problems=warn
needs: [] needs: []
rules: rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"' - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
@@ -89,12 +92,20 @@ compile-check:
- if: '$CI_COMMIT_BRANCH' - if: '$CI_COMMIT_BRANCH'
allow_failure: false allow_failure: false
javadoc-check:
<<: [*gradle-cache, *on-mr]
stage: build
image: gradle:9.3.1-jdk25
script:
- gradle javaDoc --configuration-cache --configuration-cache-problems=warn
needs: []
test: test:
<<: *gradle-cache <<: *gradle-cache
stage: test stage: test
image: gradle:9.3.1-jdk25 image: gradle:9.3.1-jdk25
script: script:
- gradle test - gradle test --configuration-cache --configuration-cache-problems=warn
artifacts: artifacts:
when: always when: always
reports: reports:
+40
View File
@@ -0,0 +1,40 @@
## Bug Report
<!-- The reccommended type is: Issue -->
### Environment
- **Branch & Commit:**
- **Operating system:**
- **How was execution started (Gradle task / IDE debug / IDE run):**
- **Java version:** `25`
- **Gradle version**: `9.3.1`
### Summary
<!-- Short, precise description of the bug -->
### Expected Behavior
<!-- What you expected to happen -->
### Actual Behavior
<!-- What actually happened instead -->
### Steps to Reproduce
<!--
1. Do this first
2. Then do that next
3. Lastly, see the unexprected ...
-->
### Stack Trace / Error Message
```
<!-- Paste stack trace or log output here -->
```
### Possible Cause / Notes
<!-- Optional: your own hypothesis about the root cause -->
### Checklist
- [ ] I reproduced the problem using the steps above
- [ ] I searched documentation 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 have described the function in detail
- [ ] I searched docs for alternative implementations matching my needs
- [ ] I added relevant labels
/label ~enhancement
+24
View File
@@ -0,0 +1,24 @@
## Task
<!-- The reccommended type is: Task -->
### Summary
<!-- What needs to be implemented? -->
### Context
<!-- Which part of the system does it belong to? -->
<!-- Examples: Networking, Game-Engine, User interface, ... -->
### Current Progress
- [ ]
- [ ]
- [ ]
### Notes
<!-- Optional: Implementation hints, links to prior design discussions, etc. -->
### Checklist
- [ ] I outlined checkpoints describing phases of my work related to this task
- [ ] I added relevant labels
- [ ] I linked to other issues or branches that must be completed first
/label ~task
+208
View File
@@ -0,0 +1,208 @@
# Contribution Guidelines
This document describes the conventions and workflows everyone must follow to keep the codebase consistent and the collaboration smooth.
If you notice a violation, speak to the person involved respectfully.
Since this project is part of a course at the University of Basel, the [Code of Conduct](https://www.unibas.ch/de/Universitaet/Administration-Services/Vizerektorat-People-And-Culture/Persoenliche-Integritaet/Code-of-Conduct.html) applies.
## Table of Contents
- [Contribution Guidelines](#contribution-guidelines)
- [Table of Contents](#table-of-contents)
- [Issues \& Tasks](#issues--tasks)
- [Creating an issue](#creating-an-issue)
- [During implementation](#during-implementation)
- [Collaborative work](#collaborative-work)
- [Git Workflow](#git-workflow)
- [Creating a branch](#creating-a-branch)
- [Working on a branch](#working-on-a-branch)
- [Commit Messages](#commit-messages)
- [Rules](#rules)
- [Examples](#examples)
- [Code Style](#code-style)
- [Linter](#linter)
- [Formatter](#formatter)
- [General guidelines](#general-guidelines)
- [CI/CD Pipeline](#cicd-pipeline)
- [Before pushing](#before-pushing)
- [Merge Requests](#merge-requests)
- [Opening a MR](#opening-a-mr)
- [Merging](#merging)
- [After merging](#after-merging)
- [Be human](#be-human)
## Issues & Tasks
Every piece of work - whether a new feature, a bug fix, or a refactoring - must be tracked as an
Issue or Task in GitLab **before** any implementation begins.
### Creating an issue
1. Open a new Issue or Task using the **relevant template** provided in the repository.
2. Fill in **all fields** specified by the template thoughtfully and completely. A well-written issue is the single source of truth for the work being done - treat it accordingly.
3. Work through the **checklist** in the template before marking the issue as ready. Do not skip items.
### During implementation
- If you encounter a problem or an unexpected finding while working on an issue, record it as a **comment** on the issue. This keeps the history intact and visible to the whole team.
- **Do not restructurally edit the original description** to incorporate new information. The description reflects the intent at the time the issue was created; comments document what happened along the way.
### Collaborative work
- When multiple people are working on the same issue, **prefer issue comments over private messages** for coordination. This keeps the current status, decisions, and open questions
centrally visible and searchable.
- Before starting work that overlaps with an existing issue, check its comment thread first to avoid duplicating effort.
## Git Workflow
We use a **feature branch -> main** strategy. The `main` branch is always in a releasable state.
### Creating a branch
We follow the [**Conventional Branch**](https://conventional-branch.github.io/) specification. Branch names follow this pattern:
```
<type>/<short-description>
```
| Type | When to use |
|------------|--------------------------------------------------|
| `feat` | New feature or capability |
| `fix` | Bug fix |
| `refactor` | Restructuring without behaviour change |
| `test` | Adding or fixing tests |
| `ci` | Pipeline, Gradle, or tooling changes |
| `docs` | Documentation only |
**Examples:**
```
feat/reconnect-command
fix/session-writer-flush
refactor/user-registry-cleanup
docs/contributing
```
### Working on a branch
Keep branches short-lived. A branch should represent one cohesive unit of work.
It is permissible to commit changes within a feature branch that cause the program to become non-functional, but these should be fixed as soon as possible. In any case, the code that is merged into `main` must be functional.
And most importantly: **Do not commit directly to `main`**.
## Commit Messages
We follow the [**Conventional Commits**](https://www.conventionalcommits.org/) specification. Every commit message must have the form:
```
<type>: <short summary>
[optional body]
```
### Rules
The summary line must be **<= 72 characters**, written in the **imperative mood** (e.g. "add", not "added" or "adds").
### Examples
```
Reat: Add RECONNECT command handler
The handler re-associates an existing User with a new Session after
a connection drop, preserving in-flight state.
```
```
Fix: Flush output stream before closing
```
```
Ci: Tighten Checkstyle failure policy to allow_failure: false
```
```
Refactor: Replace ArrayList with CopyOnWriteArrayList
```
## Code Style
Code formatting is enforced automatically. **Do not submit a MR with formatting violations.**
### Linter
We use **Checkstyle** as our linter with a custom set of rules tailored to our project.
Checkstyle runs on every pipeline. Fix all violations locally before pushing:
```bash
./gradlew checkstyleMain checkstyleTest
```
### Formatter
We use **Spotless** with the **[Google](https://google.github.io/styleguide/javaguide.html) / [AOSP Java style](https://source.android.com/docs/core/architecture/hidl/code-style?hl=en)**:
- **Indentation:** 4 spaces (no tabs)
- **Line length:** 100 characters
- No decorative blank lines directly after opening braces `{`
- Blank lines are reserved for separating logical sections within a block
Run the formatter before committing:
```bash
./gradlew spotlessApply
```
Check without applying:
```bash
./gradlew spotlessCheck
```
### General guidelines
- Add **JavaDoc** docstrings to classes, interfaces, records and methods.
- Exercise **clean architecture**
- Prefer **stateless components**
- Use **`record` types** for immutable data carriers.
- Log with **Log4J 2** (`log4j-api`). Use the appropriate level (`DEBUG` for pipeline internals, `INFO` for lifecycle events, `WARN`/`ERROR` for recoverable/unrecoverable problems).
## CI/CD Pipeline
The pipeline runs automatically on every push. It has four stages:
```
lint > report > build > test
```
| Stage | Jobs |
|----------|--------------------------------------------|
| `lint` | Spotless check, Checkstyle |
| `report` | Code Quality JSON conversion (only for mr) |
| `build` | `./gradlew assemble` |
| `test` | `./gradlew test` + JUnit result reporting |
### Before pushing
Run the full check suite locally to avoid a broken pipeline:
```bash
./gradlew spotlessCheck checkstyleMain checkstyleTest build test
```
A red pipeline blocks merging. Fix failures before creating your merge request.
## Merge Requests
### Opening a MR
- Target branch is always **`main`**.
- Fill in the MR description: what changed and why. Link the relevant issue (with 'Closing #x') if one exists.
### Merging
A MR can be merged when **all main CI pipeline stages are green** (lint, build, test).
No explicit peer approval is required, but leaving a note or question in the MR thread for non-trivial changes is encouraged.
If you spot a problem in someone else's open MR, comment - **do not push directly to their branch** and try to fix the issue yourself.
### After merging
Delete the feature branch after the MR is merged. GitLab can do this automatically via the
"Delete source branch" checkbox in the MR.
## Be human
We are all human.
We all forget things or make mistakes sometimes.
Its important that we look out for one another and **work together as a team**.
+15 -1
View File
@@ -55,7 +55,7 @@ checkstyle {
spotless { spotless {
java { java {
googleJavaFormat('1.25.2').aosp() googleJavaFormat('1.35.0').aosp()
importOrder() importOrder()
removeUnusedImports() removeUnusedImports()
trimTrailingWhitespace() trimTrailingWhitespace()
@@ -108,3 +108,17 @@ tasks.register('fatJar', Jar) {
configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) }
}) })
} }
tasks.register('javadocJar', Jar) {
group = 'build'
description = 'Assembles a Javadoc JAR.'
dependsOn tasks.named('javadoc')
archiveClassifier = 'javadoc'
from(tasks.javadoc.destinationDir)
}
tasks.register('build-cs108') {
group = 'build'
description = 'Produces executable JAR and Javadoc JAR for CS108.'
dependsOn tasks.named('fatJar'), tasks.named('javadocJar')
}
+8 -12
View File
@@ -20,7 +20,7 @@
<!-- https://checkstyle.sourceforge.io/checks/whitespace/filetabcharacter.html --> <!-- https://checkstyle.sourceforge.io/checks/whitespace/filetabcharacter.html -->
<module name="FileTabCharacter"/> <module name="FileTabCharacter"/>
<!-- Maximale Zeilenlänge --> <!-- Maximale Zeilenlänge (AOSP enforces 100 lines, the Google Java Style Guide 80)-->
<!-- https://checkstyle.sourceforge.io/checks/sizes/linelength.html --> <!-- https://checkstyle.sourceforge.io/checks/sizes/linelength.html -->
<module name="LineLength"> <module name="LineLength">
<property name="max" value="100"/> <property name="max" value="100"/>
@@ -42,16 +42,6 @@
"/> "/>
</module> </module>
<!-- Enforce indentation of four whitespaces -->
<!-- https://checkstyle.sourceforge.io/checks/misc/indentation.html -->
<module name="Indentation">
<property name="basicOffset" value="4"/>
<property name="caseIndent" value="4"/>
<property name="throwsIndent" value="4"/>
<property name="arrayInitIndent" value="4"/>
<property name="lineWrappingIndentation" value="8"/>
</module>
<!-- No wildcard imports (import x.*) --> <!-- No wildcard imports (import x.*) -->
<!-- https://checkstyle.sourceforge.io/checks/imports/avoidstarimport.html --> <!-- https://checkstyle.sourceforge.io/checks/imports/avoidstarimport.html -->
<module name="AvoidStarImport"/> <module name="AvoidStarImport"/>
@@ -62,7 +52,13 @@
<!-- No unordered / ungrouped imports --> <!-- No unordered / ungrouped imports -->
<!-- https://checkstyle.sourceforge.io/checks/imports/importorder.html --> <!-- https://checkstyle.sourceforge.io/checks/imports/importorder.html -->
<module name="ImportOrder"/> <module name="ImportOrder">
<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 --> <!-- Classes, enums, records, ... as PascalCase -->
<!-- https://checkstyle.sourceforge.io/checks/naming/typename.html --> <!-- https://checkstyle.sourceforge.io/checks/naming/typename.html -->
@@ -0,0 +1,63 @@
This Document states the Network-Protocol as it is currently implemented
## GET_MESSAGE_COUNT
This Command gets the number of messages, currently stored in the queue for the specific client.
(Is used together with GET_NEXT_MESSAGE, to get all messages that are currently in the queue)
Example:
```
GET_MESSAGE_COUNT
1
+OK
```
## GET_NEXT_MESSAGE
This Command gets the next Message that is being stored in the queue for the client.
(Command is being sent the amount of times, the GET_MESSAGE_COUNT Command returns)
Example:
```
GET_NEXT_MESSAGE
TYPE=LOBBY GAME=1 USER=player1 TARGET=null TIME=9:30 TEXT="Guten Tag"
+OK
```
## SEND_MESSAGE
This Command gets sent if the user of that client writes a message to one of the three possible chats
(The Arguments / Parameters of the Command describe all the parameters of the Object "Message" being used internally by both the Client and the Server)
Example:
```
SEND_MESSAGE TYPE=LOBBY GAME=1 USER=player1 TARGET=null TIME=10:30 TEXT="Hallo Welt"
+OK
```
## LOG_IN
This Command is used to create a user on the server and associate that user with a username
Server returns the username, slightly changed if it is already used by someone else, and an ID to identify the client.
Example:
```
LOG_IN USERNAME="Peter"
USERNAME="Peter" ID=<random UUID>
+OK
```
## LOG_OUT
This Command is used to quit the connection between client and server.
Example:
```
LOG_OUT
+OK
```
@@ -0,0 +1,249 @@
# Client Nework Architecture
<!-- vim-markdown-toc GFM -->
* [Architecture Overview](#architecture-overview)
* [network/Card.java](#networkcardjava)
* [network/GameState.java](#networkgamestatejava)
* [network/Player.java](#networkplayerjava)
* [network/ChatClient.java](#networkchatclientjava)
* [ChatClient(ClientService clientService)](#chatclientclientservice-clientservice)
* [sendMessage(Message message)](#sendmessagemessage-message)
* [getMessages()](#getmessages)
* [network/ClientService.java](#networkclientservicejava)
* [ClientService(String ip, int port)](#clientservicestring-ip-int-port)
* [processCommand(String message)](#processcommandstring-message)
* [sendRequest(Runnable request)](#sendrequestrunnable-request)
* [getRuntimeException(Exception e)](#getruntimeexceptionexception-e)
* [closeSocket()](#closesocket)
* [writeToTransport(String s) throws IOException](#writetotransportstring-s-throws-ioexception)
* [network/CoreClient.java](#networkcoreclientjava)
* [CoreClient(ClientService clientservice)](#coreclientclientservice-clientservice)
* [ping()](#ping)
* [login(String user)](#loginstring-user)
* [network/GameClient.java](#networkgameclientjava)
* [GameClient(ClientService client)](#gameclientclientservice-client)
* [getGameState()](#getgamestate)
* [parseGameState(String input)](#parsegamestatestring-input)
* [Example server response](#example-server-response)
* [network/LobbyClient.java](#networklobbyclientjava)
* [LobbyClient(ClientService client)](#lobbyclientclientservice-client)
* [fetchLobbyStatusString(int lobbyId)](#fetchlobbystatusstringint-lobbyid)
* [createLobby()](#createlobby)
* [getLobbyId()](#getlobbyid)
* [joinLobby(int lobbyId)](#joinlobbyint-lobbyid)
<!-- vim-markdown-toc -->
## Architecture Overview
```text
client/
├── game/
│ ├── Card.java
│ ├── GameState.java
│ └── Player.java
└── network/
├── ChatClient.java
├── ClientService.java
├── CoreClient.java
├── GameClient.java
└── LobbyClient.java
```
### game/Card.java
Represents a playing card with a value and suit.
### game/GameState.java
Represents the current state of the poker game, including the phase, pot size, current bet, dealer position, active player, community cards, and player information.
### game/Player.java
Represents a player in the poker game, including their name, chip count, current bet, state (e.g., `active`, `folded`), and their hole cards.
### network/ChatClient.java
The ChatClient class is responsible for sending messages to the server and retrieving messages from the server. It uses the ClientService to send commands and receive responses from the server.
#### ChatClient(ClientService clientService)
Constructs a ChatClient with the given ClientService for communication.
- **Parameter (`clientService`)**: The ClientService instance used to send commands and receive responses from the server.
#### sendMessage(Message message)
Send a Message to the server by converting it to a string format and sending a `SEND_MESSAGE` command with the message content as arguments.
- **Parameter (`message`)**: message The Message object to be sent to the server.
#### getMessages()
Retrieve messages from the server by first sending a `GET_MESSAGE_COUNT` command to determine how many messages are available and then sending `GET_NEXT_MESSAGE` commands in a loop to retrieve each message. The retrieved messages are parsed into Message objects and returned as a list.
- **Parameter (`A`)**: list of Message objects representing the messages retrieved from the server.
### network/ClientService.java
The ClientService class is responsible for managing the connection to the server, sending commands, and receiving responses. It uses a TcpTransport to
communicate with the server and an ExecutorService to handle asynchronous requests.
#### ClientService(String ip, int port)
Constructs a ClientService with the given server IP and port. It establishes a socket connection to the server and initializes the TcpTransport and ExecutorService for communication.
- **Parameter (`ip`)**: The IP address of the server to connect to.
- **Parameter (`port`)**: The port number of the server to connect to.
#### processCommand(String message)
Sends a command to the server and waits for the response. The command is sent using the TcpTransport, and the response is read in a loop until a valid response is received. The method handles `+OK` and `-ERROR` responses from the server and returns the actual response content.
- **Parameter (`message`)**: The command message to be sent to the server.
- **Return**: The response from the server as a string.
#### sendRequest(Runnable request)
Helper method to send a request to the server using the ExecutorService. It submits the request as a Runnable task and waits for its completion. If the task is interrupted or encounters an execution exception, it throws a
RuntimeException with the appropriate cause.
- **Parameter (`request`)**: The Runnable task representing the request to be sent to the server.
#### getRuntimeException(Exception e)
Helper method to extract the cause of an exception and return it as a RuntimeException. If the cause is null, it returns the original exception as a RuntimeException. If the cause is already a RuntimeException, it returns it directly. Otherwise, it wraps the cause in a new RuntimeException and returns it.
- **Parameter (`e`)**: The exception from which to extract the cause.
- **Return**: A RuntimeException representing the cause of the original exception.
#### closeSocket()
Closes the socket connection to the server and shuts down the ExecutorService. It also closes the TcpTransport used for communication. If any IOException occurs during this process, it prints the exception to the console.
#### writeToTransport(String s) throws IOException
Helper method to write a command string to the TcpTransport. It generates a unique ID for the command using the idGenerator and sends a RawPacket containing the ID and the command string to the server. If an IOException occurs during this process, it throws a RuntimeException with the cause.
- **Parameter (`s`)**: The command string to be sent to the server.
- **Throws IOException**: If an I/O error occurs while writing to the transport.
### network/CoreClient.java
The CoreClient class provides basic functionalities for communicating with the server, such as sending a ping command to check connectivity and logging in with a username. It uses the ClientService to send commands and receive responses from the server.
#### CoreClient(ClientService clientservice)
Constructs a CoreClient with the given ClientService for communication.
- **Parameter (`clientservice`)**: The ClientService instance used to send commands and receive responses from the server.
#### ping()
Sends a `PING` command to the server to check connectivity. The server should respond with a `PONG` message if the connection is successful.
#### login(String user)
Logs in to the server with the given username by sending a `LOGIN` command.
- **Parameter (`user`)**: The username to log in with.
### network/GameClient.java
The GameClient class is responsible for communicating with the server to retrieve the current game state. It sends a command to the server and parses the response into a structured GameState object.
#### GameClient(ClientService client)
Constructs a GameClient with the given ClientService for communication.
- **Parameter (`client`)**: The ClientService instance used to send commands and receive responses from the server.
#### getGameState()
Retrieves the current game state from the server by sending a command and parsing the response.
- **Return**: A GameState object representing the current state of the game.
#### parseGameState(String input)
Parses the raw response from the server into a structured GameState object.
- **Parameter (`input`)**: The raw response string from the server.
- **Return**: A GameState object representing the current state of the game.
#### Example server response
```text
+OK
PHASE=FLO P
POT=150
CURRENT_BET=50
DEALER=0
ACTIVE_PLAYER=1
CARDS
CARD
VALUE=10
SUIT=H
CARD
VALUE=7
SUIT=S
CARD
VALUE=A
SUIT=D
PLAYERS
PLAYER
NAME=Max
CHIPS=1200
BET=50
STATE=ACTIVE
CARDS
CARD
VALUE=K
SUIT=H
CARD
VALUE=3
SUIT=C
PLAYER
NAME=Anna
CHIPS=800
BET=0
STATE=FOLDED
CARDS
END
```
### network/LobbyClient.java
The LobbyClient class is responsible for communicating with the server to manage game lobbies. It provides methods to create a lobby, join a lobby, and fetch the current status of a lobby by sending appropriate commands to the server and processing the responses.
#### LobbyClient(ClientService client)
Constructs a LobbyClient with the given ClientService for communication.
- **Parameter (`client`)**: The ClientService instance used to send commands and receive responses from the server.
#### fetchLobbyStatusString(int lobbyId)
Fetch the current status of the lobby with the given id from the server.
- **Parameter (`lobbyId`)**: The id of the lobby to fetch the status for.
- **Return**: A string representing the current status of the lobby, as returned by the server.
#### createLobby()
Request the server to create a new lobby and return the id of the newly created lobby.
- **Return**: The id of the newly created lobby, as returned by the server.
#### getLobbyId()
Request the server to return the id of the lobby that the client is currently in.
- **Return**: The id of the lobby that the client is currently in, as returned by the server.
#### joinLobby(int lobbyId)
Request the server to join the lobby with the given id.
- **Parameter (`lobbyId`)**: The id of the lobby to join.
@@ -10,15 +10,15 @@ import org.apache.logging.log4j.Logger;
/** /**
* Entry point for the Casono client application. Handles client startup and connection parameters. * Entry point for the Casono client application. Handles client startup and connection parameters.
* *
* <p>Standardkonstruktor für die Anwendung. * <p>Default constructor for the application.
*/ */
public class ClientApp { public class ClientApp {
private static final Logger LOGGER = LogManager.getLogger(ClientApp.class); private static final Logger LOGGER = LogManager.getLogger(ClientApp.class);
/** Standardkonstruktor. */ /** Default constructor. */
public ClientApp() { public ClientApp() {
// Standardkonstruktor // Default constructor
} }
/** /**
@@ -36,6 +36,12 @@ public class ClientApp {
int port = Integer.parseInt(parts[1]); int port = Integer.parseInt(parts[1]);
LOGGER.info("You've selected the client. It will connect port {} at host {}", port, host); LOGGER.info("You've selected the client. It will connect port {} at host {}", port, host);
Launcher.main(new String[] {}); // Expose the chosen host/port to the UI via system properties so controllers
// (which read System.getProperty("casono.server.host"/"casono.server.port"))
// can obtain the correct connection information.
System.setProperty("casono.server.host", host);
System.setProperty("casono.server.port", Integer.toString(port));
// Forward the original address argument to the launcher as well.
Launcher.main(new String[] { arg });
} }
} }
@@ -0,0 +1,65 @@
package ch.unibas.dmi.dbis.cs108.casono.client.chat;
import ch.unibas.dmi.dbis.cs108.casono.client.network.ChatClient;
import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService;
import java.util.List;
/**
* responsible for the transferring of messages from the server to the ChatModel
* or from the ChatViewController to the server
*/
public class ChatController {
private final String username;
private final ClientService clientService;
private final ChatClient chatClient;
private ChatModel chatModel;
private int game_id;
public ChatController(String username, ClientService clientService) {
this.username = username;
this.clientService = clientService;
this.chatClient = new ChatClient(this.clientService);
}
public void createChat(int game_id) {
chatModel = new ChatModel(ChatModel.ChatType.GLOBAL, username);
this.game_id = game_id;
}
public ChatModel getChatModel() {
return chatModel;
}
/**
* method to send a message, the ChatViewController received to the server
*/
public void sendMessage(String msg, String username) {
Message message = new Message(Message.MessageType.GLOBAL, 0, username, null, msg);
onSendToNetwork(message);
}
/**
* method to get all messages from the server
*/
public Boolean receiveMessage() {
List<Message> newMessages = chatClient.getMessages();
if (!newMessages.isEmpty()) {
for (Message msg : newMessages) {
chatModel.addMessage(msg);
}
return true;
} else { return false; }
}
/**
* method to send a message to the server
* @param message
*/
public void onSendToNetwork(Message message) {
chatClient.sendMessage(message);
}
}
@@ -0,0 +1,67 @@
package ch.unibas.dmi.dbis.cs108.casono.client.chat;
import java.util.ArrayList;
/**
* ChatModel, stores the data for a specific chat
*
* Holds the current state of a chat
*/
public class ChatModel {
public ArrayList<Message> messages;
public ChatType chattype;
public String username;
public int count;
public enum ChatType {
GLOBAL,
LOBBY,
WHISPER
}
/**
* Creates a new ChatModel, given a username of the client
* @param chattype
* @param username
*/
public ChatModel(ChatType chattype, String username) {
this.messages = new ArrayList<Message>();
this.chattype = chattype;
this.username = username;
this.count = 0;
}
/**
* method, used by the ChatViewController, to access all new messages, that are stored in the ChatModel
*/
public synchronized String viewNextMessage() {
count--;
Message msg = messages.getLast();
return String.format("[%s] %s: %s", msg.timestamp, msg.user, msg.getMessage());
}
/**
* Adds a new message
* method used by the ChatController
* @param msg
*/
public synchronized void addMessage(Message msg) {
messages.add(msg);
count++;
}
/**
* method to send all current messages to the ChatViewController, if needed
*/
public void addCompleteChat() {
for (int i = 0; i < this.messages.size(); i++) {
Message msg = this.messages.get(i);
}
}
}
@@ -0,0 +1,136 @@
package ch.unibas.dmi.dbis.cs108.casono.client.chat;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Message Object for internal handling of Chat-Messages
* TODO: Should be used on both sides of the network
*/
public class Message {
private final MessageType type;
private final String message;
public String user;
public String timestamp;
public int game_id = 0;
public String target = null;
public enum MessageType {
GLOBAL, LOBBY, WHISPER
};
/**
* Constructor for creating Messages with all information given
* @param type - Either global, local or whisper
* @param game_id - lobby id, or null, if the type is global
* @param user - username
* @param target - username of the target user, for whisper chat
* @param message
*/
public Message(MessageType type, int game_id, String user, String target, String timestamp, String message) {
this.type = type;
this.game_id = game_id;
this.user = user;
this.target = target;
this.timestamp = timestamp;
this.message = message;
}
/**
* Constructor for creating the Messages of the current user, using this client -> time of writing is being recorded
* @param type - Either global, local or whisper
* @param game_id - lobby id, or null, if the type is global
* @param user - username
* @param target - username of the target user, for whisper chat
* @param message
*/
public Message(MessageType type, int game_id, String user, String target, String message) {
this.type = type;
this.game_id = game_id;
this.user = user;
this.target = target;
this.message = message;
LocalDateTime now = LocalDateTime.now();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm");
this.timestamp = now.format(formatter);
}
public String getMessage() {
return message;
}
public MessageType getMessageType() {
return type;
}
/*
* Method to test the system
* @return - String representation of the Message instance, as for example "player1: Hello World"
public String toString() {
return String.format("%s: %s", this.user, this.message);
}
*/
/**
* Method to create the request representation of the message object, to be sent to the server
* @return - request as specified in the network protocol, as String
*/
public String toArgsString() {
return String.format("TYPE=%s GAME=%d USER=%s TARGET=%s TIME=%s TEXT=%s",
this.type.toString(), this.game_id, this.user, this.target, this.timestamp, this.message);
}
/**
* Pattern, to analyze the response String with the given parameters
*/
public static Pattern msgRex = Pattern.compile(
"TYPE=(?<type>\\w+) GAME=(?<game>\\w+) USER=(?<user>\\w+) TARGET=(?<target>\\w+) TIME=(?<time>[0-9:.]+) TEXT=(?<text>.*)$");
/**
* Method to create a Message Object, from the information given by the String
* @param response - String that got sent as a response from the server
* @return - New Message Object
*/
public static Message toMessage(String response) {
Matcher m = msgRex.matcher(response);
if (! m.matches()) {
throw new RuntimeException("Can not parse message: '" + response+"'");
}
String typeString=m.group("type");
switch (typeString) {
case "GLOBAL":
return new Message(MessageType.GLOBAL,
0,
m.group("user"),
null,
m.group("time"),
m.group("text"));
case "LOBBY":
return new Message(MessageType.LOBBY,
Integer.parseInt(m.group("game")),
m.group("user"),
null,
m.group("time"),
m.group("text"));
case "WHISPER":
return new Message(MessageType.WHISPER,
Integer.parseInt(m.group("game")),
m.group("user"),
m.group("target"),
m.group("time"),
m.group("text"));
default:
throw new RuntimeException("Unknown message type " + typeString);
}
}
}
@@ -0,0 +1,9 @@
package ch.unibas.dmi.dbis.cs108.casono.client.game;
/**
* Represents a playing card with a value and suit.
*/
public class Card {
public String value;
public String suit;
}
@@ -0,0 +1,23 @@
package ch.unibas.dmi.dbis.cs108.casono.client.game;
import ch.unibas.dmi.dbis.cs108.casono.client.game.Card;
import ch.unibas.dmi.dbis.cs108.casono.client.game.Player;
import java.util.ArrayList;
import java.util.List;
/**
* Represents the current state of the poker game, including the phase, pot
* size, current bet, dealer position,
* active player, community cards, and player information.
*/
public class GameState {
public String phase;
public int pot;
public int currentBet;
public int dealer;
public int activePlayer;
public List<Card> communityCards = new ArrayList<>();
public List<Player> players = new ArrayList<>();
}
@@ -0,0 +1,17 @@
package ch.unibas.dmi.dbis.cs108.casono.client.game;
import java.util.ArrayList;
import java.util.List;
/**
* Represents a player in the poker game, including their name, chip count,
* current bet, state (e.g., "active", "folded"), and their hole cards.
*/
public class Player {
public String name;
public int chips;
public int bet;
public String state;
public List<Card> cards = new ArrayList<>();
}
@@ -0,0 +1,61 @@
package ch.unibas.dmi.dbis.cs108.casono.client.network;
import ch.unibas.dmi.dbis.cs108.casono.client.chat.Message;
import java.util.ArrayList;
import java.util.List;
/**
* The ChatClient class is responsible for sending messages to the server and
* retrieving messages from the server. It uses the ClientService to send
* commands and receive responses from the server.
*/
public class ChatClient {
private ClientService clientService;
/**
* Constructs a ChatClient with the given ClientService for communication.
*
* @param clientService The ClientService instance used to send commands and
* receive responses from the server.
*/
public ChatClient(ClientService clientService) {
this.clientService = clientService;
}
/**
* Send a Message to the server by converting it to a string format and
* sending a "SEND_MESSAGE" command with the message content as arguments.
*
* @param message The Message object to be sent to the server.
*/
public void sendMessage(Message message) {
String request = "SEND_MESSAGE " + message.toArgsString();
clientService.processCommand(request);
}
/**
* Retrieve messages from the server by first sending a "GET_MESSAGE_COUNT"
* command to determine how many messages are available and then sending
* "GET_NEXT_MESSAGE" commands in a loop to retrieve each message. The
* retrieved messages are parsed into Message objects and returned as a list.
*
* @return A list of Message objects representing the messages retrieved from
* the server.
*/
public List<Message> getMessages() {
String countStr = clientService.processCommand("GET_MESSAGE_COUNT");
int count = Integer.parseInt(countStr);
System.out.println("Got " + count + " messages");
List<Message> messages = new ArrayList<>();
for (int i = 0; i < count; i++) {
String message = clientService.processCommand("GET_NEXT_MESSAGE");
if (message != null) {
Message message1 = Message.toMessage(message);
messages.add(message1);
}
}
return messages;
}
}
@@ -0,0 +1,190 @@
package ch.unibas.dmi.dbis.cs108.casono.client.network;
import ch.unibas.dmi.dbis.cs108.casono.server.network.transport.RawPacket;
import ch.unibas.dmi.dbis.cs108.casono.server.network.transport.TcpTransport;
import java.net.Socket;
import java.util.ArrayList;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.io.IOException;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
/**
* The ClientService class is responsible for managing the connection to the
* server,
* sending commands, and receiving responses. It uses a TcpTransport to
* communicate
* with the server and an ExecutorService to handle asynchronous requests.
*/
public class ClientService {
private final TcpTransport clienttcptransport;
private final Socket socket;
private final ExecutorService executor;
private final boolean offlineMode;
public static ArrayList<String> response;
private final AtomicInteger idGenerator;
/**
* Constructs a ClientService with the given server IP and port. It establishes
* a socket connection to the server and initializes the TcpTransport and
* ExecutorService for communication.
*
* @param ip The IP address of the server to connect to.
* @param port The port number of the server to connect to.
*/
public ClientService(String ip, int port) {
this.idGenerator = new AtomicInteger(0);
this.offlineMode = false;
try {
socket = new Socket(ip, port);
clienttcptransport = new TcpTransport(socket);
} catch (IOException i) {
throw new RuntimeException(i);
}
executor = Executors.newSingleThreadExecutor();
}
/**
* Constructs a ClientService in offline mode. No network connection will be
* attempted and calls to processCommand will throw a RuntimeException.
*
* @param offline true to create an offline (no-network) client service
*/
public ClientService(boolean offline) {
this.idGenerator = new AtomicInteger(0);
this.offlineMode = offline;
this.socket = null;
this.clienttcptransport = null;
this.executor = Executors.newSingleThreadExecutor();
}
/**
* Returns true if this ClientService is running in offline mode (no network).
*/
public boolean isOffline() {
return offlineMode;
}
/**
* Sends a command to the server and waits for the response. The command is
* sent using the TcpTransport, and the response is read in a loop until a
* valid response is received. The method handles "+OK" and "-ERROR" responses
* from the server and returns the actual response content.
*
* @param message The command message to be sent to the server.
* @return The response from the server as a string.
*/
protected String processCommand(String message) {
if (offlineMode) {
throw new RuntimeException("ClientService is offline: cannot process command");
}
AtomicReference<String> response = new AtomicReference<>();
sendRequest(() -> {
try {
writeToTransport(message);
String responseLine = null;
do {
responseLine = clienttcptransport.read().payload();
System.out.println("Raw message '" + responseLine + "'");
if ("+OK".equals(responseLine)) {
return;
} else if (("-ERROR").equals(responseLine)) {
throw new RuntimeException(responseLine);
}
response.set(responseLine);
} while (true);
} catch (Exception e) {
throw getRuntimeException(e);
}
});
return response.get();
}
/**
* Helper method to send a request to the server using the ExecutorService. It
* submits the request as a Runnable task and waits for its completion. If
* the task is interrupted or encounters an execution exception, it throws a
* RuntimeException with the appropriate cause.
*
* @param request The Runnable task representing the request to be sent to the
* server.
*/
private void sendRequest(Runnable request) {
Future<?> future = executor.submit(request);
try {
future.get();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (ExecutionException e) {
throw getRuntimeException(e);
}
}
/**
* Helper method to extract the cause of an exception and return it as a
* RuntimeException. If the cause is null, it returns the original exception as
* a RuntimeException. If the cause is already a RuntimeException, it returns
* it directly. Otherwise, it wraps the cause in a new RuntimeException and
* returns it.
*
* @param e The exception from which to extract the cause.
* @return A RuntimeException representing the cause of the original exception.
*/
private static RuntimeException getRuntimeException(Exception e) {
Throwable reason = e.getCause();
RuntimeException re;
if (reason == null) {
reason = e;
} else if (reason instanceof RuntimeException rte) {
re = rte;
}
re = new RuntimeException(reason);
return re;
}
/**
* Closes the socket connection to the server and shuts down the
* ExecutorService.
* It also closes the TcpTransport used for communication. If any IOException
* occurs during this process, it prints the exception to the console.
*/
public void closeSocket() {
try {
executor.shutdown();
if (clienttcptransport != null) {
clienttcptransport.close();
}
if (socket != null) {
socket.close();
}
} catch (IOException j) {
System.out.println(j);
}
}
/**
* Helper method to write a command string to the TcpTransport. It generates a
* unique ID for the command using the idGenerator and sends a RawPacket
* containing the ID and the command string to the server. If an IOException
* occurs during this process, it throws a RuntimeException with the cause.
*
* @param s The command string to be sent to the server.
* @throws IOException If an I/O error occurs while writing to the transport.
*/
private void writeToTransport(String s) throws IOException {
int id = this.idGenerator.incrementAndGet();
this.clienttcptransport.write(new RawPacket(id, s));
}
}
@@ -0,0 +1,39 @@
package ch.unibas.dmi.dbis.cs108.casono.client.network;
/**
* The CoreClient class provides basic functionalities for communicating with
* the
* server, such as sending a ping command to check connectivity and logging in
* with a username. It uses the ClientService to send commands and receive
* responses from the server.
*/
public class CoreClient {
private final ClientService clientService;
/**
* Constructs a CoreClient with the given ClientService for communication.
*
* @param clientservice The ClientService instance used to send commands and
* receive responses from the server.
*/
public CoreClient(ClientService clientservice) {
this.clientService = clientservice;
}
/**
* Sends a "PING" command to the server to check connectivity. The server
* should respond with a "PONG" message if the connection is successful.
*/
public void ping() {
clientService.processCommand("PING");
}
/**
* Logs in to the server with the given username by sending a "LOGIN" command.
*
* @param user The username to log in with.
*/
public void login(String user) {
clientService.processCommand("LOGIN USERNAME=" + user);
}
}
@@ -0,0 +1,125 @@
package ch.unibas.dmi.dbis.cs108.casono.client.network;
import ch.unibas.dmi.dbis.cs108.casono.client.game.Card;
import ch.unibas.dmi.dbis.cs108.casono.client.game.GameState;
import ch.unibas.dmi.dbis.cs108.casono.client.game.Player;
import java.util.ArrayList;
import java.util.List;
/**
* The GameClient class is responsible for communicating with the server to
* retrieve the current game state. It sends a command to the server and
* parses the response into a structured GameState object.
*/
public class GameClient {
private final ClientService client;
/**
* Constructs a GameClient with the given ClientService for communication.
*
* @param client The ClientService instance used to send commands and receive
* responses from the server.
*/
public GameClient(ClientService client) {
this.client = client;
}
/**
* Retrieves the current game state from the server by sending a command and
* parsing the response.
*
* @return A GameState object representing the current state of the game.
*/
public GameState getGameState() {
String response = client.processCommand("GET_GAME_STATE");
return parseGameState(response);
}
/**
* Parses the raw response from the server into a structured GameState object.
*
* @param input The raw response string from the server.
* @return A GameState object representing the current state of the game.
*/
private GameState parseGameState(String input) {
GameState state = new GameState();
String[] lines = input.split("\n");
Player currentPlayer = null;
Card currentCard = null;
for (String rawLine : lines) {
String line = rawLine.trim();
if (line.startsWith("+OK") || line.equals("END")) {
continue;
}
if (line.startsWith("PHASE=")) {
state.phase = line.split("=")[1];
}
else if (line.startsWith("POT=")) {
state.pot = Integer.parseInt(line.split("=")[1]);
}
else if (line.startsWith("CURRENT_BET=")) {
state.currentBet = Integer.parseInt(line.split("=")[1]);
}
else if (line.startsWith("DEALER=")) {
state.dealer = Integer.parseInt(line.split("=")[1]);
}
else if (line.startsWith("ACTIVE_PLAYER=")) {
state.activePlayer = Integer.parseInt(line.split("=")[1]);
}
else if (line.startsWith("PLAYER")) {
currentPlayer = new Player();
state.players.add(currentPlayer);
}
else if (line.startsWith("NAME=") && currentPlayer != null) {
currentPlayer.name = line.split("=")[1];
}
else if (line.startsWith("CHIPS=") && currentPlayer != null) {
currentPlayer.chips = Integer.parseInt(line.split("=")[1]);
}
else if (line.startsWith("BET=") && currentPlayer != null) {
currentPlayer.bet = Integer.parseInt(line.split("=")[1]);
}
else if (line.startsWith("STATE=") && currentPlayer != null) {
currentPlayer.state = line.split("=")[1];
}
else if (line.startsWith("CARD")) {
currentCard = new Card();
if (currentPlayer != null) {
currentPlayer.cards.add(currentCard);
} else {
state.communityCards.add(currentCard);
}
}
else if (line.startsWith("VALUE=") && currentCard != null) {
currentCard.value = line.split("=")[1];
}
else if (line.startsWith("SUIT=") && currentCard != null) {
currentCard.suit = line.split("=")[1];
}
}
return state;
}
}
@@ -0,0 +1,77 @@
package ch.unibas.dmi.dbis.cs108.casono.client.network;
/**
* The LobbyClient class is responsible for communicating with the server to
* manage game lobbies. It provides methods to create a lobby, join a lobby,
* and fetch the current status of a lobby by sending appropriate commands to
* the server and processing the responses.
*/
public class LobbyClient {
private final ClientService client;
/**
* Constructs a LobbyClient with the given ClientService for communication.
*
* @param client The ClientService instance used to send commands and receive
* responses from the server.
*/
public LobbyClient(ClientService client) {
this.client = client;
}
public ClientService getClientService() {
return client;
}
/**
* Fetch the current status of the lobby with the given id from the server.
*
* @param lobbyId The id of the lobby to fetch the status for.
* @return A string representing the current status of the lobby, as returned
* by the server.
*/
public String fetchLobbyStatusString(int lobbyId) {
return client.processCommand("GET_LOBBY_STATUS ID=" + lobbyId);
}
/**
* Request the server to create a new lobby and return the id of the newly
* created lobby.
*
* @return The id of the newly created lobby, as returned by the server.
*/
public int createLobby() {
String response = client.processCommand("CREATE_LOBBY");
return Integer.parseInt(response);
}
/**
* Request the server to return the id of the lobby that the client is
* currently in.
*
* @return The id of the lobby that the client is currently in, as returned by
* the server.
*/
public int getLobbyId() {
String response = client.processCommand("GET_LOBBY_ID");
return Integer.parseInt(response);
}
/**
* Request the server to join the lobby with the given id.
*
* @param lobbyId The id of the lobby to join.
*/
public void joinLobby(int lobbyId) {
client.processCommand("JOIN_LOBBY ID=" + lobbyId);
}
/**
* Logs in to the server with the given username by sending a "LOGIN" command.
*
* @param user The username to log in with.
*/
public void login(String user) {
client.processCommand("LOGIN USERNAME=" + user);
}
}
@@ -6,13 +6,13 @@ import javafx.application.Application;
/** /**
* Launcher for the Casono main UI. * Launcher for the Casono main UI.
* *
* <p>Standardkonstruktor für die Anwendung. * <p>Default constructor for the application.
*/ */
public class Launcher { public class Launcher {
/** Standardkonstruktor. */ /** Default constructor. */
public Launcher() { public Launcher() {
// Standardkonstruktor // Default constructor
} }
/** /**
@@ -1,85 +0,0 @@
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,107 @@
package ch.unibas.dmi.dbis.cs108.casono.client.ui.chatui;
import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatController;
import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatModel;
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;
import java.util.Timer;
import java.util.TimerTask;
/**
* Responsible for the presentation of the ChatModel to the Client
*/
public class ChatViewController {
private final ChatModel chatmodel;
private final String username;
private final ChatController controller;
private final Timer timer;
@FXML
private VBox chatVBox;
@FXML
private TextField inputField;
@FXML
private ScrollPane chatScrollPane;
@FXML
private Button sendButton;
@FXML
private Button refreshButton;
private static final int CHAT_PADDING = 20;
public ChatViewController() {
this.username = null;
this.chatmodel = null;
this.controller = null;
this.timer = new Timer();
}
public ChatViewController(String username, ChatModel chatmodel, ChatController controller) {
this.username = username;
this.chatmodel = chatmodel;
this.controller = controller;
this.timer = new Timer();
if (this.controller != null) {
timer.schedule(new TimerTask() {
@Override
public void run() {
System.err.println("tick");
if (ChatViewController.this.controller.receiveMessage()) {
showMessage();
}
}
}, 0, 1000);
}
}
@FXML
public void initialize() {
if (inputField != null) {
inputField.setOnAction(event -> sendMessage());
}
if (sendButton != null) {
sendButton.setOnAction(event -> sendMessage());
}
if (refreshButton != null) {
refreshButton.setOnAction(event -> showMessage());
}
if (chatScrollPane != null && chatVBox != null) {
chatScrollPane.vvalueProperty().bind(chatVBox.heightProperty());
}
}
public void showMessage() {
while (chatmodel.count > 0) {
String msg = chatmodel.viewNextMessage();
Label label = new Label(msg);
label.getStyleClass().add("info-text");
label.setWrapText(true);
label.maxWidthProperty().bind(chatVBox.widthProperty().subtract(CHAT_PADDING));
chatVBox.getChildren().add(label);
}
}
public void sendMessage() {
String message = inputField.getText().trim();
if (!message.isEmpty()) {
inputField.clear();
this.controller.sendMessage(message, username);
}
}
public void endController() {
timer.cancel();
}
}
@@ -15,11 +15,22 @@ import javafx.scene.layout.VBox;
*/ */
public class CasinoGameController { public class CasinoGameController {
/** Standardkonstruktor. Wird von FXML verwendet. */
public CasinoGameController() {
// default constructor for FXML
}
@FXML private Label welcomeText; @FXML private Label welcomeText;
@FXML private VBox casinoTable; @FXML private VBox casinoTable;
// TODO: Test-Logik: wird durch echte Spielinteraktionen ersetzt, // TODO: Test-Logik: wird durch echte Spielinteraktionen ersetzt,
// sobald die GameEngine fertig ist // sobald die GameEngine fertig ist
/**
* Temporäre Test-Methode, die bei Klick auf den Tisch eine Platzhalteraktion ausführt.
*
* <p>Wird in der finalen Implementierung durch die Spiel-Logik ersetzt.
*/
@FXML @FXML
public void onTableClick() { public void onTableClick() {
welcomeText.setText("Einsatz akzeptiert!"); welcomeText.setText("Einsatz akzeptiert!");
@@ -1,3 +1,4 @@
package ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui; package ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui;
import java.io.IOException; import java.io.IOException;
@@ -7,29 +8,48 @@ import javafx.scene.Scene;
import javafx.stage.Stage; import javafx.stage.Stage;
/** /**
* Hauptklasse für das Casino-Spiel-UI. * Main class for the casino game UI.
* *
* <p>Startet die JavaFX-Anwendung, lädt die grafische Oberfläche aus der FXML-Datei und * <p>Starts the JavaFX application, loads the graphical interface from the FXML file,
* initialisiert die Haupt-Stage für das Spiel. * and initializes the main stage for the game.
* *
* <p>Aufgaben: - Lädt die FXML-Oberfläche "/ui-structure/Casinogameui.fxml". - Lädt das * <p>Responsibilities:
* Anwendungs-Icon aus "/images/logoinverted.png". - Startet die Anwendung im Vollbildmodus. * - Loads the FXML interface "/ui-structure/Casinogameui.fxml".
* - Loads the application icon from "/images/logoinverted.png".
* - Starts the application in fullscreen mode.
*/ */
import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService;
public class CasinoGameUI extends Application { public class CasinoGameUI extends Application {
// Static field for ClientService (workaround for JavaFX Application launch)
private static ClientService staticClientService;
public static void setClientService(ClientService clientService) {
staticClientService = clientService;
}
public static ClientService getClientService() {
return staticClientService;
}
/** Default no-arg constructor. */
public CasinoGameUI() {
// default no-arg constructor
}
private static final int DEFAULT_WIDTH = 1200; private static final int DEFAULT_WIDTH = 1200;
private static final int DEFAULT_HEIGHT = 800; private static final int DEFAULT_HEIGHT = 800;
/** /**
* Startet die Haupt-Stage der Anwendung. * Starts the main stage of the application.
* *
* @param stage Die vom System bereitgestellte Haupt-Stage. * @param stage The main stage provided by the system.
* @throws IOException Wenn die FXML-Datei oder Ressourcen nicht geladen werden können. * @throws IOException If the FXML file or resources cannot be loaded.
*/ */
@Override @Override
public void start(Stage stage) throws IOException { public void start(Stage stage) throws IOException {
FXMLLoader fxmlLoader = FXMLLoader fxmlLoader = new FXMLLoader(CasinoGameUI.class.getResource("/ui-structure/Casinogameui.fxml"));
new FXMLLoader(CasinoGameUI.class.getResource("/ui-structure/casinogameui.fxml"));
Scene scene = new Scene(fxmlLoader.load(), DEFAULT_WIDTH, DEFAULT_HEIGHT); Scene scene = new Scene(fxmlLoader.load(), DEFAULT_WIDTH, DEFAULT_HEIGHT);
stage.setTitle("Casono (GAME)"); stage.setTitle("Casono (GAME)");
@@ -41,9 +61,9 @@ public class CasinoGameUI extends Application {
} }
/** /**
* Startpunkt der Anwendung. * Entry point of the application.
* *
* @param args Befehlszeilenargumente. * @param args Command line arguments.
*/ */
public static void main(String[] args) { public static void main(String[] args) {
launch(); launch();
@@ -27,6 +27,8 @@ import javafx.scene.shape.Rectangle;
import javafx.scene.web.WebEngine; import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView; import javafx.scene.web.WebView;
import javafx.stage.Stage; import javafx.stage.Stage;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/** /**
* Experimenteller integrierter Browser für Casono. * Experimenteller integrierter Browser für Casono.
@@ -50,13 +52,17 @@ import javafx.stage.Stage;
*/ */
public class CasinoBrowserController { public class CasinoBrowserController {
/** Standardkonstruktor. Initialisiert den CasinoBrowserController. */
public CasinoBrowserController() {
// Intentionally left blank; controller initialization is FXML-driven.
}
private static final Set<String> TRUSTED_DOMAINS = new HashSet<>(); private static final Set<String> TRUSTED_DOMAINS = new HashSet<>();
private static final CookieManager COOKIE_MANAGER = private static final CookieManager COOKIE_MANAGER =
new CookieManager(null, CookiePolicy.ACCEPT_ORIGINAL_SERVER); new CookieManager(null, CookiePolicy.ACCEPT_ORIGINAL_SERVER);
private static final org.apache.logging.log4j.Logger LOGGER = private static final Logger LOGGER = LogManager.getLogger(CasinoBrowserController.class);
org.apache.logging.log4j.LogManager.getLogger(CasinoBrowserController.class);
private static final int LOGO_HEIGHT = 40; private static final int LOGO_HEIGHT = 40;
private static final int CORNER_RADIUS = 40; private static final int CORNER_RADIUS = 40;
@@ -354,8 +360,9 @@ public class CasinoBrowserController {
fwdBtn.getStyleClass().add("gray-button"); fwdBtn.getStyleClass().add("gray-button");
fwdBtn.setOnAction( fwdBtn.setOnAction(
e -> { e -> {
if (engine.getHistory().getCurrentIndex() int currentIndex = engine.getHistory().getCurrentIndex();
< engine.getHistory().getEntries().size() - 1) { int lastIndex = engine.getHistory().getEntries().size() - 1;
if (currentIndex < lastIndex) {
engine.getHistory().go(1); engine.getHistory().go(1);
} }
}); });
@@ -423,7 +430,7 @@ public class CasinoBrowserController {
Scene scene = new Scene(root, WINDOW_WIDTH, WINDOW_HEIGHT); Scene scene = new Scene(root, WINDOW_WIDTH, WINDOW_HEIGHT);
var css = CasinoBrowserController.class.getResource("/ui-structure/casinogameui.css"); var css = CasinoBrowserController.class.getResource("/ui-structure/Casinogameui.css");
if (css != null) { if (css != null) {
scene.getStylesheets().add(css.toExternalForm()); scene.getStylesheets().add(css.toExternalForm());
@@ -512,10 +519,11 @@ public class CasinoBrowserController {
alert.setTitle("Unbekannte Website"); alert.setTitle("Unbekannte Website");
alert.setHeaderText("Diese Website ist nicht bekannt"); alert.setHeaderText("Diese Website ist nicht bekannt");
alert.setContentText( String content =
host host
+ "\n\nDiese Seite ist nicht vom " + "\n\nDiese Seite ist nicht vom Casono Browser verifiziert.\n"
+ "Casono Browser verifiziert.\nMöchten Sie sie trotzdem öffnen?"); + "Möchten Sie sie trotzdem öffnen?";
alert.setContentText(content);
var stream = CasinoBrowserController.class.getResourceAsStream(LOGO_PATH); var stream = CasinoBrowserController.class.getResourceAsStream(LOGO_PATH);
Image logo = new Image(stream); Image logo = new Image(stream);
@@ -1,12 +1,14 @@
package ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.gameuicomponents; package ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.gameuicomponents;
import javafx.application.Platform; import ch.unibas.dmi.dbis.cs108.casono.client.ui.lobbyui.Casinomainui;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.scene.control.TextField; import javafx.scene.control.TextField;
import javafx.scene.input.KeyCode; import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent; import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseEvent; import javafx.scene.input.MouseEvent;
import javafx.scene.layout.HBox; import javafx.scene.layout.HBox;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/** /**
* Controller für die interaktive Taskleiste innerhalb der Poker-UI. * Controller für die interaktive Taskleiste innerhalb der Poker-UI.
@@ -16,8 +18,12 @@ import javafx.scene.layout.HBox;
*/ */
public class TaskbarController { public class TaskbarController {
private static final org.apache.logging.log4j.Logger LOGGER = /** Standardkonstruktor. Wird von FXML verwendet. */
org.apache.logging.log4j.LogManager.getLogger(CasinoBrowserController.class); public TaskbarController() {
// default constructor for FXML
}
private static final Logger LOGGER = LogManager.getLogger(CasinoBrowserController.class);
@FXML private HBox taskbar; @FXML private HBox taskbar;
@FXML private TextField taskbarInput; @FXML private TextField taskbarInput;
@@ -92,15 +98,21 @@ public class TaskbarController {
processBet(); 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 @FXML
private void onExitButtonClick() { private void onExitButtonClick() {
Platform.exit(); javafx.application.Platform.runLater(
() -> {
// Close game stage
javafx.stage.Stage currentStage =
(javafx.stage.Stage) taskbar.getScene().getWindow();
currentStage.close();
// Lobby-UI starten
try {
new Casinomainui().start(new javafx.stage.Stage());
} catch (Exception e) {
LOGGER.error("Fehler beim Starten der Lobby-UI: {}", e.getMessage());
}
});
} }
/** /**
@@ -10,13 +10,13 @@ import javafx.stage.Stage;
/** /**
* JavaFX Application class for the Casono main UI. * JavaFX Application class for the Casono main UI.
* *
* <p>Standardkonstruktor für die Anwendung. * <p>Default constructor for the application.
*/ */
public class Casinomainui extends Application { public class Casinomainui extends Application {
/** Standardkonstruktor. */ /** Default constructor. */
public Casinomainui() { public Casinomainui() {
// Standardkonstruktor // Default constructor
} }
private static final int SCENE_WIDTH = 1200; private static final int SCENE_WIDTH = 1200;
@@ -30,12 +30,21 @@ public class Casinomainui extends Application {
* @throws IOException If loading the FXML fails. * @throws IOException If loading the FXML fails.
*/ */
public void start(Stage stage) throws IOException { public void start(Stage stage) throws IOException {
FXMLLoader fxmlLoader = // If the launcher passed an address argument (ip:port), expose it as
new FXMLLoader(getClass().getResource("/ui-structure/Casinomainui.fxml")); // system properties so controllers can read it without embedding defaults.
var raw = getParameters().getRaw();
if (raw != null && raw.size() > 0) {
String arg = raw.get(0);
String[] parts = arg.split(":", 2);
if (parts.length == 2) {
System.setProperty("casono.server.host", parts[0]);
System.setProperty("casono.server.port", parts[1]);
}
}
FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/ui-structure/Casinomainui.fxml"));
Scene scene = new Scene(fxmlLoader.load(), SCENE_WIDTH, SCENE_HEIGHT); Scene scene = new Scene(fxmlLoader.load(), SCENE_WIDTH, SCENE_HEIGHT);
stage.setTitle("Casono"); stage.setTitle("Casono");
javafx.scene.image.Image icon = javafx.scene.image.Image icon = new javafx.scene.image.Image(
new javafx.scene.image.Image(
getClass().getResource("/images/logoinverted.png").toExternalForm()); getClass().getResource("/images/logoinverted.png").toExternalForm());
stage.getIcons().add(icon); stage.getIcons().add(icon);
stage.setScene(scene); stage.setScene(scene);
@@ -3,6 +3,9 @@ package ch.unibas.dmi.dbis.cs108.casono.client.ui.lobbyui;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.scene.control.Button; import javafx.scene.control.Button;
import javafx.scene.control.TextField;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.control.Label; import javafx.scene.control.Label;
import javafx.scene.image.Image; import javafx.scene.image.Image;
import javafx.scene.image.ImageView; import javafx.scene.image.ImageView;
@@ -11,23 +14,41 @@ import javafx.scene.layout.VBox;
import javafx.scene.shape.Rectangle; import javafx.scene.shape.Rectangle;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService;
import ch.unibas.dmi.dbis.cs108.casono.client.network.LobbyClient;
/** Controller for the Casono main UI lobby. Handles UI initialization and user actions. */ /**
* Controller for the Casono main UI lobby. Handles UI initialization and user
* actions.
*/
public class CasinomainuiController { public class CasinomainuiController {
private static final Logger LOGGER = LogManager.getLogger(CasinomainuiController.class); private static final Logger LOGGER = LogManager.getLogger(CasinomainuiController.class);
@FXML private AnchorPane rootPane; @FXML
@FXML private Label titleLabel; private AnchorPane rootPane;
@FXML private Label subtitleLabel; @FXML
@FXML private ImageView logoView; private Label titleLabel;
@FXML private Rectangle greenBox; @FXML
@FXML private Button exitbutton; private Label subtitleLabel;
@FXML private VBox casinoTable; @FXML
private ImageView logoView;
@FXML
private Rectangle greenBox;
@FXML
private Button exitbutton;
@FXML
private VBox casinoTable;
@FXML
private TextField usernameField;
@FXML
private Button loginButton;
private LobbyButtonTranslationManager translationManager; private LobbyButtonTranslationManager translationManager;
private LobbyButtonGridManager gridManager; private LobbyButtonGridManager gridManager;
private int nextButtonId = 1; private int nextButtonId = 1;
private LobbyClient lobbyClient;
/** Default constructor for dependency injection by FXMLLoader. */
public CasinomainuiController() { public CasinomainuiController() {
// Default constructor // Default constructor
} }
@@ -39,9 +60,63 @@ public class CasinomainuiController {
subtitleLabel.setText("Texas Hold'em Poker"); subtitleLabel.setText("Texas Hold'em Poker");
logoView.setImage(new Image(getClass().getResource("/images/logo.png").toExternalForm())); logoView.setImage(new Image(getClass().getResource("/images/logo.png").toExternalForm()));
translationManager = new LobbyButtonTranslationManager(); translationManager = LobbyButtonTranslationManager.getInstance();
gridManager = String host = System.getProperty("casono.server.host");
new LobbyButtonGridManager(new javafx.scene.layout.GridPane(), translationManager); int port = Integer.parseInt(System.getProperty("casono.server.port"));
ClientService clientService;
try {
clientService = new ClientService(host, port);
} catch (RuntimeException e) {
LOGGER.warn("Could not connect to server {}:{} — starting in offline mode: {}", host, port, e.getMessage());
clientService = new ClientService(true); // offline mode
}
gridManager = new LobbyButtonGridManager(new javafx.scene.layout.GridPane(), translationManager, clientService);
// LobbyClient will use the provided ClientService; in offline mode calls will
// fail with RuntimeException
lobbyClient = new LobbyClient(clientService);
casinoTable.getChildren().clear();
casinoTable.getChildren().add(gridManager.getGridPane());
gridManager.renderLobbyButtons();
}
/**
* Handles the login button action. Validates input and calls
* LobbyClient.login().
*/
@FXML
public void handleLoginButton() {
String username = usernameField.getText();
if (username == null || username.isBlank()) {
showAlert("Please enter a username.");
return;
}
// Only allow alphanumeric, _ and -
if (!username.matches("[a-zA-Z0-9_-]+")) {
showAlert("Only letters, numbers, '_' and '-' are allowed!");
return;
}
if (lobbyClient.getClientService().isOffline()) {
showAlert("Offline mode: cannot send login to server.");
return;
}
try {
lobbyClient.login(username);
showAlert("Login sent: " + username);
} catch (RuntimeException e) {
LOGGER.error("Login failed: {}", e.getMessage());
showAlert("Login failed: " + e.getMessage());
}
}
/**
* Shows an alert dialog with the given message.
*/
private void showAlert(String message) {
Alert alert = new Alert(AlertType.INFORMATION);
alert.setTitle("Info");
alert.setHeaderText(null);
alert.setContentText(message);
alert.showAndWait();
casinoTable.getChildren().clear(); casinoTable.getChildren().clear();
casinoTable.getChildren().add(gridManager.getGridPane()); casinoTable.getChildren().add(gridManager.getGridPane());
gridManager.renderLobbyButtons(); gridManager.renderLobbyButtons();
@@ -57,17 +132,25 @@ public class CasinomainuiController {
@FXML @FXML
public void handleCreateLobbyButton() { public void handleCreateLobbyButton() {
if (translationManager.isFull()) { if (translationManager.isFull()) {
LOGGER.warn("Grid voll! Keine weiteren Lobbys moeglich."); LOGGER.warn("Grid is full! No more lobbies available.");
return; return;
} }
int buttonId = nextButtonId++; int buttonId = nextButtonId++;
int lobbyId = gridManager.createLobby();
try { try {
String username = usernameField != null ? usernameField.getText() : "<unknown>";
LOGGER.info("Creating lobby for user: {}", username);
// avoid attempting to create a lobby when offline
if (lobbyClient.getClientService().isOffline()) {
LOGGER.warn("Cannot create lobby while offline");
showAlert("Offline mode: cannot create lobby.");
return;
}
int lobbyId = gridManager.createLobby();
translationManager.addLobbyButton(buttonId, lobbyId); translationManager.addLobbyButton(buttonId, lobbyId);
LOGGER.info("ButtonID: {}, LobbyID: {}", buttonId, lobbyId); LOGGER.info("ButtonID: {}, LobbyID: {}", buttonId, lobbyId);
gridManager.renderLobbyButtons(); gridManager.renderLobbyButtons();
} catch (Exception e) { } catch (Exception e) {
LOGGER.error("Fehler beim Hinzufügen: {}", e.getMessage()); LOGGER.error("Failed to create or add lobby: {}", e.getMessage());
} }
} }
} }
@@ -5,6 +5,18 @@ package ch.unibas.dmi.dbis.cs108.casono.client.ui.lobbyui;
* ButtonID to LobbyID. * ButtonID to LobbyID.
*/ */
import java.util.Map; import java.util.Map;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import ch.unibas.dmi.dbis.cs108.casono.client.network.LobbyClient;
import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService;
import javafx.scene.Node;
import javafx.scene.control.Button; import javafx.scene.control.Button;
import javafx.scene.image.Image; import javafx.scene.image.Image;
import javafx.scene.image.ImageView; import javafx.scene.image.ImageView;
@@ -13,10 +25,13 @@ import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
/** /**
* Manages the grid for lobby buttons and rendering. Uses LobbyButtonTranslationManager for mapping * Manages the grid for lobby buttons and rendering. Uses
* LobbyButtonTranslationManager for mapping
* ButtonID to LobbyID. * ButtonID to LobbyID.
*/ */
public class LobbyButtonGridManager { public class LobbyButtonGridManager {
private static final double BUTTON_WIDTH_MARGIN = 20.0;
private static final double BUTTON_MIN_SIZE = 10.0;
private static final Logger LOGGER = LogManager.getLogger(LobbyButtonGridManager.class); private static final Logger LOGGER = LogManager.getLogger(LobbyButtonGridManager.class);
/** GridPane for the button grid. */ /** GridPane for the button grid. */
@@ -25,17 +40,29 @@ public class LobbyButtonGridManager {
/** Manager for mapping ButtonID to LobbyID. */ /** Manager for mapping ButtonID to LobbyID. */
private final LobbyButtonTranslationManager translationManager; private final LobbyButtonTranslationManager translationManager;
/** Number of rows in the grid. */
private static final int ROWS = 2;
/** Number of columns in the grid. */ /** Number of columns in the grid. */
private static final int COLS = 4; private static final int COLS = 4;
/** Path to the button image. */ /** Image for a lobby in CREATED state. */
private static final String BUTTON_IMAGE_PATH = "/images/logo.png"; /** Default fallback image. */
private static final String BUTTON_FALLBACK_IMAGE = "/images/lobbypictures/error.png";
/** Max random lobby id. */ /**
private static final int MAX_RANDOM_LOBBY_ID = 10000; * Template for per-button images. Use: button index and status
* (created|running). Example:
* /images/lobby_1_created.png
*/
private static final String BUTTON_IMAGE_TEMPLATE = "/images/lobbypictures/lobby_%d_%s.png";
/** Cache for loaded Images keyed by resource path. */
private final ConcurrentHashMap<String, Image> imageCache = new ConcurrentHashMap<>();
private final LobbyClient lobbyClient;
/** Executor for background status/network tasks. */
private final ExecutorService executor = Executors.newCachedThreadPool();
/** Scheduler for periodic refresh of lobby mappings. */
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
/** /**
* Constructor for the GridManager. * Constructor for the GridManager.
@@ -44,36 +71,127 @@ public class LobbyButtonGridManager {
* @param translationManager the manager for mapping ButtonID to LobbyID * @param translationManager the manager for mapping ButtonID to LobbyID
*/ */
public LobbyButtonGridManager( public LobbyButtonGridManager(
GridPane gridPane, LobbyButtonTranslationManager translationManager) { GridPane gridPane, LobbyButtonTranslationManager translationManager, LobbyClient lobbyClient) {
this.gridPane = gridPane; this.gridPane = gridPane;
this.translationManager = translationManager; // Always use the singleton
this.translationManager = LobbyButtonTranslationManager.getInstance();
this.lobbyClient = lobbyClient;
// Start periodic refresh to keep mapping in sync with server
startPeriodicRefresh(5, 5);
} }
/** /**
* Renders all lobby buttons in the grid. Creates a button for each mapping with image and event * Convenience constructor: accept a {@link ClientService} and build a
* {@link LobbyClient} from it. This avoids any host/port System.getProperty
* lookups elsewhere — caller controls the ClientService.
*/
public LobbyButtonGridManager(
GridPane gridPane, LobbyButtonTranslationManager translationManager, ClientService clientService) {
this(gridPane, translationManager, new LobbyClient(clientService));
}
/**
* Start periodic refresh of lobby mappings.
*
* @param initialDelay initial delay in seconds
* @param period period in seconds
*/
private void startPeriodicRefresh(long initialDelay, long period) {
scheduler.scheduleAtFixedRate(this::refreshMappings, initialDelay, period, TimeUnit.SECONDS);
}
/**
* Refresh mappings by checking each stored lobby id on the server. If a lobby
* no longer exists (or an error occurs), remove it from the translation map
* and update the UI.
*/
private void refreshMappings() {
Map<Integer, Integer> mapping = translationManager.getButtonIdToLobbyId();
if (mapping.isEmpty()) {
return;
}
// Make a copy of entries to avoid concurrent modification
List<Map.Entry<Integer, Integer>> entries = new ArrayList<>(mapping.entrySet());
for (Map.Entry<Integer, Integer> e : entries) {
int buttonId = e.getKey();
int lobbyId = e.getValue();
CompletableFuture.supplyAsync(() -> {
try {
String status = lobbyClient.fetchLobbyStatusString(lobbyId);
return status;
} catch (Exception ex) {
LOGGER.info("Lobby {} appears missing or error: {}", lobbyId, ex.getMessage());
return null;
}
}, executor).thenAccept(status -> {
if (status == null) {
// remove mapping and update UI
translationManager.removeLobbyButton(buttonId);
javafx.application.Platform.runLater(() -> {
updateLobbyButtonImages();
});
}
});
}
}
// Default client creation removed to avoid implicit IP/port configuration.
// Applications must construct and provide a LobbyClient or ClientService
// explicitly.
/**
* Renders all lobby buttons in the grid. Creates a button for each mapping with
* image and event
* handler. * handler.
*/ */
public void renderLobbyButtons() { public void renderLobbyButtons() {
gridPane.getChildren().clear(); gridPane.getChildren().clear();
int index = 0;
Map<Integer, Integer> mapping = translationManager.getButtonIdToLobbyId(); Map<Integer, Integer> mapping = translationManager.getButtonIdToLobbyId();
if (mapping.isEmpty()) { if (mapping.isEmpty()) {
// No buttons to render
return; return;
} }
for (Map.Entry<Integer, Integer> entry : mapping.entrySet()) { List<Integer> buttonIds = new ArrayList<>(mapping.keySet());
int buttonId = entry.getKey(); Collections.sort(buttonIds);
int index = 0;
for (Integer buttonId : buttonIds) {
int lobbyId = mapping.get(buttonId);
Button btn = new Button(); Button btn = new Button();
btn.setId("lobbyBtn-" + buttonId); btn.setId("lobbyBtn-" + buttonId);
btn.setGraphic( // placeholder image so UI remains responsive
new ImageView(new Image(getClass().getResourceAsStream(BUTTON_IMAGE_PATH)))); Image placeholder = safeLoadImage(BUTTON_FALLBACK_IMAGE);
btn.setOnAction( ImageView imageView = new ImageView(placeholder);
e -> { imageView.setPreserveRatio(true);
Integer lobbyId = translationManager.getLobbyIdForButton(buttonId); imageView.fitWidthProperty().bind(gridPane.widthProperty().divide(COLS).subtract(BUTTON_WIDTH_MARGIN));
if (lobbyId != null) { imageView.setSmooth(true);
joinLobby(lobbyId); btn.setGraphic(imageView);
btn.setMaxWidth(Double.MAX_VALUE);
btn.setMaxHeight(Double.MAX_VALUE);
btn.setMinWidth(BUTTON_MIN_SIZE);
btn.setMinHeight(BUTTON_MIN_SIZE);
GridPane.setHgrow(btn, javafx.scene.layout.Priority.ALWAYS);
GridPane.setVgrow(btn, javafx.scene.layout.Priority.ALWAYS);
final int bId = buttonId;
btn.setOnAction(e -> {
Integer targetLobbyId = translationManager.getLobbyIdForButton(bId);
if (targetLobbyId != null) {
joinLobby(targetLobbyId);
} }
}); });
// async fetch status and update image
CompletableFuture.supplyAsync(() -> lobbyClient.fetchLobbyStatusString(lobbyId), executor)
.thenAccept(statusStr -> {
LobbyStatus status = parseLobbyStatus(statusStr);
String path = getImagePathForButton(buttonId, status == null ? LobbyStatus.CREATED : status);
Image img = safeLoadImage(path);
javafx.application.Platform.runLater(() -> {
ImageView iv = new ImageView(img);
iv.setPreserveRatio(true);
iv.fitWidthProperty()
.bind(gridPane.widthProperty().divide(COLS).subtract(BUTTON_WIDTH_MARGIN));
iv.setSmooth(true);
btn.setGraphic(iv);
});
});
int row = index / COLS; int row = index / COLS;
int col = index % COLS; int col = index % COLS;
gridPane.add(btn, col, row); gridPane.add(btn, col, row);
@@ -81,16 +199,138 @@ public class LobbyButtonGridManager {
} }
} }
/** Possible lobby statuses. */
private enum LobbyStatus {
CREATED,
RUNNING
}
/** /**
* Placeholder for lobby creation logic. Returns a generated lobbyId. * Return the current status of a lobby.
*
* @param lobbyId the lobby id to query
* @return the lobby status (mapped from server string; CREATED or RUNNING)
*/
public LobbyStatus getLobbyStatus(int lobbyId) {
String serverStatus = lobbyClient.fetchLobbyStatusString(lobbyId);
LobbyStatus parsed = parseLobbyStatus(serverStatus);
if (parsed == null) {
// Defensive fallback
LOGGER.error("Unrecognized lobby status '{}' for lobby {}. Defaulting to CREATED.", serverStatus, lobbyId);
return LobbyStatus.CREATED;
}
return parsed;
}
/**
* Parse a status string returned by the server into the local enum.
* Accepts case-insensitive values like "created" / "CREATED" / "running".
* Returns null if the string is not recognized.
*/
private LobbyStatus parseLobbyStatus(String statusStr) {
if (statusStr == null)
return null;
try {
return LobbyStatus.valueOf(statusStr.trim().toUpperCase());
} catch (IllegalArgumentException e) {
return null;
}
}
private String getImagePathForButton(int buttonId, LobbyStatus status) {
String statusStr = status == LobbyStatus.CREATED ? "created" : "running";
return String.format(BUTTON_IMAGE_TEMPLATE, buttonId, statusStr);
}
private Image safeLoadImage(String path) {
// Return cached image if present
Image cached = imageCache.get(path);
if (cached != null) {
return cached;
}
// Attempt to load the requested resource
java.io.InputStream is = getClass().getResourceAsStream(path);
if (is == null) {
LOGGER.debug(
"Image resource not found: {}. Falling back to {}",
path,
BUTTON_FALLBACK_IMAGE);
is = getClass().getResourceAsStream(BUTTON_FALLBACK_IMAGE);
}
Image loaded = null;
try {
if (is != null) {
loaded = new Image(is);
} else {
LOGGER.error(
"Both requested image '{}' and fallback '{}' are missing. No image will be set.",
path,
BUTTON_FALLBACK_IMAGE);
}
} catch (Exception e) {
LOGGER.error("Failed to load image '{}'", path, e);
}
if (loaded == null) {
// leave
// null
}
if (loaded != null) {
imageCache.put(path, loaded);
}
return loaded;
}
/** Update all lobby buttons' images according to the current lobby statuses. */
public void updateLobbyButtonImages() {
Map<Integer, Integer> mapping = translationManager.getButtonIdToLobbyId();
if (mapping.isEmpty()) {
return;
}
List<Integer> buttonIds = new ArrayList<>(mapping.keySet());
Collections.sort(buttonIds);
for (Integer buttonId : buttonIds) {
int lobbyId = mapping.get(buttonId);
CompletableFuture.supplyAsync(() -> getLobbyStatus(lobbyId), executor)
.thenAccept(status -> {
String path = getImagePathForButton(buttonId, status);
javafx.application.Platform.runLater(() -> {
for (Node node : gridPane.getChildren()) {
if (node instanceof Button && ("lobbyBtn-" + buttonId).equals(node.getId())) {
Button btn = (Button) node;
ImageView iv = new ImageView(safeLoadImage(path));
iv.setPreserveRatio(true);
iv.fitWidthProperty()
.bind(
gridPane.widthProperty()
.divide(COLS)
.subtract(BUTTON_WIDTH_MARGIN));
iv.setSmooth(true);
btn.setGraphic(iv);
break;
}
}
});
});
}
}
/**
* Creates a new lobby via the LobbyClient.
* *
* @return The generated lobbyId * @return The generated lobbyId
*/ */
public int createLobby() { public int createLobby() {
// TODO: Replace with actual lobby creation logic try {
int lobbyId = (int) (Math.random() * MAX_RANDOM_LOBBY_ID + 1); int lobbyId = lobbyClient.createLobby();
LOGGER.info("Lobby created: {}", lobbyId); LOGGER.info("Lobby created via LobbyClient: {}", lobbyId);
if (lobbyId <= 0) {
throw new RuntimeException("LobbyClient returned invalid lobby id: " + lobbyId);
}
return lobbyId; return lobbyId;
} catch (Exception e) {
LOGGER.error("Failed to create lobby via LobbyClient: {}", e.getMessage());
throw new RuntimeException("Failed to create lobby", e);
}
} }
/** /**
@@ -99,8 +339,47 @@ public class LobbyButtonGridManager {
* @param lobbyId The lobbyId to join * @param lobbyId The lobbyId to join
*/ */
public void joinLobby(int lobbyId) { public void joinLobby(int lobbyId) {
// TODO: Replace with actual join logic // Request server to join the lobby (blackbox client may throw on failure)
LOGGER.info("Joining lobby: {}", lobbyId); LOGGER.info("Joining lobby: {}", lobbyId);
try {
lobbyClient.joinLobby(lobbyId);
} catch (Exception e) {
LOGGER.error("LobbyClient failed to join lobby {}: {}", lobbyId, e.getMessage());
return;
}
javafx.application.Platform.runLater(
() -> {
// Hide lobby stage (do not close) so we can return later
javafx.scene.Scene scene = gridPane.getScene();
javafx.stage.Stage currentStage = (javafx.stage.Stage) scene.getWindow();
currentStage.hide();
// Prepare game stage and set a handler so that when it is closed the lobby is
// shown and updated
javafx.stage.Stage gameStage = new javafx.stage.Stage();
gameStage.setOnHidden(
ev -> {
try {
currentStage.show();
// refresh mappings immediately when returning from game
refreshMappings();
updateLobbyButtonImages();
} catch (Exception ex) {
LOGGER.error(
"Error while returning to lobby: {}", ex.getMessage());
}
});
// Start the Game UI using the prepared stage
try {
// ClientService an GameUI übergeben
ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.CasinoGameUI
.setClientService(lobbyClient.getClientService());
new ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.CasinoGameUI().start(gameStage);
} catch (Exception e) {
LOGGER.error("Error starting Game UI: {}", e.getMessage());
// If starting fails, show the lobby again
currentStage.show();
}
});
} }
/** /**
@@ -111,4 +390,21 @@ public class LobbyButtonGridManager {
public javafx.scene.layout.GridPane getGridPane() { public javafx.scene.layout.GridPane getGridPane() {
return gridPane; return gridPane;
} }
/**
* Expose the configured LobbyClient so callers can invoke its methods
* directly (createLobby, fetchLobbyStatusString, joinLobby, ...).
*/
public LobbyClient getLobbyClient() {
return lobbyClient;
}
/**
* Trigger an immediate refresh of mappings (poll server and remove missing
* lobbies). Public so callers can force a refresh when UI focus returns.
*/
public void refreshNow() {
refreshMappings();
}
} }
@@ -4,36 +4,53 @@ import java.util.HashMap;
import java.util.Map; import java.util.Map;
/** /**
* Verwaltet das Mapping zwischen Button-IDs und Lobby-IDs rein im Speicher. Keine Dateioperationen, * Manages the mapping between Button IDs and Lobby IDs in memory only. No file operations,
* nur Laufzeitdatenstruktur. * runtime-only data structure.
*/ */
public class LobbyButtonTranslationManager { public class LobbyButtonTranslationManager {
/** Maximale Anzahl an Buttons/Lobbys */
// Singleton instance
private static LobbyButtonTranslationManager instance;
// Singleton access
/**
* Returns the singleton instance of the manager.
*
* @return the single instance of {@code LobbyButtonTranslationManager}
*/
public static LobbyButtonTranslationManager getInstance() {
if (instance == null) {
instance = new LobbyButtonTranslationManager();
}
return instance;
}
/** Maximum number of buttons/lobbies */
private static final int MAX_BUTTONS = 8; private static final int MAX_BUTTONS = 8;
/** Zuordnung ButtonID → LobbyID */ /** Mapping ButtonID → LobbyID */
private final Map<Integer, Integer> buttonIdToLobbyId = new HashMap<>(); private final Map<Integer, Integer> buttonIdToLobbyId = new HashMap<>();
/** Konstruktor: initialisiert die Zuordnung leer. */ /** Private constructor for the singleton pattern */
public LobbyButtonTranslationManager() { private LobbyButtonTranslationManager() {
// Zuordnung bleibt leer beim Start // Mapping is empty at startup
} }
/** /**
* Prüft, ob das Grid voll ist (MAX_BUTTONS erreicht). * Checks whether the grid is full (MAX_BUTTONS reached).
* *
* @return true, wenn Grid voll; sonst false * @return true if the grid is full; otherwise false
*/ */
public boolean isFull() { public boolean isFull() {
return buttonIdToLobbyId.size() >= MAX_BUTTONS; return buttonIdToLobbyId.size() >= MAX_BUTTONS;
} }
/** /**
* Fügt eine Zuordnung ButtonID → LobbyID hinzu. * Adds a mapping ButtonID → LobbyID.
* *
* @param buttonId Die ID des Buttons * @param buttonId the ID of the button
* @param lobbyId Die ID der Lobby * @param lobbyId the ID of the lobby
* @throws Exception wenn das Grid voll ist * @throws Exception when the grid is full
*/ */
public void addLobbyButton(int buttonId, int lobbyId) throws Exception { public void addLobbyButton(int buttonId, int lobbyId) throws Exception {
if (isFull()) { if (isFull()) {
@@ -43,28 +60,28 @@ public class LobbyButtonTranslationManager {
} }
/** /**
* Entfernt eine Zuordnung für die gegebene ButtonID. * Removes the mapping for the given ButtonID.
* *
* @param buttonId Die ID des zu entfernenden Buttons * @param buttonId the ID of the button to remove
*/ */
public void removeLobbyButton(int buttonId) { public void removeLobbyButton(int buttonId) {
buttonIdToLobbyId.remove(buttonId); buttonIdToLobbyId.remove(buttonId);
} }
/** /**
* Gibt die LobbyID für eine gegebene ButtonID zurück. * Returns the LobbyID for a given ButtonID.
* *
* @param buttonId Die ButtonID * @param buttonId the ButtonID
* @return Die zugehoerige LobbyID oder null, falls nicht vorhanden * @return the associated LobbyID or null if not present
*/ */
public Integer getLobbyIdForButton(int buttonId) { public Integer getLobbyIdForButton(int buttonId) {
return buttonIdToLobbyId.get(buttonId); return buttonIdToLobbyId.get(buttonId);
} }
/** /**
* Gibt die gesamte Zuordnung ButtonID → LobbyID zurück. * Returns the full mapping ButtonID → LobbyID.
* *
* @return Map aller Zuordnungen * @return Map of all mappings
*/ */
public Map<Integer, Integer> getButtonIdToLobbyId() { public Map<Integer, Integer> getButtonIdToLobbyId() {
return buttonIdToLobbyId; return buttonIdToLobbyId;
@@ -3,8 +3,11 @@ package ch.unibas.dmi.dbis.cs108.casono.server;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserCleanupJob; import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserCleanupJob;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserRegistry; import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserRegistry;
import ch.unibas.dmi.dbis.cs108.casono.server.network.NetworkManager; import ch.unibas.dmi.dbis.cs108.casono.server.network.NetworkManager;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandRouter;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.CommandParserDispatcher;
import ch.unibas.dmi.dbis.cs108.casono.server.network.events.DisconnectEvent; import ch.unibas.dmi.dbis.cs108.casono.server.network.events.DisconnectEvent;
import ch.unibas.dmi.dbis.cs108.casono.server.network.events.EventBus; import ch.unibas.dmi.dbis.cs108.casono.server.network.events.EventBus;
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionDisconnectJob;
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionManager; import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionManager;
import java.time.Duration; import java.time.Duration;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
@@ -15,9 +18,12 @@ import org.apache.logging.log4j.Logger;
/** Application class for starting the server. */ /** Application class for starting the server. */
public class ServerApp { public class ServerApp {
public static final int USER_CLEANUP_JOB_DELAY = 0; private static final int USER_CLEANUP_JOB_DELAY = 0;
public static final int USER_CLEANUP_JOB_PERIOD = 10; private static final int USER_CLEANUP_JOB_PERIOD = 10;
public static final int USER_CLEANUP_JOB_RECONNECT_THRESHOLD = 10; private static final int USER_CLEANUP_JOB_RECONNECT_THRESHOLD = 10;
private static final int SESSION_DISCONNECT_JOB_DELAY = 0;
private static final int SESSION_DISCONNECT_JOB_PERIOD = 2;
private static final int SESSION_DISCONNECT_JOB_TIMEOUT = 5;
public static void start(String arg) { public static void start(String arg) {
int port = Integer.parseInt(arg); int port = Integer.parseInt(arg);
@@ -26,10 +32,12 @@ public class ServerApp {
logger.info("Starting server at port {}", port); logger.info("Starting server at port {}", port);
EventBus eventBus = new EventBus(); EventBus eventBus = new EventBus();
SessionManager sessionManager = new SessionManager(); CommandParserDispatcher dispatcher = new CommandParserDispatcher();
eventBus.subscribe( CommandRouter router = new CommandRouter();
DisconnectEvent.class, event -> sessionManager.removeSession(event.sessionId()));
NetworkManager networkManager = new NetworkManager(port, sessionManager, eventBus); 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(); UserRegistry userRegistry = new UserRegistry();
eventBus.subscribe( eventBus.subscribe(
@@ -41,6 +49,14 @@ public class ServerApp {
USER_CLEANUP_JOB_DELAY, USER_CLEANUP_JOB_DELAY,
USER_CLEANUP_JOB_PERIOD, USER_CLEANUP_JOB_PERIOD,
TimeUnit.SECONDS); 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(); networkManager.start();
} }
@@ -58,7 +58,6 @@ public class UserRegistry {
* *
* @param sessionId the session ID of the disconnected client * @param sessionId the session ID of the disconnected client
*/ */
// TODO: Add to EventRegistry with DisconnectEvent
public synchronized void onDisconnect(SessionId sessionId) { public synchronized void onDisconnect(SessionId sessionId) {
User user = bySessionId.remove(sessionId); User user = bySessionId.remove(sessionId);
if (user == null) { if (user == null) {
@@ -1,8 +1,5 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network; package ch.unibas.dmi.dbis.cs108.casono.server.network;
import ch.unibas.dmi.dbis.cs108.casono.server.network.events.DisconnectEvent;
import ch.unibas.dmi.dbis.cs108.casono.server.network.events.EventBus;
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.Session;
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionManager; import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionManager;
import ch.unibas.dmi.dbis.cs108.casono.server.network.transport.TcpTransport; import ch.unibas.dmi.dbis.cs108.casono.server.network.transport.TcpTransport;
import java.io.IOException; import java.io.IOException;
@@ -18,23 +15,19 @@ public class NetworkManager implements Runnable {
private Thread thread; private Thread thread;
private Boolean running; private Boolean running;
private SessionManager sessionManager; private SessionManager sessionManager;
private EventBus eventBus;
/** /**
* Creates a new NetworkManager with the given port, session manager, and event bus. * Creates a new NetworkManager with the given port, session manager, and event bus.
* *
* @param port the port to listen on * @param port the port to listen on
* @param sessionManager the session manager to use * @param sessionManager the session manager to use
* @param eventBus the event bus for events
*/ */
public NetworkManager(Integer port, SessionManager sessionManager, EventBus eventBus) { public NetworkManager(Integer port, SessionManager sessionManager) {
this.port = port; this.port = port;
this.logger = LogManager.getLogger(NetworkManager.class); this.logger = LogManager.getLogger(NetworkManager.class);
this.thread = new Thread(this, "networkManager"); this.thread = new Thread(this, "networkManager");
this.running = true; this.running = true;
this.sessionManager = sessionManager; this.sessionManager = sessionManager;
this.eventBus = eventBus;
this.eventBus.subscribe(DisconnectEvent.class, event -> clientDisconnected(event));
} }
/** Starts the internal thread to accept new connections. */ /** Starts the internal thread to accept new connections. */
@@ -43,15 +36,6 @@ public class NetworkManager implements Runnable {
thread.start(); thread.start();
} }
/**
* Handles client disconnection events.
*
* @param event the disconnect event
*/
public void clientDisconnected(DisconnectEvent event) {
logger.info("Session {} disconnected", event.sessionId().value());
}
/** Runs the network manager loop, accepting connections. */ /** Runs the network manager loop, accepting connections. */
@Override @Override
public void run() { public void run() {
@@ -61,9 +45,7 @@ public class NetworkManager implements Runnable {
logger.debug("Accepted connection from {}", clientSocket.getRemoteSocketAddress()); logger.debug("Accepted connection from {}", clientSocket.getRemoteSocketAddress());
Session session = new Session(new TcpTransport(clientSocket), eventBus); sessionManager.create(new TcpTransport(clientSocket));
sessionManager.addSession(session);
session.start();
} }
} catch (IOException e) { } catch (IOException e) {
@@ -0,0 +1,7 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.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.execution;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.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.execution;
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,18 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.PrimitiveRequest;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.Request;
/**
* Parser to convert the PrimitiveRequest to a Request and performing checks for required fields and
* data types
*/
public interface CommandParser<T extends Request> {
/**
* Parses the provided PrimitiveRequest into a command-specific request
*
* @param primitiveRequest
* @return
*/
T parse(PrimitiveRequest primitiveRequest);
}
@@ -1,5 +1,7 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.parser; package ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.PrimitiveRequest;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.Request;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
@@ -0,0 +1,4 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing;
/** Used in the PrimitiveRequest class to store the key of a parameter with its respective value */
public record RequestParameter(String key, String value) {}
@@ -1,4 +1,4 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.parser; package ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing;
/** /**
* Exception thrown when the CommandParserDispatcher has no registered handler for the provided * Exception thrown when the CommandParserDispatcher has no registered handler for the provided
@@ -1,15 +0,0 @@
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);
}
@@ -1,4 +0,0 @@
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) {}
@@ -1,6 +0,0 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.parser;
import java.util.List;
/** Created by the ProtocolParser to allow easy access to the request contents */
public record PrimitiveRequest(int requestId, String command, List<Parameter> parameters) {}
@@ -1,4 +0,0 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.parser;
/** Request, produced by the CommandParser */
public interface Request {}
@@ -1,11 +1,12 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.parser; package ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.parser;
import ch.unibas.dmi.dbis.cs108.casono.server.network.transport.RawPacket; import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.RequestParameter;
import ch.unibas.dmi.dbis.cs108.casono.server.tokenizer.RawToken; import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.parser.tokenizer.RawToken;
import ch.unibas.dmi.dbis.cs108.casono.server.tokenizer.Token; import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.parser.tokenizer.Token;
import ch.unibas.dmi.dbis.cs108.casono.server.tokenizer.TokenClassifier; import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.parser.tokenizer.TokenClassifier;
import ch.unibas.dmi.dbis.cs108.casono.server.tokenizer.TokenType; import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.parser.tokenizer.TokenType;
import ch.unibas.dmi.dbis.cs108.casono.server.tokenizer.Tokenizer; import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.parser.tokenizer.Tokenizer;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RawRequest;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
@@ -16,18 +17,18 @@ public class ProtocolParser {
/** /**
* Parses the payload of the provided RawPacket * Parses the payload of the provided RawPacket
* *
* @param packet the RawPacket containing the recieved data * @param payload the payload to parse and check for syntax
* @return created PrimitiveRequest * @return created PrimitiveRequest
*/ */
public static PrimitiveRequest parse(RawPacket packet) { public static RawRequest parse(String payload) {
List<RawToken> rawTokens = Tokenizer.tokenize(packet.payload()); List<RawToken> rawTokens = Tokenizer.tokenize(payload);
List<Token> tokens = TokenClassifier.classify(rawTokens); List<Token> tokens = TokenClassifier.classify(rawTokens);
Iterator<Token> iterator = tokens.iterator(); Iterator<Token> iterator = tokens.iterator();
String command = readCommand(iterator); String command = readCommand(iterator);
List<Parameter> parameters = readParameters(iterator); List<RequestParameter> parameters = readParameters(iterator);
return new PrimitiveRequest(packet.requestId(), command, parameters); return new RawRequest(command, parameters);
} }
/** /**
@@ -51,8 +52,8 @@ public class ProtocolParser {
* @param iterator * @param iterator
* @return list containing all parsed parameters * @return list containing all parsed parameters
*/ */
private static List<Parameter> readParameters(Iterator<Token> iterator) { private static List<RequestParameter> readParameters(Iterator<Token> iterator) {
List<Parameter> parameters = new ArrayList<>(); List<RequestParameter> parameters = new ArrayList<>();
try { try {
while (iterator.hasNext()) { while (iterator.hasNext()) {
@@ -66,7 +67,7 @@ public class ProtocolParser {
readSeperator(iterator.next()); readSeperator(iterator.next());
String value = readValue(iterator.next()); String value = readValue(iterator.next());
parameters.add(new Parameter(key, value)); parameters.add(new RequestParameter(key, value));
} }
} catch (NoSuchElementException e) { } catch (NoSuchElementException e) {
throw new ProtocolParserException("Ran out of tokens while reading parameter"); throw new ProtocolParserException("Ran out of tokens while reading parameter");
@@ -1,4 +1,4 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.parser; package ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.parser;
public class ProtocolParserException extends RuntimeException { public class ProtocolParserException extends RuntimeException {
public ProtocolParserException(String message) { public ProtocolParserException(String message) {
@@ -1,4 +1,4 @@
package ch.unibas.dmi.dbis.cs108.casono.server.tokenizer; package ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.parser.tokenizer;
/** Represents a raw (unclassified) token in the tokenizer. */ /** Represents a raw (unclassified) token in the tokenizer. */
public record RawToken(RawTokenType type, String value, int line, int column) {} public record RawToken(RawTokenType type, String value, int line, int column) {}
@@ -1,4 +1,4 @@
package ch.unibas.dmi.dbis.cs108.casono.server.tokenizer; package ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.parser.tokenizer;
public enum RawTokenType { public enum RawTokenType {
WORD, WORD,
@@ -1,4 +1,4 @@
package ch.unibas.dmi.dbis.cs108.casono.server.tokenizer; package ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.parser.tokenizer;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -1,4 +1,4 @@
package ch.unibas.dmi.dbis.cs108.casono.server.tokenizer; package ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.parser.tokenizer;
/** Represents a token in the tokenizer. */ /** Represents a token in the tokenizer. */
public record Token(TokenType type, String value, int line, int column) {} public record Token(TokenType type, String value, int line, int column) {}
@@ -1,4 +1,4 @@
package ch.unibas.dmi.dbis.cs108.casono.server.tokenizer; package ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.parser.tokenizer;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -1,4 +1,4 @@
package ch.unibas.dmi.dbis.cs108.casono.server.tokenizer; package ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.parser.tokenizer;
/** Enumeration of token types used in the tokenizer. */ /** Enumeration of token types used in the tokenizer. */
public enum TokenType { public enum TokenType {
@@ -1,4 +1,4 @@
package ch.unibas.dmi.dbis.cs108.casono.server.tokenizer; package ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.parser.tokenizer;
import java.util.List; import java.util.List;
@@ -1,4 +1,4 @@
package ch.unibas.dmi.dbis.cs108.casono.server.tokenizer; package ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.parser.tokenizer;
/** Exception thrown during tokenization. */ /** Exception thrown during tokenization. */
public class TokenizerException extends RuntimeException { public class TokenizerException extends RuntimeException {
@@ -0,0 +1,8 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.RequestParameter;
import java.util.List;
/** Created by the ProtocolParser to allow easy access to the request contents */
public record PrimitiveRequest(
RequestContext context, String command, List<RequestParameter> parameters) {}
@@ -0,0 +1,6 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.RequestParameter;
import java.util.List;
public record RawRequest(String command, List<RequestParameter> parameters) {}
@@ -0,0 +1,24 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request;
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.protocol.request;
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,26 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.accessor;
/** Exception thrown when a required parameter key is not found. */
public class MissingParameterException extends RuntimeException {
private final String parameterKey;
/**
* Creates a new exception for a missing required parameter.
*
* @param message human-readable description of the missing parameter
* @param parameterKey key of the parameter that could not be found
*/
public MissingParameterException(String message, String parameterKey) {
super(message);
this.parameterKey = parameterKey;
}
/**
* Returns the missing parameter key.
*
* @return key of the parameter that could not be found
*/
public String getParameterKey() {
return parameterKey;
}
}
@@ -0,0 +1,27 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.accessor;
/** 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,116 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.accessor;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.RequestParameter;
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<RequestParameter> parameters) {
this.index =
parameters.stream()
.collect(
Collectors.toUnmodifiableMap(
RequestParameter::key, RequestParameter::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.protocol.request.accessor;
/**
* 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,32 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder.ResponseBody;
/** 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 context the RequestContext of the request
* @param errorCode a short error code identifying the failure
* @param errorMessage a human readable error message
*/
public ErrorResponse(RequestContext context, String errorCode, String errorMessage) {
super(
context,
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.protocol.response;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder.ResponseBody;
/**
* 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 context the RequestContext of the request
*/
public OkResponse(RequestContext context) {
super(context, ResponseBody.builder().build());
}
}
@@ -0,0 +1,12 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.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,57 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder.ResponseBody;
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 RequestContext context;
private final ResponseBody body;
/**
* Create a new {@code Response}.
*
* @param context the RequestContext of the request
* @param body the structured response body
*/
protected Response(RequestContext context, ResponseBody body) {
this.context = context;
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 of the session that should receive this response.
*
* @return the target {@link SessionId}
*/
public SessionId getSessionId() {
return context.sessionId();
}
/**
* Returns the request identifier associated with this response.
*
* @return the numeric request id
*/
public int getRequestId() {
return context.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,34 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder.ResponseBody;
/**
* 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 context the RequestContext of the request
* @param body the response body
*/
protected SuccessResponse(RequestContext context, ResponseBody body) {
super(context, 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,12 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder;
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,41 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.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.protocol.response.builder;
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,8 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder;
/**
* 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.protocol.response.builder;
/**
* 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,7 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher;
public class ResponseDispatchException extends RuntimeException {
public ResponseDispatchException(String message, Throwable cause) {
super(message, cause);
}
}
@@ -0,0 +1,43 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.PrimitiveResponse;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.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 ResponseDispatchException wraps any exceptions that occur during dispatching, such as
* the {@link InterruptedException}
*/
public void dispatch(Response response) {
PrimitiveResponse primitiveResponse = ResponseEncoder.encode(response);
Session session = sessionManager.getSessionById(response.getSessionId());
try {
session.getResponseQueue().put(primitiveResponse);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new ResponseDispatchException("Interrupted while dispatching response", e);
}
}
}
@@ -0,0 +1,104 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.PrimitiveResponse;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.Response;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder.ResponseBlock;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder.ResponseNode;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder.ResponseParameter;
/**
* 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;
}
}
@@ -1,41 +1,41 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.sessions; package ch.unibas.dmi.dbis.cs108.casono.server.network.sessions;
import ch.unibas.dmi.dbis.cs108.casono.server.network.events.DisconnectEvent; import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandRouter;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.CommandParserDispatcher;
import ch.unibas.dmi.dbis.cs108.casono.server.network.events.EventBus; import ch.unibas.dmi.dbis.cs108.casono.server.network.events.EventBus;
import ch.unibas.dmi.dbis.cs108.casono.server.network.parser.PrimitiveRequest; import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.PrimitiveResponse;
import ch.unibas.dmi.dbis.cs108.casono.server.network.parser.ProtocolParser;
import ch.unibas.dmi.dbis.cs108.casono.server.network.transport.RawPacket;
import ch.unibas.dmi.dbis.cs108.casono.server.network.transport.TransportLayer; import ch.unibas.dmi.dbis.cs108.casono.server.network.transport.TransportLayer;
import java.io.EOFException; import java.time.Instant;
import java.io.IOException; import java.util.concurrent.ArrayBlockingQueue;
import org.apache.logging.log4j.LogManager; import java.util.concurrent.BlockingQueue;
import org.apache.logging.log4j.Logger;
/** Represents a client session in the network server. */ /** Represents a client session in the network server. */
public class Session implements Runnable { public class Session {
private SessionId id; private final SessionId id;
private Thread thread; private Instant lastActivity;
private TransportLayer transport; private final TransportLayer transport;
private Logger logger; private final BlockingQueue<PrimitiveResponse> responseQueue;
private Boolean running; private final CommandParserDispatcher dispatcher;
private EventBus eventBus; private final CommandRouter router;
private static final int RESPOND_QUEUE_SIZE = 32;
/** /**
* Creates a new Session with the given transport and event bus. * Creates a new Session with the given transport and event bus.
* *
* @param transport the transport layer for communication * @param transport the transport layer for communication
* @param eventBus the event bus for publishing events * @param eventBus the event bus for publishing events
* @throws IOException if an I/O error occurs during initialization
*/ */
public Session(TransportLayer transport, EventBus eventBus) throws IOException { public Session(
TransportLayer transport,
EventBus eventBus,
CommandParserDispatcher dispatcher,
CommandRouter router) {
this.id = new SessionId(); this.id = new SessionId();
this.thread = new Thread(this, "session-" + this.id.value()); this.lastActivity = Instant.now();
this.transport = transport; this.transport = transport;
this.running = true; this.dispatcher = dispatcher;
this.eventBus = eventBus; this.router = router;
this.responseQueue = new ArrayBlockingQueue<>(RESPOND_QUEUE_SIZE);
this.logger = LogManager.getLogger(Session.class.toString() + id.value());
this.logger.info("Created new session");
} }
/** /**
@@ -47,39 +47,48 @@ public class Session implements Runnable {
return this.id; return this.id;
} }
/** Starts the session thread. */ /**
public void start() { * Gets the timestamp of the last inbound activity for this session.
thread.start(); *
* @return an {@link Instant} representing the time of the last inbound activity
*/
public Instant getLastInboundActivity() {
return lastActivity;
}
/** Updates the timestamp of the last inbound activity for this session. */
public void updateLastInboundActivity() {
this.lastActivity = Instant.now();
} }
/** /**
* Closes the session and its transport. * Returns the TransportLayer of this session
* *
* @throws IOException if an I/O error occurs * @return the transport layer of the session
*/ */
public void close() throws IOException { public TransportLayer getTransport() {
transport.close(); return transport;
this.running = false;
} }
/** Runs the session loop, reading from the transport. */ /**
@Override * Returns the BlockingQueue of this session
public void run() { *
while (running) { * @return the queue containing outgoing responses
try { */
RawPacket rawPacket = transport.read(); public BlockingQueue<PrimitiveResponse> getResponseQueue() {
logger.debug("Recieved: {}", rawPacket); return responseQueue;
}
PrimitiveRequest primitiveRequest = ProtocolParser.parse(rawPacket); /**
logger.debug("Parsed request to {}", primitiveRequest); * Returns the CommandParserDispatcher of this session
} catch (EOFException e) { *
logger.info("Client disconnected"); * @return the dispatcher to dispatch PrimitiveRequests to for parsing
eventBus.publish(new DisconnectEvent(id)); */
break; public CommandParserDispatcher getDispatcher() {
} catch (IOException e) { return dispatcher;
e.printStackTrace(); }
break;
} 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) {}
@@ -14,7 +14,7 @@ public class SessionId {
/** /**
* Creates a new SessionId with the specified UUID. * Creates a new SessionId with the specified UUID.
* *
* @param UUID to use for this SessionId * @param value UUID to use for this SessionId
*/ */
public SessionId(UUID value) { public SessionId(UUID value) {
this.value = value; this.value = value;
@@ -1,44 +1,99 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.sessions; package ch.unibas.dmi.dbis.cs108.casono.server.network.sessions;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandRouter;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.CommandParserDispatcher;
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.transport.TransportLayer;
import java.io.IOException;
import java.util.Collection;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; 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. */ /** Manages active sessions in the server. */
public class SessionManager { public class SessionManager {
private Map<SessionId, Session> sessions; private Map<SessionId, SessionHandle> sessions;
private final EventBus eventBus;
private final Logger logger;
private final CommandParserDispatcher dispatcher;
private final CommandRouter router;
/** Constructs a new SessionManager. */ /** Constructs a new SessionManager. */
public SessionManager() { public SessionManager(
EventBus eventBus, CommandParserDispatcher dispatcher, CommandRouter router) {
this.sessions = new ConcurrentHashMap<>(); this.sessions = new ConcurrentHashMap<>();
this.eventBus = eventBus;
this.logger = LogManager.getLogger(SessionManager.class);
this.dispatcher = dispatcher;
this.router = router;
} }
/** /**
* Adds a session to the manager. * Create new Session from provided transport.
* *
* @param session the session to add * <p>Will create both worker threads and start them.
*
* @param transport to create session from
* @return newly created session
*/ */
public void addSession(Session session) { public Session create(TransportLayer transport) {
sessions.put(session.getId(), session); Session session = new Session(transport, eventBus, dispatcher, router);
SessionReader reader = new SessionReader(session, eventBus);
SessionWriter writer = new SessionWriter(session);
Thread readerThread =
Thread.ofVirtual()
.name("session-" + session.getId().value() + "-reader")
.unstarted(reader);
Thread writerThread =
Thread.ofVirtual()
.name("session-" + session.getId().value() + "-writer")
.unstarted(writer);
sessions.put(session.getId(), new SessionHandle(session, readerThread, writerThread));
readerThread.start();
writerThread.start();
return session;
} }
/** /**
* Removes a session by its ID. * Disconnect specified client
* *
* @param id the ID of the session to remove * <p>WARNING: Client will be uninformed about disconnect. Use with caution.
* @return the removed session, or null if not found *
* @param id of the client to disconnect
*/ */
public Session removeSession(SessionId id) { public void disconnect(SessionId id) {
return sessions.remove(id); SessionHandle handle = sessions.remove(id);
if (handle == null) {
logger.warn(
"Requested to disconnect client with id {}. Failed as client is not found",
id.value());
return;
}
logger.debug("Disconnecting session {}", id.value());
handle.reader().interrupt();
handle.writer().interrupt();
try {
handle.session().getTransport().close();
} catch (IOException e) {
logger.error("Unexpected exception while closing transport", e);
}
} }
/** /**
* Removes the specified session. * Handler for the DisconnectEvent
* *
* @param session the session to remove * @param event the DisconnectEvent to handle
* @return the removed session, or null if not found
*/ */
public Session removeSession(Session session) { public void onDisconnect(DisconnectEvent event) {
return sessions.remove(session.getId()); logger.debug("Recieved DisconnectEvent event for session {}", event.sessionId().value());
disconnect(event.sessionId());
} }
/** /**
@@ -48,6 +103,16 @@ public class SessionManager {
* @return the session with the specified ID, or null if not found * @return the session with the specified ID, or null if not found
*/ */
public Session getSessionById(SessionId id) { public Session getSessionById(SessionId id) {
return sessions.get(id); SessionHandle handle = sessions.get(id);
if (handle == null) {
return null;
}
return handle.session();
}
public Collection<Session> getAllSessions() {
return sessions.values().stream().map(SessionHandle::session).collect(Collectors.toList());
} }
} }
@@ -0,0 +1,124 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.sessions;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandRouter;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.CommandParserDispatcher;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.UnknownCommandException;
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.protocol.parser.ProtocolParser;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.parser.ProtocolParserException;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.parser.tokenizer.TokenizerException;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.PrimitiveRequest;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RawRequest;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.Request;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.ErrorResponse;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.PrimitiveResponse;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatchException;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseEncoder;
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.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;
RawRequest rawRequest = null;
RequestContext requestContext = null;
try {
// Step 1: Read from transport
rawPacket = transport.read();
session.updateLastInboundActivity();
logger.debug("Recieved: {}", rawPacket);
requestContext = new RequestContext(session.getId(), rawPacket.requestId());
// Step 2: Syntax validation and conversion into transport object
rawRequest = ProtocolParser.parse(rawPacket.payload());
logger.debug("Parsed request to {}", rawRequest);
PrimitiveRequest primitiveRequest =
new PrimitiveRequest(
requestContext, rawRequest.command(), rawRequest.parameters());
logger.debug("Converted to {}", primitiveRequest);
// Step 3: Parse into Request and execute Request
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);
sendErrorResponse(
new ErrorResponse(
requestContext,
"PARSING_ERROR",
"Error occured during parsing. Likely due to malformed payload."));
} catch (UnknownCommandException e) {
logger.error("Recieved unknown command '{}' from client", rawRequest.command(), e);
sendErrorResponse(
new ErrorResponse(
requestContext,
"UNKNOWN_COMMAND",
"This command is unknown to the server."));
} catch (ResponseDispatchException e) {
logger.error(
"Unexpected ResponseDispatchException exception while dispatching request",
e);
} catch (IOException e) {
logger.error("Unexpected IO exception while reading from transport", e);
} catch (RuntimeException e) {
logger.error("Unexpected RuntimeException occured", e);
sendErrorResponse(
new ErrorResponse(
requestContext,
"INTERNAL_ERROR",
"Unexpected internal server error occured."));
}
}
}
/**
* Helperfunction to send ErrorResponse to client
*
* @param response to send to the client
*/
private void sendErrorResponse(ErrorResponse response) {
PrimitiveResponse primitiveResponse = ResponseEncoder.encode(response);
try {
session.getResponseQueue().put(primitiveResponse);
} catch (InterruptedException e) {
logger.error("Got interrupted while sending ErrorResponse to client.");
}
}
}
@@ -0,0 +1,42 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.sessions;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.PrimitiveResponse;
import ch.unibas.dmi.dbis.cs108.casono.server.network.transport.RawPacket;
import ch.unibas.dmi.dbis.cs108.casono.server.network.transport.TransportLayer;
import java.io.IOException;
import java.util.concurrent.BlockingQueue;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class SessionWriter implements Runnable {
private final TransportLayer transport;
private final BlockingQueue<PrimitiveResponse> queue;
private final Logger logger;
public SessionWriter(Session session) {
this.transport = session.getTransport();
this.queue = session.getResponseQueue();
this.logger =
LogManager.getLogger(
SessionReader.class.toString() + "-" + session.getId().value());
}
public void run() {
while (!Thread.currentThread().isInterrupted()) {
RawPacket packet = null;
try {
PrimitiveResponse response = queue.take();
packet = new RawPacket(response.requestId(), response.payload());
transport.write(packet);
} catch (IOException e) {
logger.error(
"Unexpected exception while writing to transport. RawPacket: {}",
packet,
e);
} catch (InterruptedException e) {
logger.warn("Thread got interrupted", e);
break;
}
}
}
}
@@ -1,7 +1,7 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.transport; package ch.unibas.dmi.dbis.cs108.casono.server.network.transport;
/** /**
* Transport object, created by the transport layer to store raw data about the request * Transport object, created by the transport layer to store raw data about the request prior to any
* prior to any processing * processing
*/ */
public record RawPacket(int requestId, String payload) {} public record RawPacket(int requestId, String payload) {}
@@ -50,8 +50,8 @@ public class TcpTransport implements TransportLayer {
int requestId = data.requestId(); int requestId = data.requestId();
byte[] rawPayload = data.payload().getBytes(StandardCharsets.UTF_8); byte[] rawPayload = data.payload().getBytes(StandardCharsets.UTF_8);
out.writeInt(requestId);
out.writeInt(rawPayload.length); out.writeInt(rawPayload.length);
out.writeInt(requestId);
out.write(rawPayload); out.write(rawPayload);
out.flush(); out.flush();
} }
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 902 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 701 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 651 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 665 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 659 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 660 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 657 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 659 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 661 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 658 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 663 KiB

Some files were not shown because too many files have changed in this diff Show More