diff --git a/.gitignore b/.gitignore index 56624a0..b9d312b 100644 --- a/.gitignore +++ b/.gitignore @@ -48,6 +48,9 @@ gradle-app.setting # JDT-specific (Eclipse Java Development Tools) .classpath +# Gradle properties +gradle.properties + ## MacOS # General .DS_Store @@ -122,4 +125,28 @@ $RECYCLE.BIN/ ## bin bin/ -gradle.properties \ No newline at end of file + +# LaTeX (TeX) +## Core latex/pdflatex auxiliary files: +*.aux +*.lof +*.log +*.lot +*.fls +*.out +*.toc +*.fmt +*.fot +*.cb +*.cb2 +.*.lb + +## Build tool auxiliary files: +*.fdb_latexmk +*.synctex +*.synctex(busy) +*.synctex.gz +*.synctex.gz(busy) +*.pdfsync +*.rubbercache +rubber.cache 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 48ac98f..1b5f62a 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,12 @@ 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.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; +import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.logout.LogoutHandler; +import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.logout.LogoutParser; +import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.logout.LogoutRequest; 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; @@ -91,5 +97,13 @@ public class ServerApp { commandRouter.register( CheckUsernameRequest.class, new CheckUsernameHandler(responseDispatcher, userRegistry)); + + parserDispatcher.register("LOGIN", new LoginParser()); + commandRouter.register( + LoginRequest.class, new LoginHandler(responseDispatcher, userRegistry)); + + parserDispatcher.register("LOGOUT", new LogoutParser()); + commandRouter.register( + LogoutRequest.class, new LogoutHandler(responseDispatcher, userRegistry)); } } diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/login/LoginHandler.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/login/LoginHandler.java new file mode 100644 index 0000000..5d139f8 --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/login/LoginHandler.java @@ -0,0 +1,58 @@ +package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.login; + +import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.User; +import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserFactory; +import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserId; +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.ErrorResponse; +import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatcher; +import java.util.Optional; + +/** + * Handles {@link LoginRequest}s to create a user for a session, if the session has not assigned one + * already + */ +public class LoginHandler implements CommandHandler { + private final ResponseDispatcher responseDispatcher; + private final UserRegistry userRegistry; + private final UserFactory userFactory; + + /** + * Creates a new handler for checking for existing user and creating a new one + * + * @param responseDispatcher the dispatcher used to send the response + * @param userRegistry the registry used to look up existing users and create the new one + */ + public LoginHandler(ResponseDispatcher responseDispatcher, UserRegistry userRegistry) { + this.responseDispatcher = responseDispatcher; + this.userRegistry = userRegistry; + this.userFactory = new UserFactory(userRegistry); + } + + /** + * Executes the login request + * + *

If no user is already assigned to the session, a new user is created and its name and + * {@link UserId} returned in the response. If the session has already a user assigned, a {@code + * ALREADY_LOGGED_IN} error is responded with. + * + * @param request the request to execute + */ + @Override + public void execute(LoginRequest request) { + Optional existingUser = userRegistry.getBySessionId(request.getSessionId()); + if (existingUser.isPresent()) { + responseDispatcher.dispatch( + new ErrorResponse( + request.getContext(), + "ALREADY_LOGGED_IN", + "This session is already associated with an active user.")); + return; + } + + User user = userFactory.create(request.getUsername(), request.getSessionId()); + responseDispatcher.dispatch( + new LoginResponse(request.getContext(), user.getName(), user.getId())); + } +} diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/login/LoginParser.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/login/LoginParser.java new file mode 100644 index 0000000..c3dd5db --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/login/LoginParser.java @@ -0,0 +1,21 @@ +package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.login; + +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 LoginRequest}. */ +public class LoginParser implements CommandParser { + /** + * Extracts the required {@code USERNAME} parameter from the incoming request. + * + * @param primitiveRequest the request to parse + * @return {@link LoginRequest} containing the username + */ + @Override + public LoginRequest parse(PrimitiveRequest primitiveRequest) { + RequestParameterAccessor accessor = + new RequestParameterAccessor(primitiveRequest.parameters()); + return new LoginRequest(primitiveRequest.context(), accessor.require("USERNAME")); + } +} diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/login/LoginRequest.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/login/LoginRequest.java new file mode 100644 index 0000000..9dec2be --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/login/LoginRequest.java @@ -0,0 +1,30 @@ +package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.login; + +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 create server-side user instance based on provided username */ +public class LoginRequest extends Request { + private final String username; + + /** + * Constructs a new LoginRequest with the given context and desired username + * + * @param context the {@link RequestContext} containing information for responding to the + * request + * @param username the desired username when creating the user + */ + public LoginRequest(RequestContext context, String username) { + super(context); + this.username = username; + } + + /** + * Returns the desired username requested for login + * + * @return the desired username + */ + public String getUsername() { + return username; + } +} diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/login/LoginResponse.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/login/LoginResponse.java new file mode 100644 index 0000000..b907fab --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/login/LoginResponse.java @@ -0,0 +1,23 @@ +package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.login; + +import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserId; +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 containing the assigned username and id of said user */ +public class LoginResponse extends SuccessResponse { + /** + * @param context the {@link RequestContext} associated with the request + * @param assignedUsername the assigned username to this user + * @param id of the created user + */ + public LoginResponse(RequestContext context, String assignedUsername, UserId id) { + super( + context, + new ResponseBodyBuilder() + .param("USERNAME", assignedUsername) + .param("ID", id.value()) + .build()); + } +} diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/logout/LogoutHandler.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/logout/LogoutHandler.java new file mode 100644 index 0000000..c5ca9fd --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/logout/LogoutHandler.java @@ -0,0 +1,45 @@ +package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.logout; + +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.ErrorResponse; +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; + +/** Handles {@link LogoutRequest} to logout connected user. */ +public class LogoutHandler implements CommandHandler { + private final ResponseDispatcher responseDispatcher; + private final UserRegistry userRegistry; + + /** + * Creates a new logout handler to logout user. + * + * @param responseDispatcher dispatcher used to send the logout result back to the client + * @param userRegistry registry responsible for tracking connected user sessions + */ + public LogoutHandler(ResponseDispatcher responseDispatcher, UserRegistry userRegistry) { + this.responseDispatcher = responseDispatcher; + this.userRegistry = userRegistry; + } + + /** + * Executes the logout request for the given request. + * + *

The user is removed from the {@link UserRegistry}. + * + * @param request the request to execute + */ + @Override + public void execute(LogoutRequest request) { + boolean wasRemoved = userRegistry.removeBySessionId(request.getSessionId()); + if (wasRemoved) { + responseDispatcher.dispatch(new OkResponse(request.getContext())); + } else { + responseDispatcher.dispatch( + new ErrorResponse( + request.getContext(), + "NO_USER_ASSOCIATED", + "No user is associated with your session. Did you login before?")); + } + } +} diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/logout/LogoutParser.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/logout/LogoutParser.java new file mode 100644 index 0000000..72cdf1c --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/logout/LogoutParser.java @@ -0,0 +1,19 @@ +package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.logout; + +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 LogoutRequest}. */ +public class LogoutParser implements CommandParser { + + /** + * Parses a primitive request into a LogoutRequest. + * + * @param primitiveRequest the request to parse + * @return the created {@link LogoutRequest} + */ + @Override + public LogoutRequest parse(PrimitiveRequest primitiveRequest) { + return new LogoutRequest(primitiveRequest.context()); + } +} diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/logout/LogoutRequest.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/logout/LogoutRequest.java new file mode 100644 index 0000000..77be0b4 --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/logout/LogoutRequest.java @@ -0,0 +1,18 @@ +package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.logout; + +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 logout a currently logged in user */ +public class LogoutRequest extends Request { + + /** + * Constructs a new LogoutRequest with the given context. + * + * @param context the {@link RequestContext} containing information for responding to the + * request + */ + public LogoutRequest(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 bc0cd36..b3cd551 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 @@ -31,6 +31,66 @@ public class UserRegistry { return Optional.of(user); } + /** + * Removes a user from the registry by user ID. + * + * @param userId the ID of the user to remove + * @return true if a user was removed, false if no user with that ID exists + */ + public synchronized boolean removeByUserId(UserId userId) { + User user = byId.get(userId); + if (user == null) { + return false; + } + + byName.remove(user.getName()); + user.getSessionId().ifPresent(bySessionId::remove); + return true; + } + + /** + * Removes a user from the registry by session ID. + * + * @param sessionId the session ID associated with the user to remove + * @return true if a user was removed, false if no user with that session exists + */ + public synchronized boolean removeBySessionId(SessionId sessionId) { + User user = bySessionId.get(sessionId); + if (user == null) { + return false; + } + + remove(user); + return true; + } + + /** + * Removes a user from the registry by username. + * + * @param username the username associated with the user to remove + * @return true if a user was removed, false if no user with that session exists + */ + public synchronized boolean removeByUsername(String username) { + User user = byName.get(username); + if (user == null) { + return false; + } + + remove(user); + return true; + } + + /** + * Removes a user from the internals of the registry. + * + * @param user the user to remove + */ + private synchronized void remove(User user) { + byId.remove(user.getId()); + byName.remove(user.getName()); + user.getSessionId().ifPresent(bySessionId::remove); + } + /** * Removes the user with the given ID, but only if they are still disconnected. This prevents * removing a user who has reconnected between the cleanup job's check and its removal call. @@ -47,8 +107,7 @@ public class UserRegistry { return false; } - byId.remove(user.getId()); - byName.remove(user.getName()); + remove(user); return true; }