Feat: Auto-assign playerN and add server-side rename support
Make username mutable, assign playerN for anonymous logins, and add atomic changeUsername in UserRegistry. Refs #68
This commit is contained in:
@@ -12,7 +12,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;
|
||||||
@@ -46,10 +46,21 @@ 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.
|
||||||
*
|
*
|
||||||
@@ -62,7 +73,8 @@ public class User {
|
|||||||
/**
|
/**
|
||||||
* Returns the time at which this user disconnected, if applicable.
|
* Returns the time at which this user disconnected, if applicable.
|
||||||
*
|
*
|
||||||
* @return an Optional containing the disconnect timestamp, or empty if connected
|
* @return an Optional containing the disconnect timestamp, or empty if
|
||||||
|
* connected
|
||||||
*/
|
*/
|
||||||
public Optional<Instant> getDisconnectedAt() {
|
public Optional<Instant> getDisconnectedAt() {
|
||||||
return Optional.ofNullable(disconnectedAt);
|
return Optional.ofNullable(disconnectedAt);
|
||||||
@@ -78,7 +90,10 @@ public class User {
|
|||||||
this.disconnectedAt = null;
|
this.disconnectedAt = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Marks this user as disconnected by clearing the session and recording the timestamp. */
|
/**
|
||||||
|
* Marks this user as disconnected by clearing the session and recording the
|
||||||
|
* timestamp.
|
||||||
|
*/
|
||||||
public void markDisconnected() {
|
public void markDisconnected() {
|
||||||
this.sessionId = null;
|
this.sessionId = null;
|
||||||
this.disconnectedAt = Instant.now();
|
this.disconnectedAt = Instant.now();
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
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;
|
||||||
|
|
||||||
/** 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);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new UserFactory backed by the given registry.
|
* Creates a new UserFactory backed by the given registry.
|
||||||
@@ -16,15 +18,30 @@ 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
|
||||||
* taken, a numeric suffix is appended and incremented until a free name is found (e.g.
|
* is already
|
||||||
* Lars_001, Lars_002, ...).
|
* taken, a numeric suffix is appended and incremented until a free name is
|
||||||
|
* found (e.g.
|
||||||
|
* 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 user
|
||||||
* @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()) {
|
||||||
|
return result.get();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var result = registry.registerIfAvailable(desiredName, sessionId);
|
var result = registry.registerIfAvailable(desiredName, sessionId);
|
||||||
if (result.isPresent()) {
|
if (result.isPresent()) {
|
||||||
return result.get();
|
return result.get();
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ public class UserRegistry {
|
|||||||
private final ConcurrentHashMap<SessionId, User> bySessionId = new ConcurrentHashMap<>();
|
private final ConcurrentHashMap<SessionId, User> bySessionId = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attempts to register a user under the given name atomically. Returns the registered user, or
|
* Attempts to register a user under the given name atomically. Returns the
|
||||||
|
* registered user, or
|
||||||
* empty if the name is already taken.
|
* empty if the name is already taken.
|
||||||
*
|
*
|
||||||
* @param name the desired display name
|
* @param name the desired display name
|
||||||
@@ -92,8 +93,10 @@ public class UserRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes the user with the given ID, but only if they are still disconnected. This prevents
|
* Removes the user with the given ID, but only if they are still disconnected.
|
||||||
* removing a user who has reconnected between the cleanup job's check and its removal call.
|
* This prevents
|
||||||
|
* removing a user who has reconnected between the cleanup job's check and its
|
||||||
|
* removal call.
|
||||||
*
|
*
|
||||||
* @param userId the ID of the user to remove
|
* @param userId the ID of the user to remove
|
||||||
*/
|
*/
|
||||||
@@ -112,7 +115,8 @@ public class UserRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Marks the user associated with the given session as disconnected, clearing the session
|
* Marks the user associated with the given session as disconnected, clearing
|
||||||
|
* the session
|
||||||
* association and recording the disconnect timestamp.
|
* association and recording the disconnect timestamp.
|
||||||
*
|
*
|
||||||
* @param sessionId the session ID of the disconnected client
|
* @param sessionId the session ID of the disconnected client
|
||||||
@@ -127,7 +131,8 @@ public class UserRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reassociates a user with a new session, effectively restoring them after a reconnect.
|
* Reassociates a user with a new session, effectively restoring them after a
|
||||||
|
* reconnect.
|
||||||
*
|
*
|
||||||
* @param userId the ID of the user to reconnect
|
* @param userId the ID of the user to reconnect
|
||||||
* @param sessionId the new session ID
|
* @param sessionId the new session ID
|
||||||
@@ -148,7 +153,8 @@ public class UserRegistry {
|
|||||||
* Looks up a user by their {@link SessionId}.
|
* Looks up a user by their {@link SessionId}.
|
||||||
*
|
*
|
||||||
* @param sessionId the SessionId to look up
|
* @param sessionId the SessionId to look up
|
||||||
* @return an Optional containing the {@link User}, or empty if no user is associated with this
|
* @return an Optional containing the {@link User}, or empty if no user is
|
||||||
|
* associated with this
|
||||||
* SessionId
|
* SessionId
|
||||||
*/
|
*/
|
||||||
public Optional<User> getBySessionId(SessionId sessionId) {
|
public Optional<User> getBySessionId(SessionId sessionId) {
|
||||||
@@ -159,7 +165,8 @@ public class UserRegistry {
|
|||||||
* Looks up a user by their {@link UserId}.
|
* Looks up a user by their {@link UserId}.
|
||||||
*
|
*
|
||||||
* @param userId the UserId to look up
|
* @param userId the UserId to look up
|
||||||
* @return an Optional containing the {@link User}, or empty if no user is associated with this
|
* @return an Optional containing the {@link User}, or empty if no user is
|
||||||
|
* associated with this
|
||||||
* UserId
|
* UserId
|
||||||
*/
|
*/
|
||||||
public Optional<User> getByUserId(UserId userId) {
|
public Optional<User> getByUserId(UserId userId) {
|
||||||
@@ -170,7 +177,8 @@ public class UserRegistry {
|
|||||||
* Looks up a user by their username.
|
* Looks up a user by their username.
|
||||||
*
|
*
|
||||||
* @param username the username to look up
|
* @param username the username to look up
|
||||||
* @return an Optional containing the {@link User}, or empty if no user is associated with this
|
* @return an Optional containing the {@link User}, or empty if no user is
|
||||||
|
* associated with this
|
||||||
* username
|
* username
|
||||||
*/
|
*/
|
||||||
public Optional<User> getByUsername(String username) {
|
public Optional<User> getByUsername(String username) {
|
||||||
@@ -185,4 +193,31 @@ 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user