diff --git a/src/main/java/fr/insa/clavardator/errors/UsernameTakenException.java b/src/main/java/fr/insa/clavardator/errors/UsernameTakenException.java new file mode 100644 index 0000000..dfb69fd --- /dev/null +++ b/src/main/java/fr/insa/clavardator/errors/UsernameTakenException.java @@ -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); + } +} diff --git a/src/main/java/fr/insa/clavardator/ui/MainController.java b/src/main/java/fr/insa/clavardator/ui/MainController.java index ec12db8..65061dd 100644 --- a/src/main/java/fr/insa/clavardator/ui/MainController.java +++ b/src/main/java/fr/insa/clavardator/ui/MainController.java @@ -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(); - openEditUsernameDialog(isError ? EditUsernameDialogController.Mode.ERROR : EditUsernameDialogController.Mode.INITIAL); + 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); } } diff --git a/src/main/java/fr/insa/clavardator/ui/dialogs/EditUsernameDialogController.java b/src/main/java/fr/insa/clavardator/ui/dialogs/EditUsernameDialogController.java index d8b8a3c..a8f2b20 100644 --- a/src/main/java/fr/insa/clavardator/ui/dialogs/EditUsernameDialogController.java +++ b/src/main/java/fr/insa/clavardator/ui/dialogs/EditUsernameDialogController.java @@ -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(); - onSuccess(); - t.cancel(); - } - }, 1000); + if (userList != null && userList.isUsernameAvailable(textField.getText())) { + onSuccess(); + } else { + onError(); + } } @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() { diff --git a/src/main/java/fr/insa/clavardator/ui/users/UserListController.java b/src/main/java/fr/insa/clavardator/ui/users/UserListController.java index fe4d0dc..9d0a1eb 100644 --- a/src/main/java/fr/insa/clavardator/ui/users/UserListController.java +++ b/src/main/java/fr/insa/clavardator/ui/users/UserListController.java @@ -55,8 +55,12 @@ public class UserListController implements Initializable { } private void onUserConnected(PeerUser user) { - Log.v(this.getClass().getSimpleName(), "Add user to UI"); - Platform.runLater(() -> peerUserListView.getItems().add(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) { diff --git a/src/main/java/fr/insa/clavardator/users/PeerUser.java b/src/main/java/fr/insa/clavardator/users/PeerUser.java index b53cb3d..5e2fb4e 100644 --- a/src/main/java/fr/insa/clavardator/users/PeerUser.java +++ b/src/main/java/fr/insa/clavardator/users/PeerUser.java @@ -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 { * @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,17 +58,20 @@ public class PeerUser extends User implements Comparable { * @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); } - public void sendTextMessage(String msg, @Nullable ErrorCallback 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(), "Sending message to " + this.getUsername() + " / " + this.getId() + ": " + msg); @@ -83,38 +85,81 @@ public class PeerUser extends User implements Comparable { } } - 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; - UserInformation userInfo = (UserInformation) msg; - // TODO : Check username unique - assert id == userInfo.id; - setUsername(userInfo.getUsername()); - callback.onUserConnected(); - - // Subscribe to incoming messages - subscribeToMessages(e -> { + if (msg instanceof UserInformation) { + UserInformation userInfo = (UserInformation) msg; + final String receivedUsername = userInfo.getUsername(); + if (!receivedUsername.equals(CurrentUser.getInstance().getUsername())) { + setUsername(receivedUsername); + callback.onUserConnected(); + 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(e); - }); - setState(State.CONNECTED); - + 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 { }); } - + /** + * 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 { 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 { 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(); } diff --git a/src/main/java/fr/insa/clavardator/users/User.java b/src/main/java/fr/insa/clavardator/users/User.java index f029379..263f202 100644 --- a/src/main/java/fr/insa/clavardator/users/User.java +++ b/src/main/java/fr/insa/clavardator/users/User.java @@ -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; diff --git a/src/main/java/fr/insa/clavardator/users/UserInformation.java b/src/main/java/fr/insa/clavardator/users/UserInformation.java index 137a13e..1ceb4bd 100644 --- a/src/main/java/fr/insa/clavardator/users/UserInformation.java +++ b/src/main/java/fr/insa/clavardator/users/UserInformation.java @@ -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; diff --git a/src/main/java/fr/insa/clavardator/users/UserList.java b/src/main/java/fr/insa/clavardator/users/UserList.java index 6b4956e..0bf3fd3 100644 --- a/src/main/java/fr/insa/clavardator/users/UserList.java +++ b/src/main/java/fr/insa/clavardator/users/UserList.java @@ -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 usernameEqual = user -> user.getUsername().equals(username); + Predicate 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); - } - }