add username change and check for duplicates

This commit is contained in:
Arnaud Vergnet 2021-01-03 18:19:02 +01:00
parent 060137115f
commit 030e9b3b0a
8 changed files with 194 additions and 77 deletions

View file

@ -0,0 +1,9 @@
package fr.insa.clavardator.errors;
import java.io.Serializable;
public class UsernameTakenException extends Exception implements Serializable {
public UsernameTakenException(String message) {
super(message);
}
}

View file

@ -19,6 +19,8 @@ import java.io.IOException;
import java.net.SocketException; import java.net.SocketException;
import java.net.URL; import java.net.URL;
import java.util.ResourceBundle; import java.util.ResourceBundle;
import java.util.Timer;
import java.util.TimerTask;
public class MainController implements Initializable { public class MainController implements Initializable {
@ -58,8 +60,13 @@ public class MainController implements Initializable {
private void onCurrentUserStateChange(CurrentUser.State newState) { private void onCurrentUserStateChange(CurrentUser.State newState) {
if (newState == CurrentUser.State.VALID) if (newState == CurrentUser.State.VALID)
startChat(); startChat();
else else {
Platform.runLater(() -> showLogin(newState == CurrentUser.State.INVALID)); final boolean isError = newState == CurrentUser.State.INVALID;
if (isError) {
endChat();
}
Platform.runLater(() -> showLogin(isError));
}
} }
private void openEditUsernameDialog(EditUsernameDialogController.Mode mode) { private void openEditUsernameDialog(EditUsernameDialogController.Mode mode) {
@ -72,11 +79,11 @@ public class MainController implements Initializable {
} }
}); });
editUserDialogController.setOnSuccessListener(() -> { editUserDialogController.setOnSuccessListener(() -> {
if (!initialMode) { if (mode == EditUsernameDialogController.Mode.EDIT) {
showSnackbarEvent("Nom d'utilisateur changé !", SnackbarController.Mode.SUCCESS); showSnackbarEvent("Nom d'utilisateur changé !", SnackbarController.Mode.SUCCESS);
} }
}); });
editUserDialogController.show(root, mode); Platform.runLater(() -> editUserDialogController.show(root, mode));
} }
private void showSnackbarEvent(String text, SnackbarController.Mode mode) { private void showSnackbarEvent(String text, SnackbarController.Mode mode) {
@ -99,15 +106,26 @@ public class MainController implements Initializable {
private void discoverActiveUsers() { private void discoverActiveUsers() {
if (userList != null) { if (userList != null) {
userList.discoverActiveUsers((e) -> CurrentUser.getInstance().setState(CurrentUser.State.INVALID)); userList.discoverActiveUsers((e) -> {
Log.e(this.getClass().getSimpleName(), "Error discovering users", e);
CurrentUser.getInstance().setState(CurrentUser.State.INVALID);
});
} }
} }
private void startListening() { private void startListening() {
if (userList != null) { if (userList != null) {
userList.startDiscoveryListening(); userList.startDiscoveryListening();
userList.startUserListening((e) -> userList.startUserListening((e) -> {
Log.e(this.getClass().getSimpleName(), "Error listening to users", e)); Log.e(this.getClass().getSimpleName(), "Error listening to users", e);
CurrentUser.getInstance().setState(CurrentUser.State.INVALID);
});
}
}
private void stopListening() {
if (userList != null) {
userList.destroy();
} }
} }
@ -118,6 +136,11 @@ public class MainController implements Initializable {
Platform.runLater(this::showChat); Platform.runLater(this::showChat);
} }
private void endChat() {
Log.v(this.getClass().getSimpleName(), "Chat ended");
stopListening();
}
private void showChat() { private void showChat() {
Log.v(this.getClass().getSimpleName(), "Chat shown"); Log.v(this.getClass().getSimpleName(), "Chat shown");
loadingController.hide(); loadingController.hide();
@ -128,7 +151,14 @@ public class MainController implements Initializable {
Log.v(this.getClass().getSimpleName(), "Login shown"); Log.v(this.getClass().getSimpleName(), "Login shown");
mainContainer.setVisible(false); mainContainer.setVisible(false);
loadingController.hide(); loadingController.hide();
Timer t = new Timer();
t.schedule(new TimerTask() {
@Override
public void run() {
openEditUsernameDialog(isError ? EditUsernameDialogController.Mode.ERROR : EditUsernameDialogController.Mode.INITIAL); openEditUsernameDialog(isError ? EditUsernameDialogController.Mode.ERROR : EditUsernameDialogController.Mode.INITIAL);
t.cancel();
}
}, 500);
} }
private void showError() { private void showError() {
@ -162,5 +192,6 @@ public class MainController implements Initializable {
this.userList = userList; this.userList = userList;
listController.setUserList(userList); listController.setUserList(userList);
listController.setRefreshUserListener(this::discoverActiveUsers); listController.setRefreshUserListener(this::discoverActiveUsers);
editUserDialogController.setUserList(userList);
} }
} }

View file

@ -6,6 +6,7 @@ import com.jfoenix.controls.JFXTextField;
import com.jfoenix.validation.base.ValidatorBase; import com.jfoenix.validation.base.ValidatorBase;
import fr.insa.clavardator.ui.ButtonPressEvent; import fr.insa.clavardator.ui.ButtonPressEvent;
import fr.insa.clavardator.users.CurrentUser; import fr.insa.clavardator.users.CurrentUser;
import fr.insa.clavardator.users.UserList;
import javafx.beans.property.ReadOnlyBooleanWrapper; import javafx.beans.property.ReadOnlyBooleanWrapper;
import javafx.beans.value.ObservableValue; import javafx.beans.value.ObservableValue;
import javafx.fxml.FXML; import javafx.fxml.FXML;
@ -16,8 +17,6 @@ import javafx.scene.layout.StackPane;
import java.net.URL; import java.net.URL;
import java.util.ResourceBundle; import java.util.ResourceBundle;
import java.util.Timer;
import java.util.TimerTask;
public class EditUsernameDialogController implements Initializable { public class EditUsernameDialogController implements Initializable {
@ -44,21 +43,16 @@ public class EditUsernameDialogController implements Initializable {
private Validator validator; private Validator validator;
private ButtonPressEvent successListener; private ButtonPressEvent successListener;
private ButtonPressEvent cancelListener; private ButtonPressEvent cancelListener;
private UserList userList;
@FXML @FXML
private void onConfirm() { private void onConfirm() {
setLocked(true); setLocked(true);
CurrentUser.getInstance().setUsername(textField.getText()); if (userList != null && userList.isUsernameAvailable(textField.getText())) {
// TODO replace by network call
Timer t = new Timer();
t.schedule(new TimerTask() {
@Override
public void run() {
// onError();
onSuccess(); onSuccess();
t.cancel(); } else {
onError();
} }
}, 1000);
} }
@FXML @FXML
@ -69,6 +63,10 @@ public class EditUsernameDialogController implements Initializable {
hide(); hide();
} }
public void setUserList(UserList userList) {
this.userList = userList;
}
public void setOnSuccessListener(ButtonPressEvent listener) { public void setOnSuccessListener(ButtonPressEvent listener) {
this.successListener = listener; this.successListener = listener;
} }
@ -149,9 +147,14 @@ public class EditUsernameDialogController implements Initializable {
setFieldError(State.VALID); setFieldError(State.VALID);
setLocked(false); setLocked(false);
hide(); hide();
final String newName = textField.getText();
CurrentUser.getInstance().setUsername(newName);
if (successListener != null) { if (successListener != null) {
successListener.onPress(); successListener.onPress();
} }
if (userList != null) {
userList.propagateUsernameChange();
}
} }
private void onError() { private void onError() {

View file

@ -55,8 +55,12 @@ public class UserListController implements Initializable {
} }
private void onUserConnected(PeerUser user) { private void onUserConnected(PeerUser user) {
if (!peerUserListView.getItems().contains(user)) {
Log.v(this.getClass().getSimpleName(), "Add user to UI"); Log.v(this.getClass().getSimpleName(), "Add user to UI");
Platform.runLater(() -> peerUserListView.getItems().add(user)); Platform.runLater(() -> peerUserListView.getItems().add(user));
} else {
Log.w(this.getClass().getSimpleName(), "User already added to ui, skipping...");
}
} }
public void setUserList(UserList userList) { public void setUserList(UserList userList) {

View file

@ -2,6 +2,7 @@ package fr.insa.clavardator.users;
import fr.insa.clavardator.chat.ChatHistory; import fr.insa.clavardator.chat.ChatHistory;
import fr.insa.clavardator.chat.Message; import fr.insa.clavardator.chat.Message;
import fr.insa.clavardator.errors.UsernameTakenException;
import fr.insa.clavardator.network.PeerConnection; import fr.insa.clavardator.network.PeerConnection;
import fr.insa.clavardator.util.ErrorCallback; import fr.insa.clavardator.util.ErrorCallback;
import fr.insa.clavardator.util.Log; import fr.insa.clavardator.util.Log;
@ -35,16 +36,14 @@ public class PeerUser extends User implements Comparable<PeerUser> {
* @param callback The function to call on success * @param callback The function to call on success
* @param errorCallback The function to call on socket error * @param errorCallback The function to call on socket error
*/ */
public void connect(InetAddress ipAddr, UserConnectedCallback callback, ErrorCallback errorCallback) { public void createConnection(InetAddress ipAddr, UserConnectedCallback callback, ErrorCallback errorCallback) {
if (connection != null && connection.isOpen()) { closeConnection();
connection.close();
}
Log.v(this.getClass().getSimpleName(), "Creating new TCP connection with " + id); Log.v(this.getClass().getSimpleName(), "Creating new TCP connection with " + id);
// Connect to the peer // Connect to the peer
setState(State.CONNECTING); setState(State.CONNECTING);
connection = new PeerConnection( connection = new PeerConnection(
ipAddr, ipAddr,
(thisConnection) -> init(thisConnection, callback, errorCallback), (thisConnection) -> init(thisConnection, false, callback, errorCallback),
e -> { e -> {
Log.e(this.getClass().getSimpleName(), "Could not create TCP connection with " + id, e); Log.e(this.getClass().getSimpleName(), "Could not create TCP connection with " + id, e);
disconnect(); disconnect();
@ -59,16 +58,19 @@ public class PeerUser extends User implements Comparable<PeerUser> {
* @param callback The function to call on success, with the new ActiveUser as parameter * @param callback The function to call on success, with the new ActiveUser as parameter
* @param errorCallback The function to call on socket error * @param errorCallback The function to call on socket error
*/ */
public void connect(Socket socket, UserConnectedCallback callback, ErrorCallback errorCallback) { public void acceptConnection(Socket socket, UserConnectedCallback callback, ErrorCallback errorCallback) {
if (connection != null && connection.isOpen()) { closeConnection();
connection.close();
}
setState(State.CONNECTING); setState(State.CONNECTING);
connection = new PeerConnection(socket); connection = new PeerConnection(socket);
init(connection, true, callback, errorCallback);
init(connection, callback, errorCallback);
} }
/**
* Sends a basic text message to this user
*
* @param msg The text message to send
* @param errorCallback Callback on error
*/
public void sendTextMessage(String msg, @Nullable ErrorCallback errorCallback) { public void sendTextMessage(String msg, @Nullable ErrorCallback errorCallback) {
if (connection != null) { if (connection != null) {
Log.v(this.getClass().getSimpleName(), Log.v(this.getClass().getSimpleName(),
@ -83,38 +85,81 @@ public class PeerUser extends User implements Comparable<PeerUser> {
} }
} }
private void init(PeerConnection thisConnection, UserConnectedCallback callback, ErrorCallback errorCallback) { /**
* Sends current user information to this user
*
* @param errorCallback Callback on error
*/
public void sendCurrentUser(@Nullable ErrorCallback errorCallback) {
if (connection != null) {
final String username = CurrentUser.getInstance().getUsername();
Log.v(this.getClass().getSimpleName(),
"Sending current user information to " + this.getUsername() + " / " + this.getId() + ": " + username);
connection.send(
new UserInformation(CurrentUser.getInstance()),
null,
errorCallback);
} else {
Log.e(this.getClass().getSimpleName(), "Could not send new username: connection is not initialized");
}
}
private void sendUsernameTaken(PeerConnection thisConnection) {
Log.v(this.getClass().getSimpleName(), "Received username request using current username");
thisConnection.send(new UsernameTakenException("Username taken"), this::disconnect, null);
}
/**
* Initializes the connection with this user.
* Both user exchange their information
*
* @param thisConnection The peer connection to use
* @param callback Callback on success
* @param errorCallback Callback on error
*/
private void init(PeerConnection thisConnection, boolean isReceiving, UserConnectedCallback callback, ErrorCallback errorCallback) {
// Send our username // Send our username
String currentUserUsername = CurrentUser.getInstance().getUsername(); thisConnection.send(new UserInformation(CurrentUser.getInstance()), null, e -> {
int currentUserId = CurrentUser.getInstance().getId();
thisConnection.send(new UserInformation(currentUserId, currentUserUsername), null, e -> {
disconnect(); disconnect();
errorCallback.onError(e); errorCallback.onError(e);
}); });
// Receive peer's username // Receive peer's username
thisConnection.receiveOne(msg -> { thisConnection.receiveOne(msg -> {
assert msg instanceof UserInformation; if (msg instanceof UserInformation) {
UserInformation userInfo = (UserInformation) msg; UserInformation userInfo = (UserInformation) msg;
// TODO : Check username unique final String receivedUsername = userInfo.getUsername();
assert id == userInfo.id; if (!receivedUsername.equals(CurrentUser.getInstance().getUsername())) {
setUsername(userInfo.getUsername()); setUsername(receivedUsername);
callback.onUserConnected(); callback.onUserConnected();
// Subscribe to incoming messages
subscribeToMessages(e -> { subscribeToMessages(e -> {
disconnect(); disconnect();
errorCallback.onError(e); errorCallback.onError(e);
}); });
setState(State.CONNECTED); setState(State.CONNECTED);
} else if (isReceiving) {
sendUsernameTaken(thisConnection);
} else {
disconnect();
errorCallback.onError(new Exception("Tried to use same username as remote"));
}
} else {
disconnect();
errorCallback.onError(new Exception("Did not receive remote username"));
}
}, e -> { }, e -> {
disconnect(); disconnect();
errorCallback.onError(e); errorCallback.onError(e);
}); });
} }
/**
* Subscribe to this user messages.
* If receiving new user info, update this user.
* If receiving text message, store it in the history.
*
* @param errorCallback Callback on error
*/
private void subscribeToMessages(ErrorCallback errorCallback) { private void subscribeToMessages(ErrorCallback errorCallback) {
connection.receive( connection.receive(
msg -> { msg -> {
@ -139,19 +184,28 @@ public class PeerUser extends User implements Comparable<PeerUser> {
}); });
} }
/**
* Close the connection and set state to disconnected
*/
public void disconnect() { public void disconnect() {
Log.v(this.getClass().getSimpleName(), "Disconnecting from user: " + id); Log.v(this.getClass().getSimpleName(), "Disconnecting from user: " + id);
closeConnection();
setState(State.DISCONNECTED);
}
/**
* Close the connection to this user
*/
private void closeConnection() {
if (connection != null && connection.isOpen()) { if (connection != null && connection.isOpen()) {
connection.close(); connection.close();
connection = null; connection = null;
} }
setState(State.DISCONNECTED);
} }
/** /**
* Get the value of history * Gets the value of history
* *
* @return the value of history * @return the value of history
*/ */
@ -159,11 +213,21 @@ public class PeerUser extends User implements Comparable<PeerUser> {
return history; return history;
} }
/**
* Sets this user state
*
* @param state The new state
*/
private void setState(State state) { private void setState(State state) {
pcs.firePropertyChange("state", this.state, state); pcs.firePropertyChange("state", this.state, state);
this.state = state; this.state = state;
} }
/**
* Check if this user is active.
*
* @return True id active, false otherwise
*/
public boolean isActive() { public boolean isActive() {
return state == State.CONNECTED; return state == State.CONNECTED;
} }
@ -178,12 +242,18 @@ public class PeerUser extends User implements Comparable<PeerUser> {
return getUsername().compareTo(peerUser.getUsername()); return getUsername().compareTo(peerUser.getUsername());
} }
/**
* The user connection state
*/
public enum State { public enum State {
CONNECTING, CONNECTING,
CONNECTED, CONNECTED,
DISCONNECTED, DISCONNECTED,
} }
/**
* Callback when this user successfully connects
*/
public interface UserConnectedCallback { public interface UserConnectedCallback {
void onUserConnected(); void onUserConnected();
} }

View file

@ -1,7 +1,5 @@
package fr.insa.clavardator.users; package fr.insa.clavardator.users;
import org.jetbrains.annotations.NotNull;
import java.beans.PropertyChangeListener; import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport; import java.beans.PropertyChangeSupport;
import java.io.Serializable; import java.io.Serializable;

View file

@ -2,6 +2,9 @@ package fr.insa.clavardator.users;
import java.io.Serializable; import java.io.Serializable;
/**
* Class used to serialize useful user information
*/
public class UserInformation implements Serializable { public class UserInformation implements Serializable {
public final int id; public final int id;
private final String username; private final String username;

View file

@ -38,10 +38,9 @@ public class UserList {
netDiscoverer.discoverActiveUsers("CLAVARDATOR_BROADCAST", (ipAddr, data) -> { netDiscoverer.discoverActiveUsers("CLAVARDATOR_BROADCAST", (ipAddr, data) -> {
int id = getIdFromIp(ipAddr); int id = getIdFromIp(ipAddr);
Log.v(this.getClass().getSimpleName(), "Discovered new user: " + id); Log.v(this.getClass().getSimpleName(), "Discovered new user: " + id);
final PeerUser finalUser = createNewUser(id); final PeerUser user = createNewUser(id);
if (finalUser != null) { if (user != null) {
finalUser.connect(ipAddr, () -> user.createConnection(ipAddr, () -> onUserConnectionSuccess(user), errorCallback);
finalUser.addObserver(evt -> userChangeObserver(finalUser, evt)), errorCallback);
} }
}, errorCallback); }, errorCallback);
} }
@ -56,17 +55,22 @@ public class UserList {
public void startUserListening(ErrorCallback errorCallback) { public void startUserListening(ErrorCallback errorCallback) {
connectionListener.acceptConnection( connectionListener.acceptConnection(
(clientSocket) -> { (clientSocket) -> {
int id = getIdFromIp(clientSocket.getInetAddress()); final int id = getIdFromIp(clientSocket.getInetAddress());
Log.v(this.getClass().getSimpleName(), "new connection from user: " + id); Log.v(this.getClass().getSimpleName(), "new connection from user: " + id);
final PeerUser finalUser = createNewUser(id); final PeerUser user = createNewUser(id);
if (finalUser != null) { if (user != null) {
finalUser.connect(clientSocket, () -> user.acceptConnection(clientSocket, () ->
finalUser.addObserver(evt -> userChangeObserver(finalUser, evt)), errorCallback); onUserConnectionSuccess(user), errorCallback);
} }
}, },
errorCallback); errorCallback);
} }
private void onUserConnectionSuccess(PeerUser user) {
notifyNewUserObservers(user);
user.addObserver(evt -> userChangeObserver(user, evt));
}
private PeerUser createNewUser(int id) { private PeerUser createNewUser(int id) {
// If already connected, warn and return // If already connected, warn and return
if (activeUsers.containsKey(id)) { if (activeUsers.containsKey(id)) {
@ -82,7 +86,6 @@ public class UserList {
// Username is set on TCP connection start // Username is set on TCP connection start
user = new PeerUser(id); user = new PeerUser(id);
inactiveUsers.put(id, user); inactiveUsers.put(id, user);
notifyNewUserObservers(user);
} }
return user; return user;
@ -146,18 +149,18 @@ public class UserList {
* @return True if the username is available * @return True if the username is available
*/ */
public boolean isUsernameAvailable(String username) { public boolean isUsernameAvailable(String username) {
Predicate<User> usernameEqual = user -> user.getUsername().equals(username); Predicate<User> usernameEqual = user -> user.getUsername() != null && user.getUsername().equals(username);
return activeUsers.values().stream().noneMatch(usernameEqual) && return activeUsers.values().stream().noneMatch(usernameEqual) &&
inactiveUsers.values().stream().noneMatch(usernameEqual); inactiveUsers.values().stream().noneMatch(usernameEqual);
} }
/** /**
* Tell all active users that our username changed * Tell all active users that our username changed
*
* @param username The new username
*/ */
public void propagateUsernameChange(String username) { public void propagateUsernameChange() {
// TODO activeUsers.forEach((id, user) -> {
user.sendCurrentUser(Throwable::printStackTrace);
});
} }
/** /**
@ -177,8 +180,4 @@ public class UserList {
void onUserConnected(PeerUser user); void onUserConnected(PeerUser user);
} }
public interface UserDisconnectionCallback {
void onUserDisconnected(PeerUser user);
}
} }