Feat: accept optional client username and forward to ClientApp #271
@@ -7,6 +7,12 @@ import org.apache.logging.log4j.Logger;
|
|||||||
|
|
||||||
/** Main entry point for Casono application. Handles client and server startup. */
|
/** Main entry point for Casono application. Handles client and server startup. */
|
||||||
public final class Main {
|
public final class Main {
|
||||||
|
|
||||||
|
private static final int MIN_ARGS_FOR_USERNAME = 3;
|
||||||
|
private static final int ARGS_COUNT_SERVER = 2;
|
||||||
|
private static final int ARGS_COUNT_CLIENT_MIN = 2;
|
||||||
|
private static final int ARGS_COUNT_CLIENT_WITH_USER = 3;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main entry point for Casono.
|
* Main entry point for Casono.
|
||||||
*
|
*
|
||||||
@@ -17,10 +23,13 @@ public final class Main {
|
|||||||
printUsage();
|
printUsage();
|
||||||
System.exit(1);
|
System.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (args[0]) {
|
switch (args[0]) {
|
||||||
case "server" -> ServerApp.start(args[1]);
|
case "server" -> ServerApp.start(args[1]);
|
||||||
case "client" -> ClientApp.start(args[1]);
|
case "client" -> {
|
||||||
|
String address = args[1];
|
||||||
|
String username = args.length >= MIN_ARGS_FOR_USERNAME ? args[2] : null;
|
||||||
|
ClientApp.start(address, username);
|
||||||
|
}
|
||||||
default -> {
|
default -> {
|
||||||
printUsage();
|
printUsage();
|
||||||
System.exit(1);
|
System.exit(1);
|
||||||
@@ -29,12 +38,15 @@ public final class Main {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static boolean isValid(String[] args) {
|
private static boolean isValid(String[] args) {
|
||||||
if (args.length != 2) {
|
if (args.length < ARGS_COUNT_SERVER) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return switch (args[0]) {
|
return switch (args[0]) {
|
||||||
case "server", "client" -> true;
|
case "server" -> args.length == ARGS_COUNT_SERVER;
|
||||||
|
case "client" ->
|
||||||
|
args.length == ARGS_COUNT_CLIENT_MIN
|
||||||
|
|| args.length == ARGS_COUNT_CLIENT_WITH_USER;
|
||||||
default -> false;
|
default -> false;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -45,7 +57,7 @@ public final class Main {
|
|||||||
"""
|
"""
|
||||||
Usage:
|
Usage:
|
||||||
java -jar xyz.jar server <listenPort>
|
java -jar xyz.jar server <listenPort>
|
||||||
java -jar xyz.jar client <serverIp>:<serverPort>
|
java -jar xyz.jar client <serverIp>:<serverPort> [<username>]
|
||||||
""");
|
""");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,33 +1,51 @@
|
|||||||
package ch.unibas.dmi.dbis.cs108.casono.client;
|
package ch.unibas.dmi.dbis.cs108.casono.client;
|
||||||
|
|
||||||
/**
|
import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService;
|
||||||
* Entry point for the Casono client application. Handles client startup and connection parameters.
|
import ch.unibas.dmi.dbis.cs108.casono.client.network.LobbyClient;
|
||||||
*/
|
import ch.unibas.dmi.dbis.cs108.casono.client.network.LoginResult;
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.client.ui.Launcher;
|
import ch.unibas.dmi.dbis.cs108.casono.client.ui.Launcher;
|
||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Entry point for the Casono client application. Handles client startup and connection parameters.
|
* Entry point and bootstrap helper for the Casono client application.
|
||||||
*
|
*
|
||||||
* <p>Default constructor for the application.
|
* <p>This class is responsible for two tasks: - performing an optional startup LOGIN when a
|
||||||
|
* username is supplied on the command line, and - providing a shared {@link ClientService} instance
|
||||||
|
* that the UI can reuse so the initial connection remains active.
|
||||||
*/
|
*/
|
||||||
public class ClientApp {
|
public class ClientApp {
|
||||||
|
|
||||||
private static final Logger LOGGER = LogManager.getLogger(ClientApp.class);
|
private static final Logger LOGGER = LogManager.getLogger(ClientApp.class);
|
||||||
|
|
||||||
|
/** Shared client connection used when a username is provided at startup. */
|
||||||
|
private static volatile ClientService sharedClientService;
|
||||||
|
|
||||||
/** Default constructor. */
|
/** Default constructor. */
|
||||||
public ClientApp() {
|
public ClientApp() {
|
||||||
// Default constructor
|
// Default constructor
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the shared {@link ClientService} instance created during startup, or {@code null} if
|
||||||
|
* none exists. The UI may call this to reuse the connection that already performed the initial
|
||||||
|
* LOGIN.
|
||||||
|
*/
|
||||||
|
public static ClientService getSharedClientService() {
|
||||||
|
return sharedClientService;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void setSharedClientService(ClientService cs) {
|
||||||
|
sharedClientService = cs;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts the client application with the given address.
|
* Starts the client application with the given address.
|
||||||
*
|
*
|
||||||
* @param arg Address in the format "ip:port".
|
* @param arg Address in the format "ip:port".
|
||||||
* @throws IllegalArgumentException if the address format is invalid.
|
* @throws IllegalArgumentException if the address format is invalid.
|
||||||
*/
|
*/
|
||||||
public static void start(String arg) {
|
public static void start(String arg, String username) {
|
||||||
String[] parts = arg.split(":", 2);
|
String[] parts = arg.split(":", 2);
|
||||||
if (parts.length != 2) {
|
if (parts.length != 2) {
|
||||||
throw new IllegalArgumentException("Address must be in format <ip>:<port>.");
|
throw new IllegalArgumentException("Address must be in format <ip>:<port>.");
|
||||||
@@ -36,16 +54,69 @@ public class ClientApp {
|
|||||||
int port = Integer.parseInt(parts[1]);
|
int port = Integer.parseInt(parts[1]);
|
||||||
|
|
||||||
LOGGER.info("You've selected the client. It will connect port {} at host {}", port, host);
|
LOGGER.info("You've selected the client. It will connect port {} at host {}", port, host);
|
||||||
// Expose the chosen host/port to the UI via system properties so controllers
|
|
||||||
// (which read System.getProperty("casono.server.host"/"casono.server.port"))
|
|
||||||
// can obtain the correct connection information.
|
|
||||||
System.setProperty("casono.server.host", host);
|
System.setProperty("casono.server.host", host);
|
||||||
System.setProperty("casono.server.port", Integer.toString(port));
|
System.setProperty("casono.server.port", String.valueOf(port));
|
||||||
// Forward the original address argument to the launcher as well.
|
// If a username was provided, create a shared connection and perform
|
||||||
Launcher.main(new String[] {arg});
|
// the initial LOGIN asynchronously on that connection. The connection
|
||||||
|
// is intentionally NOT closed so the UI can reuse it.
|
||||||
|
if (username != null && !username.isBlank()) {
|
||||||
|
try {
|
||||||
|
ClientService clientService = new ClientService(host, port);
|
||||||
|
setSharedClientService(clientService);
|
||||||
|
|
||||||
|
java.util.concurrent.ExecutorService bg =
|
||||||
|
java.util.concurrent.Executors.newSingleThreadExecutor(
|
||||||
|
r -> {
|
||||||
|
Thread t = new Thread(r);
|
||||||
|
t.setDaemon(true);
|
||||||
|
t.setName("casono-startup-login");
|
||||||
|
return t;
|
||||||
|
});
|
||||||
|
bg.submit(
|
||||||
|
() -> {
|
||||||
|
try {
|
||||||
|
LobbyClient lobbyClient = new LobbyClient(clientService);
|
||||||
|
LoginResult res = lobbyClient.login(username);
|
||||||
|
LOGGER.info(
|
||||||
|
"Assigned username='{}' id={}",
|
||||||
|
res.getUsername(),
|
||||||
|
res.getId());
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
LOGGER.warn("Startup login failed: {}", e.getMessage());
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOGGER.warn("Unexpected error during startup login", e);
|
||||||
|
} finally {
|
||||||
|
bg.shutdown();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
LOGGER.warn(
|
||||||
|
"Could not establish initial connection for startup login: {}",
|
||||||
|
e.getMessage());
|
||||||
|
// UI will create its own connection when it initializes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (username != null && !username.isBlank()) {
|
||||||
|
Launcher.main(new String[] {arg, username});
|
||||||
|
} else {
|
||||||
|
Launcher.main(new String[] {arg});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main entry point. {@code <host:port>} (e.g. {@code 127.0.0.1:1234})
|
||||||
|
*
|
||||||
|
* <p>If a username is provided the application attempts a startup LOGIN on the shared
|
||||||
|
* connection; the UI will still start and will reuse the connection when possible. The username
|
||||||
|
* is not propagated via a system property for UI display.
|
||||||
|
*/
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
start(args[0]);
|
if (args == null || args.length == 0) {
|
||||||
|
throw new IllegalArgumentException("Address argument required: <host:port>");
|
||||||
|
}
|
||||||
|
// Optional username argument
|
||||||
|
String username = args.length > 1 && args[1] != null && !args[1].isBlank() ? args[1] : null;
|
||||||
|
start(args[0], username);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -138,33 +138,64 @@ public class ClientService {
|
|||||||
() -> {
|
() -> {
|
||||||
try {
|
try {
|
||||||
writeToTransport(message);
|
writeToTransport(message);
|
||||||
String responseText = null;
|
String responseText = clienttcptransport.read().payload();
|
||||||
|
logger.info("Raw message '{}'", responseText);
|
||||||
|
|
||||||
responseText = clienttcptransport.read().payload();
|
boolean hasStatus = false;
|
||||||
logger.info("Raw message '" + responseText + "'");
|
boolean success = false;
|
||||||
Boolean success = null;
|
for (String rawLine : responseText.split("\n")) {
|
||||||
int count = 0;
|
String line = rawLine;
|
||||||
for (String line : responseText.split("\n")) {
|
if (!hasStatus) {
|
||||||
if (success == null) {
|
|
||||||
if ("+OK".equals(line)) {
|
if ("+OK".equals(line)) {
|
||||||
success = true;
|
success = true;
|
||||||
|
hasStatus = true;
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
} else if (("-ERROR").equals(responseText)) {
|
|
||||||
success = false;
|
|
||||||
}
|
}
|
||||||
|
if (line.startsWith("-ERR") || line.startsWith("-ERROR")) {
|
||||||
|
success = false;
|
||||||
|
hasStatus = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// ignore any lines before the status indicator
|
||||||
continue;
|
continue;
|
||||||
} else if ("END".equals(line)) {
|
}
|
||||||
|
|
||||||
|
if ("END".equals(line)) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
line = line.replaceFirst("^\t", "");
|
|
||||||
|
// strip a single leading tab if present (protocol formatting)
|
||||||
|
if (line.startsWith("\t")) {
|
||||||
|
line = line.substring(1);
|
||||||
|
}
|
||||||
response.add(line);
|
response.add(line);
|
||||||
}
|
}
|
||||||
if (success != null && success) {
|
|
||||||
return;
|
if (!hasStatus) {
|
||||||
} else {
|
// Fallback for servers that do not place the status on a
|
||||||
throw new RuntimeException("Error in " + message + ": " + response);
|
// dedicated line: try to detect status markers anywhere
|
||||||
|
// in the payload to remain compatible with older servers.
|
||||||
|
if (responseText.contains("+OK")) {
|
||||||
|
success = true;
|
||||||
|
hasStatus = true;
|
||||||
|
} else if (responseText.contains("-ERR")
|
||||||
|
|| responseText.contains("-ERROR")) {
|
||||||
|
success = false;
|
||||||
|
hasStatus = true;
|
||||||
|
} else {
|
||||||
|
throw new RuntimeException(
|
||||||
|
"No status line in response for '"
|
||||||
|
+ message
|
||||||
|
+ "': "
|
||||||
|
+ responseText);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new RuntimeException("Error in " + message + ": " + response);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw getRuntimeException(e);
|
throw getRuntimeException(e);
|
||||||
}
|
}
|
||||||
@@ -200,15 +231,14 @@ public class ClientService {
|
|||||||
* @return A RuntimeException representing the cause of the original exception.
|
* @return A RuntimeException representing the cause of the original exception.
|
||||||
*/
|
*/
|
||||||
private static RuntimeException getRuntimeException(Exception e) {
|
private static RuntimeException getRuntimeException(Exception e) {
|
||||||
Throwable reason = e.getCause();
|
Throwable cause = e.getCause();
|
||||||
RuntimeException re;
|
if (cause == null) {
|
||||||
if (reason == null) {
|
return e instanceof RuntimeException ? (RuntimeException) e : new RuntimeException(e);
|
||||||
reason = e;
|
|
||||||
} else if (reason instanceof RuntimeException rte) {
|
|
||||||
re = rte;
|
|
||||||
}
|
}
|
||||||
re = new RuntimeException(reason);
|
if (cause instanceof RuntimeException) {
|
||||||
return re;
|
return (RuntimeException) cause;
|
||||||
|
}
|
||||||
|
return new RuntimeException(cause);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
package ch.unibas.dmi.dbis.cs108.casono.client.network;
|
package ch.unibas.dmi.dbis.cs108.casono.client.network;
|
||||||
|
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.RequestParameter;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The LobbyClient class is responsible for communicating with the server to manage game lobbies. It
|
* The LobbyClient class is responsible for communicating with the server to manage game lobbies. It
|
||||||
* provides methods to create a lobby, join a lobby, and fetch the current status of a lobby by
|
* provides methods to create a lobby, join a lobby, and fetch the current status of a lobby by
|
||||||
@@ -65,8 +68,23 @@ public class LobbyClient {
|
|||||||
* Logs in to the server with the given username by sending a "LOGIN" command.
|
* Logs in to the server with the given username by sending a "LOGIN" command.
|
||||||
*
|
*
|
||||||
* @param user The username to log in with.
|
* @param user The username to log in with.
|
||||||
|
* @return a {@link LoginResult} containing the assigned username and id as returned by the
|
||||||
|
* server
|
||||||
*/
|
*/
|
||||||
public void login(String user) {
|
public LoginResult login(String user) {
|
||||||
client.processCommand("LOGIN USERNAME=" + user);
|
List<String> lines = client.processCommand("LOGIN USERNAME=" + user);
|
||||||
|
|
||||||
|
List<RequestParameter> params = ClientService.convertToRequestParameters(lines);
|
||||||
|
|
||||||
|
String assigned = user;
|
||||||
|
String id = null;
|
||||||
|
for (RequestParameter p : params) {
|
||||||
|
if ("USERNAME".equalsIgnoreCase(p.key())) {
|
||||||
|
assigned = p.value();
|
||||||
|
} else if ("ID".equalsIgnoreCase(p.key())) {
|
||||||
|
id = p.value();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new LoginResult(assigned, id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.client.network;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Result of a successful login request.
|
||||||
|
*
|
||||||
|
* <p>Holds the username assigned by the server and the server-side id as a string (UUID). The
|
||||||
|
* client previously attempted to parse the id as an integer which failed when the server returned a
|
||||||
|
* UUID; this class therefore stores the id as a {@link String}.
|
||||||
|
*/
|
||||||
|
public class LoginResult {
|
||||||
|
private final String username;
|
||||||
|
private final String id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a login result.
|
||||||
|
*
|
||||||
|
* @param username the assigned username
|
||||||
|
* @param id the assigned id (UUID string) returned by the server
|
||||||
|
*/
|
||||||
|
public LoginResult(String username, String id) {
|
||||||
|
this.username = username;
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the assigned username
|
||||||
|
*/
|
||||||
|
public String getUsername() {
|
||||||
|
return username;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the assigned id as returned by the server (UUID string)
|
||||||
|
*/
|
||||||
|
public String getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
}
|
||||||
+24
-13
@@ -1,5 +1,6 @@
|
|||||||
package ch.unibas.dmi.dbis.cs108.casono.client.ui.lobbyui;
|
package ch.unibas.dmi.dbis.cs108.casono.client.ui.lobbyui;
|
||||||
|
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.client.ClientApp;
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService;
|
import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService;
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.client.network.LobbyClient;
|
import ch.unibas.dmi.dbis.cs108.casono.client.network.LobbyClient;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
@@ -36,12 +37,16 @@ public class CasinomainuiController {
|
|||||||
private int nextButtonId = 1;
|
private int nextButtonId = 1;
|
||||||
private LobbyClient lobbyClient;
|
private LobbyClient lobbyClient;
|
||||||
|
|
||||||
/** Default constructor for dependency injection by FXMLLoader. */
|
/** Default constructor used by FXMLLoader. */
|
||||||
public CasinomainuiController() {
|
public CasinomainuiController() {
|
||||||
// Default constructor
|
// Default constructor
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Initializes the UI components and sets default values. */
|
/**
|
||||||
|
* Initializes the UI components and sets default values. If a shared {@link
|
||||||
|
* ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService} exists (created at application
|
||||||
|
* start), the controller reuses it so the connection remains open and already-logged-in.
|
||||||
|
*/
|
||||||
@FXML
|
@FXML
|
||||||
public void initialize() {
|
public void initialize() {
|
||||||
titleLabel.setText("Casono");
|
titleLabel.setText("Casono");
|
||||||
@@ -51,16 +56,18 @@ public class CasinomainuiController {
|
|||||||
translationManager = LobbyButtonTranslationManager.getInstance();
|
translationManager = LobbyButtonTranslationManager.getInstance();
|
||||||
String host = System.getProperty("casono.server.host");
|
String host = System.getProperty("casono.server.host");
|
||||||
int port = Integer.parseInt(System.getProperty("casono.server.port"));
|
int port = Integer.parseInt(System.getProperty("casono.server.port"));
|
||||||
ClientService clientService;
|
ClientService clientService = ClientApp.getSharedClientService();
|
||||||
try {
|
if (clientService == null) {
|
||||||
clientService = new ClientService(host, port);
|
try {
|
||||||
} catch (RuntimeException e) {
|
clientService = new ClientService(host, port);
|
||||||
LOGGER.warn(
|
} catch (RuntimeException e) {
|
||||||
"Could not connect to server {}:{} — starting in offline mode: {}",
|
LOGGER.warn(
|
||||||
host,
|
"Could not connect to server {}:{} — starting in offline mode: {}",
|
||||||
port,
|
host,
|
||||||
e.getMessage());
|
port,
|
||||||
clientService = new ClientService(true); // offline mode
|
e.getMessage());
|
||||||
|
clientService = new ClientService(true); // offline mode
|
||||||
|
}
|
||||||
}
|
}
|
||||||
gridManager =
|
gridManager =
|
||||||
new LobbyButtonGridManager(
|
new LobbyButtonGridManager(
|
||||||
@@ -117,7 +124,11 @@ public class CasinomainuiController {
|
|||||||
Platform.exit();
|
Platform.exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Handles creation of a new lobby button. */
|
/**
|
||||||
|
* Handles creation of a new lobby button. Attempts to create a lobby on the server via the
|
||||||
|
* {@link LobbyButtonGridManager} and registers the new button in the local translation manager.
|
||||||
|
* Errors are logged and displayed as an informational alert.
|
||||||
|
*/
|
||||||
@FXML
|
@FXML
|
||||||
public void handleCreateLobbyButton() {
|
public void handleCreateLobbyButton() {
|
||||||
if (translationManager.isFull()) {
|
if (translationManager.isFull()) {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import java.util.concurrent.ConcurrentLinkedQueue;
|
|||||||
/** Represents an authenticated user on the server. */
|
/** Represents an authenticated user on the server. */
|
||||||
public class User {
|
public class User {
|
||||||
private final UserId id;
|
private final UserId id;
|
||||||
private final String name;
|
private String name;
|
||||||
private SessionId sessionId;
|
private SessionId sessionId;
|
||||||
private Instant disconnectedAt;
|
private Instant disconnectedAt;
|
||||||
private final Queue<Message> messages;
|
private final Queue<Message> messages;
|
||||||
@@ -47,10 +47,20 @@ public class User {
|
|||||||
*
|
*
|
||||||
* @return the user name
|
* @return the user name
|
||||||
*/
|
*/
|
||||||
public String getName() {
|
public synchronized String getName() {
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a new display name for this user. Thread-safe; callers must ensure the registry is
|
||||||
|
* updated to maintain uniqueness when needed.
|
||||||
|
*
|
||||||
|
* @param newName the new display name
|
||||||
|
*/
|
||||||
|
public synchronized void setName(String newName) {
|
||||||
|
this.name = newName;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the session currently associated with this user, if any.
|
* Returns the session currently associated with this user, if any.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
package ch.unibas.dmi.dbis.cs108.casono.server.domain.user;
|
package ch.unibas.dmi.dbis.cs108.casono.server.domain.user;
|
||||||
|
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionId;
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionId;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
|
||||||
/** Creates new users, resolving name conflicts automatically. */
|
/** Creates new users, resolving name conflicts automatically. */
|
||||||
public class UserFactory {
|
public class UserFactory {
|
||||||
private final UserRegistry registry;
|
private final UserRegistry registry;
|
||||||
|
private final AtomicInteger anonymousCounter = new AtomicInteger(1);
|
||||||
|
private static final Logger LOGGER = LogManager.getLogger(UserFactory.class);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new UserFactory backed by the given registry.
|
* Creates a new UserFactory backed by the given registry.
|
||||||
@@ -18,16 +23,40 @@ public class UserFactory {
|
|||||||
/**
|
/**
|
||||||
* Creates and registers a new user with the given name and session. If the name is already
|
* Creates and registers a new user with the given name and session. If the name is already
|
||||||
* taken, a numeric suffix is appended and incremented until a free name is found (e.g.
|
* taken, a numeric suffix is appended and incremented until a free name is found (e.g.
|
||||||
* Lars_001, Lars_002, ...).
|
* Lars_001, Lars_002, ...). If the desiredName is null or empty, an automatic name of the form
|
||||||
|
* "playerN" is assigned, incrementing N until a free name is found.
|
||||||
*
|
*
|
||||||
* @param desiredName the preferred display name
|
* @param desiredName the preferred display name
|
||||||
* @param sessionId the session to associate with the new user
|
* @param sessionId the session to associate with the new userwas
|
||||||
* @return the newly created and registered user
|
* @return the newly created and registered user
|
||||||
*/
|
*/
|
||||||
public User create(String desiredName, SessionId sessionId) {
|
public User create(String desiredName, SessionId sessionId) {
|
||||||
|
if (desiredName == null || desiredName.isBlank()) {
|
||||||
|
// assign playerN names for anonymous logins
|
||||||
|
while (true) {
|
||||||
|
String candidate = "player" + anonymousCounter.getAndIncrement();
|
||||||
|
var result = registry.registerIfAvailable(candidate, sessionId);
|
||||||
|
if (result.isPresent()) {
|
||||||
|
var user = result.get();
|
||||||
|
LOGGER.info(
|
||||||
|
"Registered user '{}' with id {} for session {}",
|
||||||
|
user.getName(),
|
||||||
|
user.getId().value(),
|
||||||
|
sessionId == null ? "null" : sessionId.value());
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var result = registry.registerIfAvailable(desiredName, sessionId);
|
var result = registry.registerIfAvailable(desiredName, sessionId);
|
||||||
if (result.isPresent()) {
|
if (result.isPresent()) {
|
||||||
return result.get();
|
var user = result.get();
|
||||||
|
LOGGER.info(
|
||||||
|
"Registered user '{}' with id {} for session {}",
|
||||||
|
user.getName(),
|
||||||
|
user.getId().value(),
|
||||||
|
sessionId == null ? "null" : sessionId.value());
|
||||||
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
int suffix = 1;
|
int suffix = 1;
|
||||||
@@ -35,7 +64,13 @@ public class UserFactory {
|
|||||||
String candidate = desiredName + "_" + String.format("%03d", suffix);
|
String candidate = desiredName + "_" + String.format("%03d", suffix);
|
||||||
result = registry.registerIfAvailable(candidate, sessionId);
|
result = registry.registerIfAvailable(candidate, sessionId);
|
||||||
if (result.isPresent()) {
|
if (result.isPresent()) {
|
||||||
return result.get();
|
var user = result.get();
|
||||||
|
LOGGER.info(
|
||||||
|
"Registered user '{}' with id {} for session {}",
|
||||||
|
user.getName(),
|
||||||
|
user.getId().value(),
|
||||||
|
sessionId == null ? "null" : sessionId.value());
|
||||||
|
return user;
|
||||||
}
|
}
|
||||||
suffix++;
|
suffix++;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -185,4 +185,29 @@ public class UserRegistry {
|
|||||||
public Collection<User> getAllUsers() {
|
public Collection<User> getAllUsers() {
|
||||||
return byId.values();
|
return byId.values();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to change the username of an existing user. Ensures the new name is not already
|
||||||
|
* taken and updates internal indices atomically.
|
||||||
|
*
|
||||||
|
* @param userId the id of the user to rename
|
||||||
|
* @param newName the desired new name
|
||||||
|
* @return true if the rename succeeded, false if the name was already taken or user not found
|
||||||
|
*/
|
||||||
|
public synchronized boolean changeUsername(UserId userId, String newName) {
|
||||||
|
User user = byId.get(userId);
|
||||||
|
if (user == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (byName.containsKey(newName)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove old mapping and put new mapping
|
||||||
|
byName.remove(user.getName());
|
||||||
|
user.setName(newName);
|
||||||
|
byName.put(newName, user);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.domain.user;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionId;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
/** Tests for {@link UserFactory} and {@link UserRegistry} name assignment behavior. */
|
||||||
|
public class UserFactoryTest {
|
||||||
|
|
||||||
|
// Logger removed as per patch requirement
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void createUsersWithDuplicateDesiredNamesAssignsUniqueNames() {
|
||||||
|
UserRegistry registry = new UserRegistry();
|
||||||
|
UserFactory factory = new UserFactory(registry);
|
||||||
|
|
||||||
|
SessionId s1 = new SessionId();
|
||||||
|
SessionId s2 = new SessionId();
|
||||||
|
SessionId s3 = new SessionId();
|
||||||
|
|
||||||
|
// Create first user with desired name "Alice"
|
||||||
|
User u1 = factory.create("Alice", s1);
|
||||||
|
// Create second user requesting the same name
|
||||||
|
User u2 = factory.create("Alice", s2);
|
||||||
|
// Create third user requesting the same name again
|
||||||
|
User u3 = factory.create("Alice", s3);
|
||||||
|
|
||||||
|
assertNotNull(u1);
|
||||||
|
assertNotNull(u2);
|
||||||
|
assertNotNull(u3);
|
||||||
|
|
||||||
|
// All assigned names must be distinct
|
||||||
|
String n1 = u1.getName();
|
||||||
|
String n2 = u2.getName();
|
||||||
|
String n3 = u3.getName();
|
||||||
|
|
||||||
|
// assigned names: n1, n2, n3
|
||||||
|
|
||||||
|
assertNotEquals(n1, n2);
|
||||||
|
assertNotEquals(n1, n3);
|
||||||
|
assertNotEquals(n2, n3);
|
||||||
|
|
||||||
|
// Registry should contain all assigned names
|
||||||
|
assertTrue(registry.getByUsername(n1).isPresent());
|
||||||
|
assertTrue(registry.getByUsername(n2).isPresent());
|
||||||
|
assertTrue(registry.getByUsername(n3).isPresent());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void createUserWithNullDesiredAssignsPlayerN() {
|
||||||
|
UserRegistry registry = new UserRegistry();
|
||||||
|
UserFactory factory = new UserFactory(registry);
|
||||||
|
|
||||||
|
SessionId s1 = new SessionId();
|
||||||
|
|
||||||
|
User u = factory.create(null, s1);
|
||||||
|
assertNotNull(u);
|
||||||
|
String name = u.getName();
|
||||||
|
// assigned anonymous name: name
|
||||||
|
assertTrue(name.startsWith("player"), "expected automatic name starting with 'player'");
|
||||||
|
assertTrue(registry.getByUsername(name).isPresent());
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user