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 5687b5b..48ac98f 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 @@ -1,5 +1,11 @@ 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.UserRegistry; 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.events.DisconnectEvent; 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.SessionManager; import java.time.Duration; @@ -58,6 +65,31 @@ public class ServerApp { SESSION_DISCONNECT_JOB_PERIOD, TimeUnit.SECONDS); + ResponseDispatcher responseDispatcher = new ResponseDispatcher(sessionManager); + + registerCommands(dispatcher, router, responseDispatcher, userRegistry); + 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)); + } } diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/check_nick/CheckUsernameHandler.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/check_nick/CheckUsernameHandler.java new file mode 100644 index 0000000..c238ba1 --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/check_nick/CheckUsernameHandler.java @@ -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 { + 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. + * + *

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 = userRegistry.getByUsername(request.getUsername()); + UsernameAvailability availability; + if (user.isEmpty()) { + availability = UsernameAvailability.FREE; + } else { + availability = UsernameAvailability.TAKEN; + } + responseDispatcher.dispatch(new CheckUsernameResponse(request.getContext(), availability)); + } +} diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/check_nick/CheckUsernameParser.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/check_nick/CheckUsernameParser.java new file mode 100644 index 0000000..20624c0 --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/check_nick/CheckUsernameParser.java @@ -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 { + /** + * 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")); + } +} diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/check_nick/CheckUsernameRequest.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/check_nick/CheckUsernameRequest.java new file mode 100644 index 0000000..7247b0e --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/check_nick/CheckUsernameRequest.java @@ -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; + } +} diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/check_nick/CheckUsernameResponse.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/check_nick/CheckUsernameResponse.java new file mode 100644 index 0000000..5018323 --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/check_nick/CheckUsernameResponse.java @@ -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()); + } +} diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/check_nick/UsernameAvailability.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/check_nick/UsernameAvailability.java new file mode 100644 index 0000000..df3d358 --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/check_nick/UsernameAvailability.java @@ -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 +} diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/ping/PingHandler.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/ping/PingHandler.java new file mode 100644 index 0000000..1669cd8 --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/ping/PingHandler.java @@ -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 { + 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())); + } +} diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/ping/PingParser.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/ping/PingParser.java new file mode 100644 index 0000000..69ab980 --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/ping/PingParser.java @@ -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. + * + *

Converts a low-level {@link PrimitiveRequest} into a {@link PingRequest}. + */ +public class PingParser implements CommandParser { + /** + * 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()); + } +} diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/ping/PingRequest.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/ping/PingRequest.java new file mode 100644 index 0000000..25ab93f --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/ping/PingRequest.java @@ -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); + } +} diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/user/UserRegistry.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/user/UserRegistry.java index adb1abe..bc0cd36 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/user/UserRegistry.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/user/UserRegistry.java @@ -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 - * @return an Optional containing the user, or empty if no user is associated with this session + * @param sessionId the SessionId to look up + * @return an Optional containing the {@link User}, or empty if no user is associated with this + * SessionId */ - public Optional findBySessionId(SessionId sessionId) { + public Optional getBySessionId(SessionId 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 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 getByUsername(String username) { + return Optional.ofNullable(byName.get(username)); + } + /** * Returns all currently registered users. * diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/sessions/SessionReader.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/sessions/SessionReader.java index 6fb4378..78ffbfd 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/sessions/SessionReader.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/sessions/SessionReader.java @@ -12,6 +12,7 @@ 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.Request; 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.PrimitiveResponse; import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatchException; @@ -94,6 +95,14 @@ public class SessionReader implements Runnable { "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) { logger.error("Unexpected IO exception while reading from transport", e);