Merge remote-tracking branch 'origin/main'
This commit is contained in:
@@ -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`.
|
||||
|
||||

|
||||
|
||||
## 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`.
|
||||
|
||||
<img src="../../../images/docs/networking/commands/sequence_diagram.png" alt="Sequence diagram of all involved components to process and respond to an incoming request" />
|
||||
|
||||
## Parsing Pipeline
|
||||
### `CommandParser<T extends Request>`
|
||||
<img src="../../../images/docs/networking/commands/command_parser.png" alt="Class diagram of the CommandParser" />
|
||||
|
||||
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`
|
||||
<img src="../../../images/docs/networking/commands/command_parser_dispatcher.png" alt="Class diagram of CommandParserDispatcher" />
|
||||
|
||||
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`
|
||||
<img src="../../../images/docs/networking/commands/request_parameter_accessor.png" alt="Class diagram of RequestParameterAccessor" />
|
||||
|
||||
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`
|
||||
<img src="../../../images/docs/networking/commands/request.png" alt="Class diagram of the Request" />
|
||||
|
||||
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>`
|
||||
<img src="../../../images/docs/networking/commands/command_handler.png" alt="Class diagram of the CommandHandler" />
|
||||
|
||||
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`
|
||||
<img src="../../../images/docs/networking/commands/command_router.png" alt="Class diagram of the CommandRouter" />
|
||||
|
||||
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
|
||||
<img src="../../../images/docs/networking/commands/response_types.png" alt="Class diagram of the response interface and built-in implementations" />
|
||||
|
||||
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.
|
||||
@@ -26,8 +26,7 @@ Responses start with `+OK` on success or `-ERR` when something goes wrong. Comma
|
||||
We don't use HTTP, there's no JSON body, no headers. Just a raw socket, a text stream, and a clearly defined set of commands.
|
||||
|
||||
## Core Components & Their Roles
|
||||
|
||||

|
||||
<img src="../../images/docs/networking/server-architecure/networking_components.png" alt="PlantUML diagram of all components outlined in this document" />
|
||||
|
||||
Here's a quick rundown of the main building blocks:
|
||||
|
||||
|
||||
Reference in New Issue
Block a user