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.URL;
import java.util.ResourceBundle;
import java.util.Timer;
import java.util.TimerTask;
public class MainController implements Initializable {
@ -58,8 +60,13 @@ public class MainController implements Initializable {
private void onCurrentUserStateChange(CurrentUser.State newState) {
if (newState == CurrentUser.State.VALID)
startChat();
else
Platform.runLater(() -> showLogin(newState == CurrentUser.State.INVALID));
else {
final boolean isError = newState == CurrentUser.State.INVALID;
if (isError) {
endChat();
}
Platform.runLater(() -> showLogin(isError));
}
}
private void openEditUsernameDialog(EditUsernameDialogController.Mode mode) {
@ -72,11 +79,11 @@ public class MainController implements Initializable {
}
});
editUserDialogController.setOnSuccessListener(() -> {
if (!initialMode) {
if (mode == EditUsernameDialogController.Mode.EDIT) {
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) {
@ -99,15 +106,26 @@ public class MainController implements Initializable {
private void discoverActiveUsers() {
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() {
if (userList != null) {
userList.startDiscoveryListening();
userList.startUserListening((e) ->
Log.e(this.getClass().getSimpleName(), "Error listening to users", e));
userList.startUserListening((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);
}
private void endChat() {
Log.v(this.getClass().getSimpleName(), "Chat ended");
stopListening();
}
private void showChat() {
Log.v(this.getClass().getSimpleName(), "Chat shown");
loadingController.hide();
@ -128,7 +151,14 @@ public class MainController implements Initializable {
Log.v(this.getClass().getSimpleName(), "Login shown");
mainContainer.setVisible(false);
loadingController.hide();
Timer t = new Timer();
t.schedule(new TimerTask() {
@Override
public void run() {
openEditUsernameDialog(isError ? EditUsernameDialogController.Mode.ERROR : EditUsernameDialogController.Mode.INITIAL);
t.cancel();
}
}, 500);
}
private void showError() {
@ -162,5 +192,6 @@ public class MainController implements Initializable {
this.userList = userList;
listController.setUserList(userList);
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 fr.insa.clavardator.ui.ButtonPressEvent;
import fr.insa.clavardator.users.CurrentUser;
import fr.insa.clavardator.users.UserList;
import javafx.beans.property.ReadOnlyBooleanWrapper;
import javafx.beans.value.ObservableValue;
import javafx.fxml.FXML;
@ -16,8 +17,6 @@ import javafx.scene.layout.StackPane;
import java.net.URL;
import java.util.ResourceBundle;
import java.util.Timer;
import java.util.TimerTask;
public class EditUsernameDialogController implements Initializable {
@ -44,21 +43,16 @@ public class EditUsernameDialogController implements Initializable {
private Validator validator;
private ButtonPressEvent successListener;
private ButtonPressEvent cancelListener;
private UserList userList;
@FXML
private void onConfirm() {
setLocked(true);
CurrentUser.getInstance().setUsername(textField.getText());
// TODO replace by network call
Timer t = new Timer();
t.schedule(new TimerTask() {
@Override
public void run() {
// onError();
if (userList != null && userList.isUsernameAvailable(textField.getText())) {
onSuccess();
t.cancel();
} else {
onError();
}
}, 1000);
}
@FXML
@ -69,6 +63,10 @@ public class EditUsernameDialogController implements Initializable {
hide();
}
public void setUserList(UserList userList) {
this.userList = userList;
}
public void setOnSuccessListener(ButtonPressEvent listener) {
this.successListener = listener;
}
@ -149,9 +147,14 @@ public class EditUsernameDialogController implements Initializable {
setFieldError(State.VALID);
setLocked(false);
hide();
final String newName = textField.getText();
CurrentUser.getInstance().setUsername(newName);
if (successListener != null) {
successListener.onPress();
}
if (userList != null) {
userList.propagateUsernameChange();
}
}
private void onError() {

View file

@ -55,8 +55,12 @@ public class UserListController implements Initializable {
}
private void onUserConnected(PeerUser user) {
if (!peerUserListView.getItems().contains(user)) {
Log.v(this.getClass().getSimpleName(), "Add user to UI");
Platform.runLater(() -> peerUserListView.getItems().add(user));
} else {
Log.w(this.getClass().getSimpleName(), "User already added to ui, skipping...");
}
}
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.Message;
import fr.insa.clavardator.errors.UsernameTakenException;
import fr.insa.clavardator.network.PeerConnection;
import fr.insa.clavardator.util.ErrorCallback;
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 errorCallback The function to call on socket error
*/
public void connect(InetAddress ipAddr, UserConnectedCallback callback, ErrorCallback errorCallback) {
if (connection != null && connection.isOpen()) {
connection.close();
}
public void createConnection(InetAddress ipAddr, UserConnectedCallback callback, ErrorCallback errorCallback) {
closeConnection();
Log.v(this.getClass().getSimpleName(), "Creating new TCP connection with " + id);
// Connect to the peer
setState(State.CONNECTING);
connection = new PeerConnection(
ipAddr,
(thisConnection) -> init(thisConnection, callback, errorCallback),
(thisConnection) -> init(thisConnection, false, callback, errorCallback),
e -> {
Log.e(this.getClass().getSimpleName(), "Could not create TCP connection with " + id, e);
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 errorCallback The function to call on socket error
*/
public void connect(Socket socket, UserConnectedCallback callback, ErrorCallback errorCallback) {
if (connection != null && connection.isOpen()) {
connection.close();
}
public void acceptConnection(Socket socket, UserConnectedCallback callback, ErrorCallback errorCallback) {
closeConnection();
setState(State.CONNECTING);
connection = new PeerConnection(socket);
init(connection, callback, errorCallback);
init(connection, true, 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) {
if (connection != null) {
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
String currentUserUsername = CurrentUser.getInstance().getUsername();
int currentUserId = CurrentUser.getInstance().getId();
thisConnection.send(new UserInformation(currentUserId, currentUserUsername), null, e -> {
thisConnection.send(new UserInformation(CurrentUser.getInstance()), null, e -> {
disconnect();
errorCallback.onError(e);
});
// Receive peer's username
thisConnection.receiveOne(msg -> {
assert msg instanceof UserInformation;
if (msg instanceof UserInformation) {
UserInformation userInfo = (UserInformation) msg;
// TODO : Check username unique
assert id == userInfo.id;
setUsername(userInfo.getUsername());
final String receivedUsername = userInfo.getUsername();
if (!receivedUsername.equals(CurrentUser.getInstance().getUsername())) {
setUsername(receivedUsername);
callback.onUserConnected();
// Subscribe to incoming messages
subscribeToMessages(e -> {
disconnect();
errorCallback.onError(e);
});
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 -> {
disconnect();
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) {
connection.receive(
msg -> {
@ -139,19 +184,28 @@ public class PeerUser extends User implements Comparable<PeerUser> {
});
}
/**
* Close the connection and set state to disconnected
*/
public void disconnect() {
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()) {
connection.close();
connection = null;
}
setState(State.DISCONNECTED);
}
/**
* Get the value of history
* Gets the value of history
*
* @return the value of history
*/
@ -159,11 +213,21 @@ public class PeerUser extends User implements Comparable<PeerUser> {
return history;
}
/**
* Sets this user state
*
* @param state The new state
*/
private void setState(State state) {
pcs.firePropertyChange("state", this.state, state);
this.state = state;
}
/**
* Check if this user is active.
*
* @return True id active, false otherwise
*/
public boolean isActive() {
return state == State.CONNECTED;
}
@ -178,12 +242,18 @@ public class PeerUser extends User implements Comparable<PeerUser> {
return getUsername().compareTo(peerUser.getUsername());
}
/**
* The user connection state
*/
public enum State {
CONNECTING,
CONNECTED,
DISCONNECTED,
}
/**
* Callback when this user successfully connects
*/
public interface UserConnectedCallback {
void onUserConnected();
}

View file

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

View file

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

View file

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