Feat/Game Engine #233

Merged
j.kropff merged 18 commits from feat/game-engine into main 2026-04-05 11:26:12 +02:00
186 changed files with 968 additions and 131 deletions
Showing only changes of commit d4e69d26d7 - Show all commits
@@ -0,0 +1,31 @@
# Commands
Commands are the primary extension point of the server.
Every client-facing operation e.g. checking a username, sending a chat message, joining a lobby is implemented as a command.
Each command consists of four classes: a **Parser**, a **Request**, a **Handler**, and a **Response**.
The infrastructure for routing and dispatching commands lives in the `network/` layer and is intentionally kept generic.
The concrete implementations for each command live in `app/commands/` and are wired together at startup in `ServerApp`.
![Overview of all components directly related to executing requests](/documents/images/docs/networking/commands/overview.svg)
## Contents
### Guides
- [Implementing a Command](./guide-on-implementing-a-command.md) -
Step-by-step walkthrough for adding a new command to the server, including registration and common pitfalls.
### Reference
- [Command Infrastructure](./commands-deep-dive.md) -
Technical deep-dive into the `CommandParser`, `CommandParserDispatcher`, `CommandHandler`, `CommandRouter`, `Request`, and the response hierarchy.
## Key Concepts
**Each command is self-contained.**
A command's Parser, Request, Handler, and Response all live in the same package under `app/commands/<name>/`.
This keeps related code co-located and makes it easy to reason about a single command without navigating across multiple directories.
**The `network/` layer knows nothing about specific commands.**
`CommandParser` and `CommandHandler` are generic interfaces. The `CommandParserDispatcher` and `CommandRouter` operate on those interfaces.
Adding a new command **never** requires modifying infrastructure code.
**Registration happens at the composition root.**
All commands are wired in `ServerApp` by calling `parserDispatcher.register(...)` and `commandRouter.register(...)`.
This keeps the wiring explicit and compiler-checked.
@@ -0,0 +1,108 @@
# Command Infrastructure
This document describes the generic command infrastructure that lives in the `network/` layer.
It covers the parsing pipeline, the routing pipeline, and the base types that every command builds on.
The infrastructure is intentionally project-agnostic: It has no knowledge of specific commands and is never modified when a new command is added.
## Overview
An incoming request travels through two sequential pipelines: **parsing** and **execution**.
The parsing pipeline converts a stringly-typed `PrimitiveRequest` into a strongly-typed `Request` subclass.
The execution pipeline routes that typed request to the correct handler, which produces a `Response`.
![Sequence diagram of all involved components to process and respond to an incoming request](/documents/images/docs/networking/commands/sequence_diagram.svg)
## Parsing Pipeline
### `CommandParser<T extends Request>`
![Class diagram of the CommandParser](/documents/images/docs/networking/commands/command_parser.svg)
The `CommandParser` is a single-method interface responsible for converting a `PrimitiveRequest` into a concrete, typed `Request` subclass.
Implementations live in `app/commands/<name>/` and are registered by name in `CommandParserDispatcher`.
The parser is the correct place to validate and extract parameters.
If a required parameter is absent, `RequestParameterAccessor.require(...)` throws an `MissingParameterException`, which the `SessionReader` catches and converts into a `MISSING_PARAMETER` error response for the client.
Parsers **do not** perform any domain logic. Their only job is extraction and type conversion.
### `CommandParserDispatcher`
![Class diagram of CommandParserDispatcher](/documents/images/docs/networking/commands/command_parser_dispatcher.svg)
The `CommandParserDispatcher` holds a map from command name strings (e.g. `"PING"`) to their corresponding `CommandParser`.
When the `SessionReader` receives a `PrimitiveRequest`, it calls `dispatcher.parse(...)`, which looks up the parser by the request's command string and delegates parsing.
If no parser is registered for the command name, `parse(...)` throws an `UnknownCommandException`, which `SessionReader` catches and converts into an `UNKNOWN_COMMAND` error response for the client.
### `RequestParameterAccessor`
![Class diagram of RequestParameterAccessor](/documents/images/docs/networking/commands/request_parameter_accessor.svg)
The `RequestParameterAccessor` is a helper provided to parsers for reading typed parameter values from a `PrimitiveRequest`.
It indexes the parameter list by key on construction for O(1) lookups.
```java
// Require a parameter — throws MissingParameterException if absent
String username = accessor.require("USERNAME");
// Require and parse — throws ParameterParseException if conversion fails
int count = accessor.require("COUNT", Integer::parseInt);
// Optional with a default
String mode = accessor.optional("MODE", "default");
```
The `ThrowingParser<T>` functional interface accepted by the typed overloads allows any checked or unchecked exception to propagate from the conversion function.
The `RequestParameterAccessor` wraps it in a `ParameterParseException`.
## Execution Pipeline
### `Request`
![Class diagram of Request](/documents/images/docs/networking/commands/request.svg)
The `Request` is the abstract base class for all typed command requests. It carries a `RequestContext`, an immutable record containing the originating `SessionId` and the numeric `requestId`.
Both of which are later used by the handler to direct the response to the correct session.
Concrete subclasses add command-specific fields, all set via constructor. Requests are immutable value objects. They carry data, not behaviour.
### `CommandHandler<T extends Request>`
![Class diagram of CommandHandler](/documents/images/docs/networking/commands/command_handler.svg)
The `CommandHandler` is a single-method interface responsible for executing a typed request.
The handler contains the domain logic: reading from registries and managers, modifying state, and dispatching a response via the `ResponseDispatcher`.
Handlers receive their dependencies (the `ResponseDispatcher`, registries and managers etc.) through constructor injection.
This keeps them fully testable without the network layer.
### `CommandRouter`
![Class diagram of CommandRouter](/documents/images/docs/networking/commands/command_router.svg)
The `CommandRouter` maps `Request` subclasses to their handlers using the request's runtime class as the key.
The type safety of `register(...)` ensures that a handler can only be registered for the exact type it is parameterised on.
The unchecked cast in `execute(...)` is therefore safe by construction and is documented with a `@SuppressWarnings` comment in the source.
If no handler is registered for the given request type, `execute(...)` throws `UnknownRequestException`.
Unlike `UnknownCommandException` (which covers unknown command strings), this exception indicates a programming error i.e. a parser was registered without a corresponding handler.
## Response Types
![Class diagram of the response interface and built-in implementations](/documents/images/docs/networking/commands/response_types.svg)
The `Response` is the abstract base for all server responses. Its two concrete branches are `SuccessResponse` (prefix `+OK`) and `ErrorResponse` (prefix `-ERR`).
Command-specific responses extend `SuccessResponse` and populate the body using the `ResponseBodyBuilder`.
`OkResponse` is a pre-built convenience subclass of `SuccessResponse` with an empty body, used for commands that need only acknowledge success without returning data (e.g. `PING`).
The body is built with a fluent `ResponseBodyBuilder`:
```java
// Simple key/value parameters
new ResponseBodyBuilder()
.param("STATUS", UsernameAvailability.FREE)
.build();
// Nested block
new ResponseBodyBuilder()
.block("USER", b -> b
.param("ID", user.getId().value())
.param("NAME", user.getName()))
.build();
```
`ResponseEncoder` serialises the body into the wire format (tab-indented, `END`-terminated blocks) and wraps it in a `PrimitiveResponse`.
`ResponseDispatcher` then enqueues this into the target session's bounded response queue.
@@ -0,0 +1,215 @@
# Implementing a Command
This guide walks through the full process of adding a new command to the server. By the end you
will have a working command with a Parser, Request, Handler, and Response, all correctly wired
into the server.
For background on how these components interact at a technical level, see [Command Infrastructure](../reference/command-infrastructure.md).
## Before You Start
A command consists of exactly four classes, all placed in the same package:
```
app/commands/<your_command>/
YourCommandParser.java
YourCommandRequest.java
YourCommandHandler.java
YourCommandResponse.java ← omit this if OkResponse is sufficient
```
Name the package after the command in `snake_case`, matching the wire-protocol name (e.g. `join_lobby` for the `JOIN_LOBBY` command).
Name the classes in `UpperCamelCase` with the command name as prefix.
## Step 1 — Define the Request
The `Request` subclass is a typed, immutable value object holding everything the handler needs.
Define it first, because both the Parser and the Handler depend on it.
```java
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.greet;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.Request;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
public class GreetRequest extends Request {
private final String name;
public GreetRequest(RequestContext context, String name) {
super(context);
this.name = name;
}
public String getName() {
return name;
}
}
```
**Rules for the Request class:**
- Always pass `context` directly to `super(context)`. Never store it in a separate field.
- Fields must be `private final`. Set them only through the constructor.
- Provide a getter for every field. No setters.
- No logic. The request is data, not behaviour.
## Step 2 — Implement the Parser
The Parser extracts parameters from the `PrimitiveRequest` and constructs the typed Request.
```java
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.greet;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.CommandParser;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.PrimitiveRequest;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.accessor.RequestParameterAccessor;
public class GreetParser implements CommandParser<GreetRequest> {
@Override
public GreetRequest parse(PrimitiveRequest primitiveRequest) {
RequestParameterAccessor accessor = new RequestParameterAccessor(primitiveRequest.parameters());
String name = accessor.require("NAME");
return new GreetRequest(primitiveRequest.context(), name);
}
}
```
**Rules for the Parser class:**
- Always create a `RequestParameterAccessor` from `primitiveRequest.parameters()`.
- Use `accessor.require(key)` for mandatory parameters. It throws `MissingParameterException`
automatically — do not write your own null checks.
- Use `accessor.optional(key, defaultValue)` for optional parameters.
- Use the typed overloads (e.g. `accessor.require("COUNT", Integer::parseInt)`) for non-string
parameters. The resulting `ParameterParseException` is handled by `SessionReader`.
- Always pass `primitiveRequest.context()` as the first argument to the Request constructor.
- No domain logic.
### Choosing between `require` and `optional`
| Use | When |
|-|-|
| `accessor.require(key)` | The command cannot function without this parameter |
| `accessor.require(key, parser)` | Same, but the value must be converted to a specific type |
| `accessor.optional(key, default)` | The parameter has a sensible default when omitted |
| `accessor.optional(key, default, parser)` | Optional + type conversion |
## Step 3 — Implement the Response
If your command returns data, create a dedicated Response class. If it only needs to signal
success, use `OkResponse` directly in the handler and skip this step.
```java
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.greet;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.SuccessResponse;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder.ResponseBodyBuilder;
public class GreetResponse extends SuccessResponse {
public GreetResponse(RequestContext context, String greeting) {
super(context, new ResponseBodyBuilder()
.param("GREETING", greeting)
.build());
}
}
```
**Rules for the Response class:**
- Extend `SuccessResponse` for successful outcomes, not `Response` directly.
- Build the body inline in the `super(...)` call using `ResponseBodyBuilder`. Do not store
the builder or body separately.
- Use `ResponseBodyBuilder.block(tag, consumer)` to add nested structures when the response
carries a list or a complex sub-object.
- Parameter keys must be `UPPER_SNAKE_CASE` to match the wire protocol convention.
- If you need to return an error (e.g. lobby not found), do not throw — dispatch an
`ErrorResponse` from the handler instead (see Step 4).
## Step 4 — Implement the Handler
The Handler contains the domain logic. It reads from registries, modifies state if needed, and
always dispatches exactly one response.
```java
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.greet;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatcher;
public class GreetHandler implements CommandHandler<GreetRequest> {
public final ResponseDispatcher responseDispatcher;
public GreetHandler(ResponseDispatcher responseDispatcher) {
this.responseDispatcher = responseDispatcher;
}
@Override
public void execute(GreetRequest request) {
GreetResponse response = new GreetResponse(request.getContext(), "Hello " + request.getName() + ", nice to meet you");
responseDispatcher.dispatch(response);
}
}
```
**Rules for the Handler class:**
- Declare all dependencies as `private final` fields, injected through the constructor.
- Always dispatch exactly one response per execution path. Every branch must end with a
`responseDispatcher.dispatch(...)` call.
- Use `ErrorResponse` for domain-level failures (e.g. entity not found, precondition not met).
Do not throw exceptions for expected failure cases.
- Use `request.getContext()` when constructing any Response — never construct a `RequestContext`
yourself.
- Do not call `responseDispatcher.dispatch(...)` more than once in a single `execute` invocation.
## Step 5 — Register the Command
Open `ServerApp.registerCommands(...)` and add two lines: one to register the parser and one to
register the handler.
```java
private static void registerCommands(
CommandParserDispatcher parserDispatcher,
CommandRouter commandRouter,
ResponseDispatcher responseDispatcher /* add new dependencies here */) {
// ... existing commands ...
parserDispatcher.register("GREET", new GreetParser());
commandRouter.register(GreetRequest.class,
new GreetHandler(responseDispatcher));
}
```
The string passed to `parserDispatcher.register(...)` must exactly match the command name as
sent by the client on the wire, in `UPPER_SNAKE_CASE`.
> **Important:** Always register both the parser **and** the handler. Registering a parser
> without a handler will result in an `UnknownRequestException` at runtime when the command is
> received — the parser will succeed, but the router will find no handler for the resulting
> request type.
## Common Mistakes
**Forgetting to pass `context` through to the Response.** The `context` is how the response
finds its way back to the right client. Dropping it means the response is dispatched to the
wrong session or causes a NullPointerException.
**Putting domain logic in the Parser.** Parsers run before the request is validated as
meaningful. A parser that calls a registry or modifies state creates hidden coupling between the
parsing and execution phases and makes the parser difficult to test.
**Dispatching a response before an early return.** A common mistake is to dispatch an error
and then fall through to dispatch a success response as well. Always `return` immediately after
dispatching an error.
**Using a raw string for the error code.** Error codes should be `UPPER_SNAKE_CASE` constant
strings that the client can match against programmatically. Avoid spaces or punctuation.
@@ -0,0 +1,8 @@
@startuml
skinparam backgroundColor transparent
interface CommandHandler<T extends Request> {
+ execute(request: T): void
}
@enduml
@@ -0,0 +1 @@
<?plantuml 1.2026.2?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" contentStyleType="text/css" data-diagram-type="CLASS" height="85px" preserveAspectRatio="none" style="width:287px;height:85px;" version="1.1" viewBox="0 0 287 85" width="287px" zoomAndPan="magnify"><defs/><g><!--class CommandHandler--><g class="entity" data-qualified-name="CommandHandler" data-source-line="3" id="ent0002"><rect fill="#F1F1F1" height="64.4883" rx="2.5" ry="2.5" style="stroke:#181818;stroke-width:0.5;" width="266.835" x="7" y="7"/><ellipse cx="22" cy="23" fill="#B4A7E5" rx="11" ry="11" style="stroke:#181818;stroke-width:1;"/><path d="M18.4277,19.2651 L18.4277,17.1069 L25.8071,17.1069 L25.8071,19.2651 L23.3418,19.2651 L23.3418,27.3418 L25.8071,27.3418 L25.8071,29.5 L18.4277,29.5 L18.4277,27.3418 L20.8931,27.3418 L20.8931,19.2651 Z " fill="#000000"/><text fill="#000000" font-family="sans-serif" font-size="14" font-style="italic" lengthAdjust="spacing" textLength="122.7529" x="36" y="28.291">CommandHandler</text><rect fill="#FFFFFF" height="16.1328" style="stroke:#181818;stroke-width:1;stroke-dasharray:2,2;" width="110.082" x="166.7529" y="4"/><text fill="#000000" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="108.082" x="167.7529" y="16.6016">T extends Request</text><line style="stroke:#181818;stroke-width:0.5;" x1="8" x2="272.835" y1="39" y2="39"/><line style="stroke:#181818;stroke-width:0.5;" x1="8" x2="272.835" y1="47" y2="47"/><g data-visibility-modifier="PUBLIC_METHOD"><ellipse cx="18" cy="60.7441" fill="#84BE84" rx="3" ry="3" style="stroke:#038048;stroke-width:1;"/></g><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="168.6631" x="27" y="64.5352">execute(request: T): void</text></g><?plantuml-src 9Sb12W8n34RXVKwHfU8D1d4p6zSo5uZjhvIP9fgc8eXtj-23j_hQMlGXBfMlajQxMzSyssREuQ9j43I8YWRy9WayMwsY-JpUuCU5yGveRdp1iwF5YJ_4eyC0f1xO-HycNlOJlwna-j8F?></g></svg>
@@ -0,0 +1,8 @@
@startuml
skinparam backgroundColor transparent
interface CommandParser<T extends Request> {
+ parse(primitiveRequest: PrimitiveRequest): T
}
@enduml
@@ -0,0 +1 @@
<?plantuml 1.2026.2?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" contentStyleType="text/css" data-diagram-type="CLASS" height="85px" preserveAspectRatio="none" style="width:347px;height:85px;" version="1.1" viewBox="0 0 347 85" width="347px" zoomAndPan="magnify"><defs/><g><!--class CommandParser--><g class="entity" data-qualified-name="CommandParser" data-source-line="3" id="ent0002"><rect fill="#F1F1F1" height="64.4883" rx="2.5" ry="2.5" style="stroke:#181818;stroke-width:0.5;" width="326.5283" x="7" y="7"/><ellipse cx="54.2124" cy="23" fill="#B4A7E5" rx="11" ry="11" style="stroke:#181818;stroke-width:1;"/><path d="M50.6401,19.2651 L50.6401,17.1069 L58.0195,17.1069 L58.0195,19.2651 L55.5542,19.2651 L55.5542,27.3418 L58.0195,27.3418 L58.0195,29.5 L50.6401,29.5 L50.6401,27.3418 L53.1055,27.3418 L53.1055,19.2651 Z " fill="#000000"/><text fill="#000000" font-family="sans-serif" font-size="14" font-style="italic" lengthAdjust="spacing" textLength="111.5215" x="74.7124" y="28.291">CommandParser</text><rect fill="#FFFFFF" height="16.1328" style="stroke:#181818;stroke-width:1;stroke-dasharray:2,2;" width="110.082" x="226.4463" y="4"/><text fill="#000000" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="108.082" x="227.4463" y="16.6016">T extends Request</text><line style="stroke:#181818;stroke-width:0.5;" x1="8" x2="332.5283" y1="39" y2="39"/><line style="stroke:#181818;stroke-width:0.5;" x1="8" x2="332.5283" y1="47" y2="47"/><g data-visibility-modifier="PUBLIC_METHOD"><ellipse cx="18" cy="60.7441" fill="#84BE84" rx="3" ry="3" style="stroke:#038048;stroke-width:1;"/></g><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="300.5283" x="27" y="64.5352">parse(primitiveRequest: PrimitiveRequest): T</text></g><?plantuml-src NOf12i9034NtESLVAjwWY9jUe4Wl43jHXZgPCKcB8DxT2gvyo_UUpraR6nTSU9flLXTDVRrNGnYhRqaqYBA6s9KdGLzBOKq3cuiTHyWpH9FZB8z5F4vu4JOEQDzZrooN77cLN-ym_9bzXv7Uz040?></g></svg>
@@ -0,0 +1,11 @@
@startuml
skinparam backgroundColor transparent
class CommandParserDispatcher {
- parsers: Map<String, CommandParser>
+ register(command: String, parser: CommandParser): void
+ parse(primitiveRequest: PrimitiveRequest): Request
}
@enduml
@@ -0,0 +1 @@
<?plantuml 1.2026.2?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" contentStyleType="text/css" data-diagram-type="CLASS" height="118px" preserveAspectRatio="none" style="width:434px;height:118px;" version="1.1" viewBox="0 0 434 118" width="434px" zoomAndPan="magnify"><defs/><g><!--class CommandParserDispatcher--><g class="entity" data-qualified-name="CommandParserDispatcher" data-source-line="3" id="ent0002"><rect fill="#F1F1F1" height="97.4648" rx="2.5" ry="2.5" style="stroke:#181818;stroke-width:0.5;" width="413.0234" x="7" y="7"/><ellipse cx="117.0825" cy="23" fill="#ADD1B2" rx="11" ry="11" style="stroke:#181818;stroke-width:1;"/><path d="M119.5557,29.1431 Q118.9746,29.4419 118.3354,29.5913 Q117.6963,29.7407 116.9907,29.7407 Q114.4839,29.7407 113.1641,28.0889 Q111.8442,26.437 111.8442,23.3159 Q111.8442,20.1865 113.1641,18.5347 Q114.4839,16.8828 116.9907,16.8828 Q117.6963,16.8828 118.3438,17.0322 Q118.9912,17.1816 119.5557,17.4805 L119.5557,20.2031 Q118.9248,19.6221 118.3313,19.3523 Q117.7378,19.0825 117.1069,19.0825 Q115.7622,19.0825 115.0774,20.1492 Q114.3926,21.2158 114.3926,23.3159 Q114.3926,25.4077 115.0774,26.4744 Q115.7622,27.541 117.1069,27.541 Q117.7378,27.541 118.3313,27.2712 Q118.9248,27.0015 119.5557,26.4204 Z " fill="#000000"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="184.3584" x="137.5825" y="28.291">CommandParserDispatcher</text><line style="stroke:#181818;stroke-width:0.5;" x1="8" x2="419.0234" y1="39" y2="39"/><g data-visibility-modifier="PRIVATE_FIELD"><rect fill="none" height="6" style="stroke:#C82930;stroke-width:1;" width="6" x="15" y="49.7441"/></g><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="270.1426" x="27" y="56.5352">parsers: Map&lt;String, CommandParser&gt;</text><line style="stroke:#181818;stroke-width:0.5;" x1="8" x2="419.0234" y1="63.4883" y2="63.4883"/><g data-visibility-modifier="PUBLIC_METHOD"><ellipse cx="18" cy="77.2324" fill="#84BE84" rx="3" ry="3" style="stroke:#038048;stroke-width:1;"/></g><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="387.0234" x="27" y="81.0234">register(command: String, parser: CommandParser): void</text><g data-visibility-modifier="PUBLIC_METHOD"><ellipse cx="18" cy="93.7207" fill="#84BE84" rx="3" ry="3" style="stroke:#038048;stroke-width:1;"/></g><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="346.001" x="27" y="97.5117">parse(primitiveRequest: PrimitiveRequest): Request</text></g><?plantuml-src NOmx3i8m343tdC9Z4N212B6K5Qc24vZKAbQR3tRQ1N5tehOCnPFbzvwrvv1GqCC3NTz974DRno4APC6W1L78nhW1LQ6EtcDe6nGbkN2XsJr9u6sWp17I3DJ25TFfdeL3TzX6PpEhUn3gM3D9vHPiuUSlJ-ossrcO8hThF2jL4lQSUQ8RlKRIRA7vkvHgtSp7V040?></g></svg>
@@ -0,0 +1,10 @@
@startuml
skinparam backgroundColor transparent
class CommandRouter {
- handlers: Map<Class<? extends Request>, CommandHandler<?>>
+ register(requestClass: Class<T>, handler: CommandHandler<T>): void
+ execute(request: Request): void
}
@enduml
@@ -0,0 +1 @@
<?plantuml 1.2026.2?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" contentStyleType="text/css" data-diagram-type="CLASS" height="118px" preserveAspectRatio="none" style="width:531px;height:118px;" version="1.1" viewBox="0 0 531 118" width="531px" zoomAndPan="magnify"><defs/><g><!--class CommandRouter--><g class="entity" data-qualified-name="CommandRouter" data-source-line="3" id="ent0002"><rect fill="#F1F1F1" height="97.4648" rx="2.5" ry="2.5" style="stroke:#181818;stroke-width:0.5;" width="510.5996" x="7" y="7"/><ellipse cx="200.7681" cy="23" fill="#ADD1B2" rx="11" ry="11" style="stroke:#181818;stroke-width:1;"/><path d="M203.2412,29.1431 Q202.6602,29.4419 202.021,29.5913 Q201.3818,29.7407 200.6763,29.7407 Q198.1694,29.7407 196.8496,28.0889 Q195.5298,26.437 195.5298,23.3159 Q195.5298,20.1865 196.8496,18.5347 Q198.1694,16.8828 200.6763,16.8828 Q201.3818,16.8828 202.0293,17.0322 Q202.6768,17.1816 203.2412,17.4805 L203.2412,20.2031 Q202.6104,19.6221 202.0168,19.3523 Q201.4233,19.0825 200.7925,19.0825 Q199.4478,19.0825 198.7629,20.1492 Q198.0781,21.2158 198.0781,23.3159 Q198.0781,25.4077 198.7629,26.4744 Q199.4478,27.541 200.7925,27.541 Q201.4233,27.541 202.0168,27.2712 Q202.6104,27.0015 203.2412,26.4204 Z " fill="#000000"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="114.5635" x="221.2681" y="28.291">CommandRouter</text><line style="stroke:#181818;stroke-width:0.5;" x1="8" x2="516.5996" y1="39" y2="39"/><g data-visibility-modifier="PRIVATE_FIELD"><rect fill="none" height="6" style="stroke:#C82930;stroke-width:1;" width="6" x="15" y="49.7441"/></g><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="459.2725" x="27" y="56.5352">handlers: Map&lt;Class&lt;? extends Request&gt;, CommandHandler&lt;?&gt;&gt;</text><line style="stroke:#181818;stroke-width:0.5;" x1="8" x2="516.5996" y1="63.4883" y2="63.4883"/><g data-visibility-modifier="PUBLIC_METHOD"><ellipse cx="18" cy="77.2324" fill="#84BE84" rx="3" ry="3" style="stroke:#038048;stroke-width:1;"/></g><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="484.5996" x="27" y="81.0234">register(requestClass: Class&lt;T&gt;, handler: CommandHandler&lt;T&gt;): void</text><g data-visibility-modifier="PUBLIC_METHOD"><ellipse cx="18" cy="93.7207" fill="#84BE84" rx="3" ry="3" style="stroke:#038048;stroke-width:1;"/></g><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="214.1357" x="27" y="97.5117">execute(request: Request): void</text></g><?plantuml-src NOun3i8m34NtdC8Z2BX0LAL35nQMgXSmZLMgjacn4rG9SNT2sonujDz_tpBqRaR62UxO3Xtxw6pbHyyG69sa4xcWL3kY25H-cj3PsiT036y5QIxmIAkHM3JSS2wg7rcKG4iWPmLgUaQIOCuvVTtmeZHc5Po0gUib6G-yiQj2msPgKd9lqF-AnXmrl7nlTn4jrARNiaVdyanzr1S0?></g></svg>
@@ -0,0 +1,33 @@
@startuml
skinparam backgroundColor transparent
package "network/ (infrastructure)" {
class PrimitiveRequest <<record>>
interface CommandParser<T extends Request>
interface CommandHandler<T extends Request>
class CommandParserDispatcher
class CommandRouter
abstract class Request
abstract class Response
PrimitiveRequest --> CommandParserDispatcher : routes
PrimitiveRequest ..> CommandParser : parsed by
CommandParserDispatcher --> CommandParser : routes to
CommandParser --> Request : parses to
Request --> CommandRouter : routes
CommandRouter --> CommandHandler : routes to
Request ..> CommandHandler : executed by
CommandHandler --> Response : creates
}
package "app/commands/<name>/ (per command)" {
class ExampleParser implements CommandParser
class ExampleRequest extends Request
class ExampleHandler implements CommandHandler
class ExampleResponse extends Response
ExampleParser --> ExampleRequest : parses to
ExampleRequest --> ExampleHandler : executed by
ExampleHandler --> ExampleResponse : produces
}
@enduml
File diff suppressed because one or more lines are too long
@@ -0,0 +1,11 @@
@startuml
skinparam backgroundColor transparent
abstract class Request {
# context: RequestContext
+ getContext(): RequestContext
+ getSessionId(): SessionId
+ getRequestId(): int
}
@enduml
@@ -0,0 +1 @@
<?plantuml 1.2026.2?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" contentStyleType="text/css" data-diagram-type="CLASS" height="134px" preserveAspectRatio="none" style="width:248px;height:134px;" version="1.1" viewBox="0 0 248 134" width="248px" zoomAndPan="magnify"><defs/><g><!--class Request--><g class="entity" data-qualified-name="Request" data-source-line="3" id="ent0002"><rect fill="#F1F1F1" height="113.9531" rx="2.5" ry="2.5" style="stroke:#181818;stroke-width:0.5;" width="227.7217" x="7" y="7"/><ellipse cx="89.4482" cy="23" fill="#A9DCDF" rx="11" ry="11" style="stroke:#181818;stroke-width:1;"/><path d="M89.3115,18.3481 L88.1577,23.4199 L90.4736,23.4199 Z M87.8174,16.1069 L90.814,16.1069 L94.1592,28.5 L91.7104,28.5 L90.9468,25.437 L87.668,25.437 L86.9209,28.5 L84.4722,28.5 Z " fill="#000000"/><text fill="#000000" font-family="sans-serif" font-size="14" font-style="italic" lengthAdjust="spacing" textLength="54.3252" x="109.9482" y="28.291">Request</text><line style="stroke:#181818;stroke-width:0.5;" x1="8" x2="233.7217" y1="39" y2="39"/><g data-visibility-modifier="PROTECTED_FIELD"><polygon fill="none" points="18,47.7441,22,51.7441,18,55.7441,14,51.7441" style="stroke:#B38D22;stroke-width:1;"/></g><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="168.335" x="27" y="56.5352">context: RequestContext</text><line style="stroke:#181818;stroke-width:0.5;" x1="8" x2="233.7217" y1="63.4883" y2="63.4883"/><g data-visibility-modifier="PUBLIC_METHOD"><ellipse cx="18" cy="77.2324" fill="#84BE84" rx="3" ry="3" style="stroke:#038048;stroke-width:1;"/></g><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="201.7217" x="27" y="81.0234">getContext(): RequestContext</text><g data-visibility-modifier="PUBLIC_METHOD"><ellipse cx="18" cy="93.7207" fill="#84BE84" rx="3" ry="3" style="stroke:#038048;stroke-width:1;"/></g><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="167.3164" x="27" y="97.5117">getSessionId(): SessionId</text><g data-visibility-modifier="PUBLIC_METHOD"><ellipse cx="18" cy="110.209" fill="#84BE84" rx="3" ry="3" style="stroke:#038048;stroke-width:1;"/></g><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="124.8721" x="27" y="114">getRequestId(): int</text></g><?plantuml-src AyxEp2j8B4hCLKXAJCvEByelpKjnpi_9Br8eAKhCAmPAfUQLS74b9XK3-Sab2iavYSN52a6fXQMfnIKArLmAGA2ia9oVLv9QKM85at26yC6osWhfgJ013KtSIe9JYuipy_CyKq2Au1o4F5GVH3uJwAXQBW00?></g></svg>
@@ -0,0 +1,14 @@
@startuml
skinparam backgroundColor transparent
class RequestParameterAccessor {
- index: Map<String, String>
+ RequestParameterAccessor(parameters: List<RequestParameters>)
+ require(key: String): String
+ require(key: String, parser: ThrowingParser<T>): T
+ optional(key: String, defaultValue: String): String
+ optional(key: String, defaultValue: T, parser: ThrowingParser<T>): T
}
@enduml
@@ -0,0 +1 @@
<?plantuml 1.2026.2?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" contentStyleType="text/css" data-diagram-type="CLASS" height="167px" preserveAspectRatio="none" style="width:510px;height:167px;" version="1.1" viewBox="0 0 510 167" width="510px" zoomAndPan="magnify"><defs/><g><!--class RequestParameterAccessor--><g class="entity" data-qualified-name="RequestParameterAccessor" data-source-line="3" id="ent0002"><rect fill="#F1F1F1" height="146.9297" rx="2.5" ry="2.5" style="stroke:#181818;stroke-width:0.5;" width="489.9072" x="7" y="7"/><ellipse cx="156.0576" cy="23" fill="#ADD1B2" rx="11" ry="11" style="stroke:#181818;stroke-width:1;"/><path d="M158.5308,29.1431 Q157.9497,29.4419 157.3105,29.5913 Q156.6714,29.7407 155.9658,29.7407 Q153.459,29.7407 152.1392,28.0889 Q150.8193,26.437 150.8193,23.3159 Q150.8193,20.1865 152.1392,18.5347 Q153.459,16.8828 155.9658,16.8828 Q156.6714,16.8828 157.3188,17.0322 Q157.9663,17.1816 158.5308,17.4805 L158.5308,20.2031 Q157.8999,19.6221 157.3064,19.3523 Q156.7129,19.0825 156.082,19.0825 Q154.7373,19.0825 154.0525,20.1492 Q153.3677,21.2158 153.3677,23.3159 Q153.3677,25.4077 154.0525,26.4744 Q154.7373,27.541 156.082,27.541 Q156.7129,27.541 157.3064,27.2712 Q157.8999,27.0015 158.5308,26.4204 Z " fill="#000000"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="183.292" x="176.5576" y="28.291">RequestParameterAccessor</text><line style="stroke:#181818;stroke-width:0.5;" x1="8" x2="495.9072" y1="39" y2="39"/><g data-visibility-modifier="PRIVATE_FIELD"><rect fill="none" height="6" style="stroke:#C82930;stroke-width:1;" width="6" x="15" y="49.7441"/></g><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="186.4502" x="27" y="56.5352">index: Map&lt;String, String&gt;</text><line style="stroke:#181818;stroke-width:0.5;" x1="8" x2="495.9072" y1="63.4883" y2="63.4883"/><g data-visibility-modifier="PUBLIC_METHOD"><ellipse cx="18" cy="77.2324" fill="#84BE84" rx="3" ry="3" style="stroke:#038048;stroke-width:1;"/></g><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="454.2002" x="27" y="81.0234">RequestParameterAccessor(parameters: List&lt;RequestParameters&gt;)</text><g data-visibility-modifier="PUBLIC_METHOD"><ellipse cx="18" cy="93.7207" fill="#84BE84" rx="3" ry="3" style="stroke:#038048;stroke-width:1;"/></g><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="178.6641" x="27" y="97.5117">require(key: String): String</text><g data-visibility-modifier="PUBLIC_METHOD"><ellipse cx="18" cy="110.209" fill="#84BE84" rx="3" ry="3" style="stroke:#038048;stroke-width:1;"/></g><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="345.2969" x="27" y="114">require(key: String, parser: ThrowingParser&lt;T&gt;): T</text><g data-visibility-modifier="PUBLIC_METHOD"><ellipse cx="18" cy="126.6973" fill="#84BE84" rx="3" ry="3" style="stroke:#038048;stroke-width:1;"/></g><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="328.3916" x="27" y="130.4883">optional(key: String, defaultValue: String): String</text><g data-visibility-modifier="PUBLIC_METHOD"><ellipse cx="18" cy="143.1855" fill="#84BE84" rx="3" ry="3" style="stroke:#038048;stroke-width:1;"/></g><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="463.9072" x="27" y="146.9766">optional(key: String, defaultValue: T, parser: ThrowingParser&lt;T&gt;): T</text></g><?plantuml-src ZOvD2i9038NtSueiMzGBb58Gjmf5Y_lOHXqwpjHaYefuTyVQBXIAPfE_xxr4QcqRP3p13ilwmAwrrS8Pn-0PhOGLMQzKQL04rdHkINp-uiaJpykIH09xLn1Y1jfMT4rWXKswyQpjOGhldAcEc8nQHCqmaGIMMdpwJKeMznuSDfefgkcMzFxnK8mZKmWdX3Y1uiZk4YPvrwT55jH5BtIDrywY-LHMjCVM-2sQbiPE_gCiVZtnK4y0?></g></svg>
@@ -0,0 +1,23 @@
@startuml
skinparam backgroundColor transparent
abstract class Response {
# context: RequestContext
# body: ResponseBody
+ prefix(): String
+ getSessionId(): SessionId
+ getRequestId(): int
+ getBody(): ResponseBody
}
abstract class SuccessResponse extends Response {
+ prefix(): String (+OK)
}
class OkResponse extends SuccessResponse
class ErrorResponse extends Response {
+ prefix(): String (-ERR)
}
@enduml
File diff suppressed because one or more lines are too long
@@ -0,0 +1,20 @@
@startuml
skinparam backgroundColor transparent
participant SessionReader
participant CommandParserDispatcher as Dispatcher
participant "CommandParser<T>" as Parser
participant CommandRouter as Router
participant "CommandHandler<T>" as Handler
participant ResponseDispatcher
SessionReader -> Dispatcher : parse(primitiveRequest)
Dispatcher -> Parser : parse(primitiveRequest)
Parser --> Dispatcher : ExampleRequest
Dispatcher --> SessionReader : Request
SessionReader -> Router : execute(request)
Router -> Handler : execute(ExampleRequest)
Handler -> ResponseDispatcher : dispatch(ExampleRequest)
@enduml
File diff suppressed because one or more lines are too long
@@ -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
} }
/** /**
@@ -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
} }
/** /**
@@ -41,6 +41,11 @@ public class ChatController {
chatScrollPane.vvalueProperty().bind(chatVBox.heightProperty()); chatScrollPane.vvalueProperty().bind(chatVBox.heightProperty());
} }
/** Standardkonstruktor. Wird von FXML verwendet. */
public ChatController() {
// default constructor for FXML
}
/** /**
* Diese Methode wird vom Senden-Button oder Enter ausgelöst. Sie gibt die eigene Nachricht an * Diese Methode wird vom Senden-Button oder Enter ausgelöst. Sie gibt die eigene Nachricht an
* das Netzwerkprotokoll weiter. * das Netzwerkprotokoll weiter.
@@ -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!");
@@ -17,6 +17,11 @@ import javafx.stage.Stage;
*/ */
public class CasinoGameUI extends Application { public class CasinoGameUI extends Application {
/** Standardkonstruktor. */
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;
@@ -29,7 +34,7 @@ public class CasinoGameUI extends Application {
@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)");
@@ -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;
@@ -28,6 +28,7 @@ public class CasinomainuiController {
private LobbyButtonGridManager gridManager; private LobbyButtonGridManager gridManager;
private int nextButtonId = 1; private int nextButtonId = 1;
/** Default constructor for dependency injection by FXMLLoader. */
public CasinomainuiController() { public CasinomainuiController() {
// Default constructor // Default constructor
} }
@@ -39,7 +40,7 @@ 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 = gridManager =
new LobbyButtonGridManager(new javafx.scene.layout.GridPane(), translationManager); new LobbyButtonGridManager(new javafx.scene.layout.GridPane(), translationManager);
casinoTable.getChildren().clear(); casinoTable.getChildren().clear();
@@ -57,7 +58,7 @@ 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++;
@@ -67,7 +68,7 @@ public class CasinomainuiController {
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("Error while adding lobby button: {}", e.getMessage());
} }
} }
} }
@@ -17,6 +17,8 @@ import org.apache.logging.log4j.Logger;
* 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. */
@@ -46,7 +48,8 @@ public class LobbyButtonGridManager {
public LobbyButtonGridManager( public LobbyButtonGridManager(
GridPane gridPane, LobbyButtonTranslationManager translationManager) { GridPane gridPane, LobbyButtonTranslationManager translationManager) {
this.gridPane = gridPane; this.gridPane = gridPane;
this.translationManager = translationManager; // Singleton immer verwenden
this.translationManager = LobbyButtonTranslationManager.getInstance();
} }
/** /**
@@ -65,8 +68,21 @@ public class LobbyButtonGridManager {
int buttonId = entry.getKey(); int buttonId = entry.getKey();
Button btn = new Button(); Button btn = new Button();
btn.setId("lobbyBtn-" + buttonId); btn.setId("lobbyBtn-" + buttonId);
btn.setGraphic( ImageView imageView =
new ImageView(new Image(getClass().getResourceAsStream(BUTTON_IMAGE_PATH)))); new ImageView(new Image(getClass().getResourceAsStream(BUTTON_IMAGE_PATH)));
imageView.setPreserveRatio(true);
// Dynamische Breite: Bindung an die Zellengröße
imageView
.fitWidthProperty()
.bind(gridPane.widthProperty().divide(COLS).subtract(BUTTON_WIDTH_MARGIN));
imageView.setSmooth(true);
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);
btn.setOnAction( btn.setOnAction(
e -> { e -> {
Integer lobbyId = translationManager.getLobbyIdForButton(buttonId); Integer lobbyId = translationManager.getLobbyIdForButton(buttonId);
@@ -99,8 +115,22 @@ 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 // Game-UI starten und Lobby-UI schließen
LOGGER.info("Joining lobby: {}", lobbyId); LOGGER.info("Joining lobby: {}", lobbyId);
javafx.application.Platform.runLater(
() -> {
// Lobby-Stage schließen
javafx.stage.Stage currentStage =
(javafx.stage.Stage) gridPane.getScene().getWindow();
currentStage.close();
// Game-UI starten
try {
new ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.CasinoGameUI()
.start(new javafx.stage.Stage());
} catch (Exception e) {
LOGGER.error("Fehler beim Starten der Game-UI: {}", e.getMessage());
}
});
} }
/** /**
@@ -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;
@@ -1,5 +1,11 @@
package ch.unibas.dmi.dbis.cs108.casono.server; package ch.unibas.dmi.dbis.cs108.casono.server;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.check_nick.CheckUsernameHandler;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.check_nick.CheckUsernameParser;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.check_nick.CheckUsernameRequest;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.ping.PingHandler;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.ping.PingParser;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.ping.PingRequest;
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;
@@ -7,6 +13,7 @@ import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandR
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.CommandParserDispatcher; 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.protocol.response.dispatcher.ResponseDispatcher;
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionDisconnectJob; 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;
@@ -58,6 +65,31 @@ public class ServerApp {
SESSION_DISCONNECT_JOB_PERIOD, SESSION_DISCONNECT_JOB_PERIOD,
TimeUnit.SECONDS); TimeUnit.SECONDS);
ResponseDispatcher responseDispatcher = new ResponseDispatcher(sessionManager);
registerCommands(dispatcher, router, responseDispatcher, userRegistry);
networkManager.start(); networkManager.start();
} }
/**
* Registers command parsers and handlers.
*
* @param parserDispatcher the dispatcher responsible for parsing incoming commands
* @param commandRouter the router that dispatches parsed commands to appropriate handlers
* @param responseDispatcher the dispatcher responsible for sending responses back to clients
*/
private static void registerCommands(
CommandParserDispatcher parserDispatcher,
CommandRouter commandRouter,
ResponseDispatcher responseDispatcher,
UserRegistry userRegistry) {
parserDispatcher.register("PING", new PingParser());
commandRouter.register(PingRequest.class, new PingHandler(responseDispatcher));
parserDispatcher.register("CHECK_USERNAME", new CheckUsernameParser());
commandRouter.register(
CheckUsernameRequest.class,
new CheckUsernameHandler(responseDispatcher, userRegistry));
}
} }
@@ -0,0 +1,44 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.check_nick;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.User;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserRegistry;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatcher;
import java.util.Optional;
/** Handles {@link CheckUsernameRequest}s to check whether a username is available. */
public class CheckUsernameHandler implements CommandHandler<CheckUsernameRequest> {
private final ResponseDispatcher responseDispatcher;
private final UserRegistry userRegistry;
/**
* Creates a new handler for checking username availability.
*
* @param responseDispatcher the dispatcher used to send the response
* @param userRegistry the registry used to look up existing users
*/
public CheckUsernameHandler(ResponseDispatcher responseDispatcher, UserRegistry userRegistry) {
this.responseDispatcher = responseDispatcher;
this.userRegistry = userRegistry;
}
/**
* Executes the username availability check for the given request.
*
* <p>If no user exists for the requested username, the username is reported as {@link
* UsernameAvailability#FREE}; otherwise, it is reported as {@link UsernameAvailability#TAKEN}.
*
* @param request the request to execute
*/
@Override
public void execute(CheckUsernameRequest request) {
Optional<User> user = userRegistry.getByUsername(request.getUsername());
UsernameAvailability availability;
if (user.isEmpty()) {
availability = UsernameAvailability.FREE;
} else {
availability = UsernameAvailability.TAKEN;
}
responseDispatcher.dispatch(new CheckUsernameResponse(request.getContext(), availability));
}
}
@@ -0,0 +1,21 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.check_nick;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.CommandParser;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.PrimitiveRequest;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.accessor.RequestParameterAccessor;
/** Parses a primitive request into a {@link CheckUsernameRequest}. */
public class CheckUsernameParser implements CommandParser<CheckUsernameRequest> {
/**
* Extracts the required {@code USERNAME} parameter from the incoming request.
*
* @param primitiveRequest the request to parse
* @return {@link CheckUsernameRequest} containing the username
*/
@Override
public CheckUsernameRequest parse(PrimitiveRequest primitiveRequest) {
RequestParameterAccessor accessor =
new RequestParameterAccessor(primitiveRequest.parameters());
return new CheckUsernameRequest(primitiveRequest.context(), accessor.require("USERNAME"));
}
}
@@ -0,0 +1,30 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.check_nick;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.Request;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
/** Request implementation used to check whether a username is available or already taken */
public class CheckUsernameRequest extends Request {
private final String username;
/**
* Constructs a new CheckUsernameRequest with the given context and username to check
*
* @param context the {@link RequestContext} containing information for responding to the
* request
* @param username the username to check for availability
*/
public CheckUsernameRequest(RequestContext context, String username) {
super(context);
this.username = username;
}
/**
* Returns the provided username in the request
*
* @return username to check
*/
public String getUsername() {
return username;
}
}
@@ -0,0 +1,18 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.check_nick;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.SuccessResponse;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder.ResponseBodyBuilder;
/** Response indicating the availability status of a username check. */
public class CheckUsernameResponse extends SuccessResponse {
/**
* Creates a new response to respond to the username availability check to
*
* @param context the {@link RequestContext} associated with the request
* @param availability the availability status of the requested username
*/
public CheckUsernameResponse(RequestContext context, UsernameAvailability availability) {
super(context, new ResponseBodyBuilder().param("STATUS", availability).build());
}
}
@@ -0,0 +1,10 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.check_nick;
/** Represents the availability status of a username */
enum UsernameAvailability {
/** Username is available */
FREE,
/** Username is already in use */
TAKEN
}
@@ -0,0 +1,29 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.ping;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.OkResponse;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatcher;
/** Handler for {@link PingRequest}. */
public class PingHandler implements CommandHandler<PingRequest> {
private final ResponseDispatcher responseDispatcher;
/**
* Create a new PingHandler to execute {@link PingRequest}s
*
* @param responseDispatcher dispatcher used to send responses back to clients
*/
public PingHandler(ResponseDispatcher responseDispatcher) {
this.responseDispatcher = responseDispatcher;
}
/**
* Execute the ping request.
*
* @param request the ping request to handle
*/
@Override
public void execute(PingRequest request) {
responseDispatcher.dispatch(new OkResponse(request.getContext()));
}
}
@@ -0,0 +1,22 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.ping;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.CommandParser;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.PrimitiveRequest;
/**
* Parser for the Ping command.
*
* <p>Converts a low-level {@link PrimitiveRequest} into a {@link PingRequest}.
*/
public class PingParser implements CommandParser<PingRequest> {
/**
* Parse the given primitive request into a {@link PingRequest}.
*
* @param primitiveRequest the raw request to parse
* @return {@link PingRequest}
*/
@Override
public PingRequest parse(PrimitiveRequest primitiveRequest) {
return new PingRequest(primitiveRequest.context());
}
}
@@ -0,0 +1,19 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.ping;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.Request;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
/**
* Represents a "PING" request sent by a client to check server availability and keep the connection
* alive.
*/
public class PingRequest extends Request {
/**
* Constructs a new PingRequest with the given context.
*
* @param context the request context associated with this request
*/
public PingRequest(RequestContext context) {
super(context);
}
}
@@ -86,15 +86,38 @@ public class UserRegistry {
} }
/** /**
* Looks up a user by their session ID. * Looks up a user by their {@link SessionId}.
* *
* @param sessionId the session ID to look up * @param sessionId the SessionId to look up
* @return an Optional containing the user, or empty if no user is associated with this session * @return an Optional containing the {@link User}, or empty if no user is associated with this
* SessionId
*/ */
public Optional<User> findBySessionId(SessionId sessionId) { public Optional<User> getBySessionId(SessionId sessionId) {
return Optional.ofNullable(bySessionId.get(sessionId)); return Optional.ofNullable(bySessionId.get(sessionId));
} }
/**
* Looks up a user by their {@link UserId}.
*
* @param userId the UserId to look up
* @return an Optional containing the {@link User}, or empty if no user is associated with this
* UserId
*/
public Optional<User> getByUserId(UserId userId) {
return Optional.ofNullable(byId.get(userId));
}
/**
* Looks up a user by their username.
*
* @param username the username to look up
* @return an Optional containing the {@link User}, or empty if no user is associated with this
* username
*/
public Optional<User> getByUsername(String username) {
return Optional.ofNullable(byName.get(username));
}
/** /**
* Returns all currently registered users. * Returns all currently registered users.
* *
@@ -7,12 +7,12 @@ 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 * Parser to convert the PrimitiveRequest to a Request and performing checks for required fields and
* data types * data types
*/ */
public interface CommandParser { public interface CommandParser<T extends Request> {
/** /**
* Parses the provided PrimitiveRequest into a command-specific request * Parses the provided PrimitiveRequest into a command-specific request
* *
* @param primitiveRequest * @param primitiveRequest
* @return * @return
*/ */
Request parse(PrimitiveRequest primitiveRequest); T parse(PrimitiveRequest primitiveRequest);
} }
@@ -1,23 +1,20 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response; 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.protocol.response.builder.ResponseBody;
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionId;
/** Response representing an error outcome for a client's request. */ /** Response representing an error outcome for a client's request. */
public class ErrorResponse extends Response { public class ErrorResponse extends Response {
/** /**
* Construct an error response with a code and message. * Construct an error response with a code and message.
* *
* @param sessionId the target session id * @param context the RequestContext of the request
* @param requestId the originating request id
* @param errorCode a short error code identifying the failure * @param errorCode a short error code identifying the failure
* @param errorMessage a human readable error message * @param errorMessage a human readable error message
*/ */
public ErrorResponse( public ErrorResponse(RequestContext context, String errorCode, String errorMessage) {
SessionId sessionId, int requestId, String errorCode, String errorMessage) {
super( super(
sessionId, context,
requestId,
ResponseBody.builder().param("CODE", errorCode).param("MSG", errorMessage).build()); ResponseBody.builder().param("CODE", errorCode).param("MSG", errorMessage).build());
} }
@@ -1,7 +1,7 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response; 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.protocol.response.builder.ResponseBody;
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionId;
/** /**
* A simple success response with an empty body. * A simple success response with an empty body.
@@ -12,10 +12,9 @@ public class OkResponse extends SuccessResponse {
/** /**
* Create a minimal successful response (no body content). * Create a minimal successful response (no body content).
* *
* @param sessionId the target session id * @param context the RequestContext of the request
* @param requestId the originating request id
*/ */
public OkResponse(SessionId sessionId, int requestId) { public OkResponse(RequestContext context) {
super(sessionId, requestId, ResponseBody.builder().build()); super(context, ResponseBody.builder().build());
} }
} }
@@ -1,24 +1,22 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response; 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.protocol.response.builder.ResponseBody;
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionId; import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionId;
/** Abstract base class for all server responses sent to clients. */ /** Abstract base class for all server responses sent to clients. */
public abstract class Response { public abstract class Response {
private final SessionId sessionId; private final RequestContext context;
private final int requestId;
private final ResponseBody body; private final ResponseBody body;
/** /**
* Create a new {@code Response}. * Create a new {@code Response}.
* *
* @param sessionId the id of the session this response targets * @param context the RequestContext of the request
* @param requestId the request identifier this response corresponds to
* @param body the structured response body * @param body the structured response body
*/ */
protected Response(SessionId sessionId, int requestId, ResponseBody body) { protected Response(RequestContext context, ResponseBody body) {
this.sessionId = sessionId; this.context = context;
this.requestId = requestId;
this.body = body; this.body = body;
} }
@@ -30,12 +28,12 @@ public abstract class Response {
public abstract String prefix(); public abstract String prefix();
/** /**
* Returns the session id that should receive this response. * Returns the session id of the session that should receive this response.
* *
* @return the target {@link SessionId} * @return the target {@link SessionId}
*/ */
public SessionId getSessionId() { public SessionId getSessionId() {
return sessionId; return context.sessionId();
} }
/** /**
@@ -44,7 +42,7 @@ public abstract class Response {
* @return the numeric request id * @return the numeric request id
*/ */
public int getRequestId() { public int getRequestId() {
return requestId; return context.requestId();
} }
/** /**
@@ -1,7 +1,7 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response; 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.protocol.response.builder.ResponseBody;
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionId;
/** /**
* Abstract {@link Response} specialization indicating a successful outcome. * Abstract {@link Response} specialization indicating a successful outcome.
@@ -13,12 +13,11 @@ public abstract class SuccessResponse extends Response {
/** /**
* Create a successful response with the provided body. * Create a successful response with the provided body.
* *
* @param sessionId the session id this response targets * @param context the RequestContext of the request
* @param requestId the originating request id
* @param body the response body * @param body the response body
*/ */
protected SuccessResponse(SessionId sessionId, int requestId, ResponseBody body) { protected SuccessResponse(RequestContext context, ResponseBody body) {
super(sessionId, requestId, body); super(context, body);
} }
/** /**
@@ -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);
}
}
@@ -27,12 +27,17 @@ public class ResponseDispatcher {
* the target session's response queue. * the target session's response queue.
* *
* @param response the response to dispatch * @param response the response to dispatch
* @throws InterruptedException if the thread is interrupted while waiting to enqueue the * @throws ResponseDispatchException wraps any exceptions that occur during dispatching, such as
* primitive response * the {@link InterruptedException}
*/ */
public void dispatch(Response response) throws InterruptedException { public void dispatch(Response response) {
PrimitiveResponse primitiveResponse = ResponseEncoder.encode(response); PrimitiveResponse primitiveResponse = ResponseEncoder.encode(response);
Session session = sessionManager.getSessionById(response.getSessionId()); Session session = sessionManager.getSessionById(response.getSessionId());
session.getResponseQueue().put(primitiveResponse); try {
session.getResponseQueue().put(primitiveResponse);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new ResponseDispatchException("Interrupted while dispatching response", e);
}
} }
} }
@@ -12,8 +12,10 @@ import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.Primitive
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RawRequest; 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.Request;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext; import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.accessor.MissingParameterException;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.ErrorResponse; 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.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.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.RawPacket;
import ch.unibas.dmi.dbis.cs108.casono.server.network.transport.TransportLayer; import ch.unibas.dmi.dbis.cs108.casono.server.network.transport.TransportLayer;
@@ -46,11 +48,13 @@ public class SessionReader implements Runnable {
while (!Thread.currentThread().isInterrupted()) { while (!Thread.currentThread().isInterrupted()) {
RawPacket rawPacket = null; RawPacket rawPacket = null;
RawRequest rawRequest = null; RawRequest rawRequest = null;
RequestContext requestContext = null;
try { try {
// Step 1: Read from transport // Step 1: Read from transport
rawPacket = transport.read(); rawPacket = transport.read();
session.updateLastInboundActivity(); session.updateLastInboundActivity();
logger.debug("Recieved: {}", rawPacket); logger.debug("Recieved: {}", rawPacket);
requestContext = new RequestContext(session.getId(), rawPacket.requestId());
// Step 2: Syntax validation and conversion into transport object // Step 2: Syntax validation and conversion into transport object
rawRequest = ProtocolParser.parse(rawPacket.payload()); rawRequest = ProtocolParser.parse(rawPacket.payload());
@@ -58,9 +62,7 @@ public class SessionReader implements Runnable {
PrimitiveRequest primitiveRequest = PrimitiveRequest primitiveRequest =
new PrimitiveRequest( new PrimitiveRequest(
new RequestContext(session.getId(), rawPacket.requestId()), requestContext, rawRequest.command(), rawRequest.parameters());
rawRequest.command(),
rawRequest.parameters());
logger.debug("Converted to {}", primitiveRequest); logger.debug("Converted to {}", primitiveRequest);
// Step 3: Parse into Request and execute Request // Step 3: Parse into Request and execute Request
@@ -76,8 +78,7 @@ public class SessionReader implements Runnable {
sendErrorResponse( sendErrorResponse(
new ErrorResponse( new ErrorResponse(
session.getId(), requestContext,
rawPacket.requestId(),
"PARSING_ERROR", "PARSING_ERROR",
"Error occured during parsing. Likely due to malformed payload.")); "Error occured during parsing. Likely due to malformed payload."));
@@ -85,11 +86,23 @@ public class SessionReader implements Runnable {
logger.error("Recieved unknown command '{}' from client", rawRequest.command(), e); logger.error("Recieved unknown command '{}' from client", rawRequest.command(), e);
sendErrorResponse( sendErrorResponse(
new ErrorResponse( new ErrorResponse(
session.getId(), requestContext,
rawPacket.requestId(),
"UNKNOWN_COMMAND", "UNKNOWN_COMMAND",
"This command is unknown to the server.")); "This command is unknown to the server."));
} catch (ResponseDispatchException e) {
logger.error(
"Unexpected ResponseDispatchException exception while dispatching request",
e);
} catch (MissingParameterException e) {
logger.error(
"Recieved request for command '{}' was missing the '{}' parameter",
rawRequest.command(),
e.getParameterKey());
sendErrorResponse(
new ErrorResponse(requestContext, "MISSING_PARAMETER", e.getMessage()));
} catch (IOException e) { } catch (IOException e) {
logger.error("Unexpected IO exception while reading from transport", e); logger.error("Unexpected IO exception while reading from transport", e);
@@ -97,8 +110,7 @@ public class SessionReader implements Runnable {
logger.error("Unexpected RuntimeException occured", e); logger.error("Unexpected RuntimeException occured", e);
sendErrorResponse( sendErrorResponse(
new ErrorResponse( new ErrorResponse(
session.getId(), requestContext,
rawPacket.requestId(),
"INTERNAL_ERROR", "INTERNAL_ERROR",
"Unexpected internal server error occured.")); "Unexpected internal server error occured."));
} }
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

Binary file not shown.

After

Width:  |  Height:  |  Size: 662 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 663 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 657 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 664 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 665 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 648 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 661 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 656 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 656 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 653 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 655 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 658 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 654 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 660 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 658 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 659 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 653 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 660 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 660 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 693 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 644 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 658 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 652 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 653 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 650 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 651 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 654 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 650 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 656 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 655 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 656 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 650 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 657 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 658 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 694 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 647 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 661 KiB

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