diff --git a/.gitlab/issue_templates/Milestone Achievement.md b/.gitlab/issue_templates/Milestone Achievement.md new file mode 100644 index 0000000..93de10c --- /dev/null +++ b/.gitlab/issue_templates/Milestone Achievement.md @@ -0,0 +1,16 @@ +## Milestone Achievement + + +### Category + + +### Title + + +### Rewarded points on completion + + +### Description of milestone + + +/label ~achievement diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 92a520a..c6028a9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -12,6 +12,7 @@ Since this project is part of a course at the University of Basel, the [Code of - [Creating an issue](#creating-an-issue) - [During implementation](#during-implementation) - [Collaborative work](#collaborative-work) + - [Milestone Achievements](#milestone-achievements) - [Git Workflow](#git-workflow) - [Creating a branch](#creating-a-branch) - [Working on a branch](#working-on-a-branch) @@ -49,6 +50,20 @@ Issue or Task in GitLab **before** any implementation begins. centrally visible and searchable. - Before starting work that overlaps with an existing issue, check its comment thread first to avoid duplicating effort. +### Milestone Achievements +For every process and product-related milestone achievement, there is a dedicated milestone task. + +- **Do not create a branch directly from a milestone task.** +- Instead, create a normal implementation task (using the regular task template) and reference the milestone task there. +- In the merge request, reference the milestone task again. +- If the milestone condition is fully met, you may use `Closing #` to close the milestone task. +- If it is only partially addressed, use `Relates to #` or `Contributes to #` so the milestone task stays open. + +Milestone tasks may, but do not have to, be assigned to a specific person. + +- If multiple people are involved, contribution is tracked through linked tasks that reference the milestone task. +- For small topics, a Milestone Achievement may be assigned to one person. Others should only contribute on request and should not modify components introduced under that achievement without coordination. + ## Git Workflow We use a **feature branch -> main** strategy. The `main` branch is always in a releasable state. diff --git a/documents/docs/networking/commands/README.md b/documents/docs/networking/commands/README.md index 391ec9d..3a85340 100644 --- a/documents/docs/networking/commands/README.md +++ b/documents/docs/networking/commands/README.md @@ -17,6 +17,10 @@ The concrete implementations for each command live in `app/commands/` and are wi - [Command Infrastructure](./commands-deep-dive.md) - Technical deep-dive into the `CommandParser`, `CommandParserDispatcher`, `CommandHandler`, `CommandRouter`, `Request`, and the response hierarchy. +### Protocol document +- [Protocol Document](./protocol-document.md) - + List of all supported commands by the server, along with descriptions, required pre-execution checks, and an example for both request and response. + ## Key Concepts **Each command is self-contained.** A command's Parser, Request, Handler, and Response all live in the same package under `app/commands//`. diff --git a/documents/docs/networking/commands/protocol-document.md b/documents/docs/networking/commands/protocol-document.md new file mode 100644 index 0000000..b08cf7f --- /dev/null +++ b/documents/docs/networking/commands/protocol-document.md @@ -0,0 +1,344 @@ +# Protocol Document +This document describes the protocol for client-server communication in our application. It defines the structure of requests and responses, the supported commands along with their request parameters, response formats, and possible errors. + + +# Table of Contents +- [Protocol Document](#protocol-document) +- [Table of Contents](#table-of-contents) +- [General structure of requests](#general-structure-of-requests) +- [Preconditions](#preconditions) + - [Parsing](#parsing) + - [Error Response](#error-response) + - [Command dispatching](#command-dispatching) + - [Error Response](#error-response-1) + - [Command parsing](#command-parsing) + - [Error Response](#error-response-2) +- [Pre-execution checks](#pre-execution-checks) + - [UserLoggedInCheck](#userloggedincheck) + - [Error Response](#error-response-3) +- [Commands](#commands) + - [PING command](#ping-command) + - [Required pre-execution checks](#required-pre-execution-checks) + - [Request Parameters](#request-parameters) + - [Success Response](#success-response) + - [Example Request](#example-request) + - [Example Response](#example-response) + - [CHECK\_USERNAME command](#check_username-command) + - [Required pre-execution checks](#required-pre-execution-checks-1) + - [Request Parameters](#request-parameters-1) + - [Success Response](#success-response-1) + - [Example Request](#example-request-1) + - [Example Response](#example-response-1) + - [LOGIN command](#login-command) + - [Required pre-execution checks](#required-pre-execution-checks-2) + - [Request Parameters](#request-parameters-2) + - [Success Response](#success-response-2) + - [Error Response](#error-response-4) + - [Example Request](#example-request-2) + - [Example Response](#example-response-2) + - [LOGOUT command](#logout-command) + - [Required pre-execution checks](#required-pre-execution-checks-3) + - [Request Parameters](#request-parameters-3) + - [Success Response](#success-response-3) + - [Error Response](#error-response-5) + - [Example Request](#example-request-3) + - [Example Response](#example-response-3) + - [LIST\_USERS command](#list_users-command) + - [Required pre-execution checks](#required-pre-execution-checks-4) + - [Request Parameters](#request-parameters-4) + - [Success Response](#success-response-4) + - [Error Response](#error-response-6) + - [Example Request](#example-request-4) + - [Example Response](#example-response-4) + + + +# General structure of requests +As mentioned before, our protocol is based on POP3. +Each command is represented as a single line of text, starting with the command name followed by parameters. The server responds with a status line indicating success or failure, followed by the body. + +Requests can have parameters that provide additional information for the command. Parameters are key-value pairs separated by an equal sign (`=`). + +Responses are collections of key-value pairs, containing either a value or another collection, allowing for nested structures. +Each collection is ended with the `END` keyword. + + + +# Preconditions +The serverside pipeline to process incoming requests consists of multiple stages. +Each of these stages can yield an error response if the request does not meet the requirements of that stage. + +## Parsing +One of these stages is the parsing. It is responsible for parsing the raw request into a structured format that can be easily processed by the command handlers. It validates the syntax of the request as well. + +### Error Response +| Code | Description | +| :-------------- | :---------------------------------------------------------------------------------------------- | +| `PARSING_ERROR` | The body of the request contains syntax errors (see message field of response for more details) | + + +## Command dispatching +After the request has been successfully parsed, the next stage is to dispatch the `PrimitiveRequest` to the appropriate `CommandParser`. This is done by the `CommandDispatcherDispatcher`, which uses the command name to determine which parser to use. + +### Error Response +| Code | Description | +| :---------------- | :---------------------------------------------------------------- | +| `UNKNOWN_COMMAND` | The command is unknown to this server. No parser has been defined | + + +## Command parsing +Once the `PrimitiveRequest` has been dispatched to the appropriate `CommandParser`, the parser is responsible for parsing the parameters of the request and creating a `Request` that can be executed by the responsible `CommandHandler`. + +### Error Response +| Code | Description | +| :------------------ | :------------------------------------------------------------------------------------------------ | +| `MISSING_PARAMETER` | A required parameter is missing from the request (see message field of response for more details) | + + + +# Pre-execution checks +Pre-execution checks are reusable validation steps that can be registered on command handlers. +They are implemented as `HandlerCheck` instances and are executed by the `CommandHandlerExecutor` before the handler's main logic is invoked. + + + +## UserLoggedInCheck +The `UserLoggedInCheck` is a common pre-execution check that verifies whether the user is logged in (i.e. has a user associated with his session). + +### Error Response +| Code | Description | +| :------------------- | :------------------------ | +| `USER_NOT_LOGGED_IN` | The user is not logged in | + + + +# Commands +Commands are the core of our protocol, representing the various actions that clients can request from the server. Each command has a unique name and may require specific parameters in addition to pre-execution checks. +The server processes these commands and responds accordingly. + + + +## PING command +The `PING` command is a simple command that can be used to check if the server is responsive. + +### Required pre-execution checks +None. + +### Request Parameters +No parameters. + +### Success Response +No response fields. + +### Example Request +``` +PING +``` + +### Example Response +``` ++OK +END +``` + + +## CHECK_USERNAME command +The `CHECK_USERNAME` command is used to check if a username is already taken by another user. Additional users can still log in with the same username, but their name will be substituted with a suffix. + +### Required pre-execution checks +None. + +### Request Parameters +| Parameter Name | Type | Optional | Description | +| :------------- | :------- | :------- | :------------------------------------- | +| `USERNAME` | `String` | no | The username to check for availability | + +### Success Response +| Field | Type | Description | +| :------- | :--------------------------- | :---------------------------------------------------------------------- | +| `STATUS` | `Enum` | Member of enum indicating if the username is available or already taken | + +| Members of `UsernameAvailability` | Description | +| :-------------------------------- | :------------------------- | +| `FREE` | Username is available | +| `TAKEN` | Username is already in use | + +### Example Request +``` +CHECK_USERNAME USERNAME='Lars' +``` + +### Example Response +``` ++OK + STATUS=FREE +END +``` + +## LOGIN command +The `LOGIN` command is used to log in a user with a specified username. If the username is already taken by another user, the server will append a suffix to the username to make it unique. + +### Required pre-execution checks +None. + +### Request Parameters +| Parameter Name | Type | Optional | Description | +| :------------- | :------- | :------- | :----------------------------------- | +| `USERNAME` | `String` | no | The username to create the user with | + +### Success Response +| Field | Type | Description | +| :--------- | :---------------------------------------------------------------------- | :-------------------------------------------------------------------- | +| `USERNAME` | `String` | Username of the newly created user, can differ from the requested one | +| `ID` | [`UUID`](https://docs.oracle.com/javase/8/docs/api/java/util/UUID.html) | The ID of the created user | + +### Error Response +| Code | Description | +| :------------------ | :----------------------------------------------------------------------------- | +| `ALREADY_LOGGED_IN` | The session is already associated with a user, logging in again is prohibited. | + +### Example Request +``` +LOGIN USERNAME='Lars' +``` + +### Example Response +``` ++OK + USERNAME='Lars_1234' + ID=e47a671e-2b2a-42df-bb82-953fe2ebd307 +END +``` + + +## LOGOUT command +Description of the command, what it does, and when it should be used. + +### Required pre-execution checks +None. + +### Request Parameters +No parameters. + +### Success Response +No response fields. + +### Error Response +| Code | Description | +| :------------------- | :--------------------------------------------------------------- | +| `NO_USER_ASSOCIATED` | The session has no user associated, logging out is not possible. | + +### Example Request +``` +LOGOUT +``` + +### Example Response +``` ++OK +END +``` + +## LIST_USERS command +The `LIST_USERS` command is used to retrieve a list of all currently logged-in users. + +### Required pre-execution checks +None. + +### Request Parameters +No parameters. + +### Success Response +| Field | Type | Description | +| :------ | :----------------- | :--------------------------------------- | +| `USERS` | `Collection` | Collection of all users currently online | + +| Fields of `User` | Type | Description | +| :--------------- | :---------------------------------------------------------------------- | :-------------------------------------------------------------------- | +| `USERNAME` | `String` | Username of the newly created user, can differ from the requested one | +| `ID` | [`UUID`](https://docs.oracle.com/javase/8/docs/api/java/util/UUID.html) | The ID of the created user | + + +### Error Response +None. + +### Example Request +``` +LIST_USERS +``` + +### Example Response +``` ++OK + USERS + USER + USERNAME=Lars_001 + ID=56765d0f-8cd3-4eec-91b2-7e36265c1a5d + END + USER + USERNAME=Lars_002 + ID=b7bbd9b3-0d49-4c92-8306-b1c8506d2ff0 + END + USER + USERNAME=Lars + ID=982bc78e-547f-495a-a821-433a3603f92c + END + END +END +``` \ No newline at end of file diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/ServerApp.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/ServerApp.java index febc9a9..57729a8 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/ServerApp.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/ServerApp.java @@ -3,6 +3,9 @@ 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.list_users.ListUsersHandler; +import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.list_users.ListUsersParser; +import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.list_users.ListUsersRequest; import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.login.LoginHandler; import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.login.LoginParser; import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.login.LoginRequest; @@ -106,5 +109,9 @@ public class ServerApp { parserDispatcher.register("LOGOUT", new LogoutParser()); commandRouter.register( LogoutRequest.class, new LogoutHandler(responseDispatcher, userRegistry)); + + parserDispatcher.register("LIST_USERS", new ListUsersParser()); + commandRouter.register( + ListUsersRequest.class, new ListUsersHandler(responseDispatcher, userRegistry)); } } diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/list_users/ListUsersHandler.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/list_users/ListUsersHandler.java new file mode 100644 index 0000000..2ad7fe2 --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/list_users/ListUsersHandler.java @@ -0,0 +1,40 @@ +package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.list_users; + +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.Collection; + +/** + * Handles {@link ListUsersRequest}s by retrieving all users from the {@link UserRegistry} and + * dispatching a {@link ListUsersResponse} containing the list of users. + */ +public class ListUsersHandler extends CommandHandler { + private final UserRegistry userRegistry; + + /** + * Creates a new handler for listing all users + * + * @param responseDispatcher the dispatcher used to send the response + * @param userRegistry the registry used to look up existing users + */ + public ListUsersHandler(ResponseDispatcher responseDispatcher, UserRegistry userRegistry) { + super(responseDispatcher); + this.userRegistry = userRegistry; + } + + /** + * Executes the list users request + * + *

All users are retrieved from the {@link UserRegistry} and returned in a {@link + * ListUsersResponse}. + * + * @param request the request to execute + */ + @Override + public void execute(ListUsersRequest request) { + Collection users = userRegistry.getAllUsers(); + responseDispatcher.dispatch(new ListUsersResponse(request.getContext(), users)); + } +} diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/list_users/ListUsersParser.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/list_users/ListUsersParser.java new file mode 100644 index 0000000..f55149e --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/list_users/ListUsersParser.java @@ -0,0 +1,18 @@ +package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.list_users; + +import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.CommandParser; +import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.PrimitiveRequest; + +/** Parses a primitive request into a {@link ListUsersRequest}. */ +public class ListUsersParser implements CommandParser { + /** + * Parses a primitive request into a ListUsersRequest. + * + * @param primitiveRequest the request to parse + * @return the created {@link ListUsersRequest} + */ + @Override + public ListUsersRequest parse(PrimitiveRequest primitiveRequest) { + return new ListUsersRequest(primitiveRequest.context()); + } +} diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/list_users/ListUsersRequest.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/list_users/ListUsersRequest.java new file mode 100644 index 0000000..9ed8fdd --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/list_users/ListUsersRequest.java @@ -0,0 +1,17 @@ +package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.list_users; + +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 retrieve all existing users from the server. */ +public class ListUsersRequest extends Request { + /** + * Constructs a new ListUsersRequest with the given context + * + * @param context the {@link RequestContext} containing information for responding to the + * request + */ + public ListUsersRequest(RequestContext context) { + super(context); + } +} diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/list_users/ListUsersResponse.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/list_users/ListUsersResponse.java new file mode 100644 index 0000000..4e48aaf --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/list_users/ListUsersResponse.java @@ -0,0 +1,35 @@ +package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.list_users; + +import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.User; +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.ResponseBody; +import java.util.Collection; + +/** Response containing a list of all active users on the server */ +public class ListUsersResponse extends SuccessResponse { + /** + * Creates a new ListUsersResponse containing the given list of users + * + * @param context the {@link RequestContext} associated with the request + * @param users the collection of users currently active on the server + */ + public ListUsersResponse(RequestContext context, Collection users) { + super( + context, + ResponseBody.builder() + .block( + "USERS", + users_block -> { + for (User user : users) { + users_block.block( + "USER", + user_block -> { + user_block.param("USERNAME", user.getName()); + user_block.param("ID", user.getId().value()); + }); + } + }) + .build()); + } +} diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/command/execution/CommandHandler.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/command/execution/CommandHandler.java index f0f8b97..4e82e3a 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/command/execution/CommandHandler.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/command/execution/CommandHandler.java @@ -52,5 +52,5 @@ public abstract class CommandHandler { * * @param request the request to execute, of type T */ - public void execute(T request) {} + public abstract void execute(T request); } diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/request/accessor/ThrowingParser.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/request/accessor/ThrowingParser.java index 688304b..b851e85 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/request/accessor/ThrowingParser.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/protocol/request/accessor/ThrowingParser.java @@ -6,7 +6,7 @@ package ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.accessor * @param target type produced by the parser */ @FunctionalInterface -interface ThrowingParser { +public interface ThrowingParser { /** * Parses the provided raw parameter value. *