diff --git a/src/main/java/fr/insa/clavardator/MainApp.java b/src/main/java/fr/insa/clavardator/MainApp.java index c0768c8..c8306d0 100644 --- a/src/main/java/fr/insa/clavardator/MainApp.java +++ b/src/main/java/fr/insa/clavardator/MainApp.java @@ -13,7 +13,7 @@ import javafx.stage.Stage; public class MainApp extends Application { - private UserList userList; + private MainController mainController; public static void main(String[] args) { launch(args); @@ -26,9 +26,7 @@ public class MainApp extends Application { final FXMLLoader mainLoader = new FXMLLoader(getClass().getResource("ui/scene.fxml")); final Parent content = mainLoader.load(); - MainController mainController = mainLoader.getController(); - - userList = mainController.getUserList(); + mainController = mainLoader.getController(); Scene scene = new Scene(content); @@ -43,8 +41,8 @@ public class MainApp extends Application { @Override public void stop() throws Exception { // Stop all threads and active connections before exiting - if (userList != null) { - userList.destroy(); + if (mainController != null) { + mainController.stop(); } super.stop(); } diff --git a/src/main/java/fr/insa/clavardator/server/InsaPresence.java b/src/main/java/fr/insa/clavardator/server/InsaPresence.java index 52ee41b..50dfef8 100644 --- a/src/main/java/fr/insa/clavardator/server/InsaPresence.java +++ b/src/main/java/fr/insa/clavardator/server/InsaPresence.java @@ -1,11 +1,16 @@ package fr.insa.clavardator.server; import fr.insa.clavardator.network.TcpConnection; +import fr.insa.clavardator.users.CurrentUser; import fr.insa.clavardator.users.UserInformation; import fr.insa.clavardator.util.ErrorCallback; +import fr.insa.clavardator.util.Log; import fr.insa.clavardator.util.ParametrizedCallback; import fr.insa.clavardator.util.SimpleCallback; +import org.jetbrains.annotations.Nullable; +import java.net.InetAddress; +import java.net.UnknownHostException; import java.util.ArrayList; /** @@ -29,6 +34,10 @@ public class InsaPresence implements Presence { private final int presencePort; private final int proxyPort; + private TcpConnection presenceConnection; + private TcpConnection proxyConnection; + + public InsaPresence(String path, int presencePort, int proxyPort) { this.path = path; this.presencePort = presencePort; @@ -37,21 +46,131 @@ public class InsaPresence implements Presence { @Override public void subscribe(ParametrizedCallback> callback, ErrorCallback errorCallback) { + if (!isConnected()) { + connectToPresence( + () -> connectToProxy(() -> { + sendSubscribeMessage(errorCallback); + receiveSubscribeNotifications(callback, errorCallback); + }, errorCallback), + errorCallback); + } else { + Log.v(getClass().getSimpleName(), "Already subscribed to presence server"); + } + } + /** + * Send current user information to tell the server who is subscribing + * + * @param errorCallback Called on connection error + */ + private void sendSubscribeMessage(ErrorCallback errorCallback) { + presenceConnection.send(new UserInformation(CurrentUser.getInstance()), null, errorCallback); + } + + /** + * Waits for presence server response to the subscribe request + * + * @param callback Called when the message is successfully received + * @param errorCallback Called on connection error + */ + private void receiveSubscribeNotifications(ParametrizedCallback> callback, @Nullable ErrorCallback errorCallback) { + presenceConnection.receive( + msg -> { + // TODO decide what we should receive + final ArrayList test = new ArrayList<>(); + test.add(new UserInformation("test", "test")); + Log.v(getClass().getSimpleName(), "Receive subscribe response: " + msg); + if (callback != null) { + callback.call(test); + } + }, errorCallback); } @Override - public void unsubscribe(SimpleCallback callback, ErrorCallback errorCallback) { - + public void unsubscribe(SimpleCallback callback, @Nullable ErrorCallback errorCallback) { + if (isConnected()) { + disconnect(); + } else { + Log.v(getClass().getSimpleName(), "Not subscribed to presence server"); + } } - @Override - public void publish(boolean connected, SimpleCallback callback, ErrorCallback errorCallback) { + /** + * Connects to the presence server by TCP + * + * @param callback Called when connection is successful + * @param errorCallback Called on connection error + */ + private void connectToPresence(SimpleCallback callback, ErrorCallback errorCallback) { + try { + presenceConnection = new TcpConnection(InetAddress.getByName(path), + presencePort, + (newConnection) -> { + if (callback != null) { + callback.call(); + } + }, + errorCallback); + } catch (UnknownHostException e) { + Log.e(getClass().getSimpleName(), "Could not connect to presence server", e); + } + } + /** + * Connects to the presence proxy by TCP + * + * @param callback Called when connection is successful + * @param errorCallback Called on connection error + */ + private void connectToProxy(SimpleCallback callback, ErrorCallback errorCallback) { + try { + proxyConnection = new TcpConnection(InetAddress.getByName(path), + proxyPort, + (newConnection) -> { + if (callback != null) { + callback.call(); + } + }, + errorCallback); + } catch (UnknownHostException e) { + Log.e(getClass().getSimpleName(), "Could not connect to presence proxy", e); + } + } + + /** + * Closes the given connection + */ + private void closeConnection(@Nullable TcpConnection c) { + if (c != null && c.isOpen()) { + c.close(); + } + } + + /** + * Checks if we are connected to the presence server and proxy + * + * @return True only if we are connected to both presence server and proxy + */ + private boolean isConnected() { + return presenceConnection != null && + presenceConnection.isOpen() && + proxyConnection != null && + proxyConnection.isOpen(); + } + + /** + * Disconnects from both presence server and proxy + */ + private void disconnect() { + Log.v(this.getClass().getSimpleName(), "Disconnecting presence server"); + closeConnection(presenceConnection); + closeConnection(proxyConnection); + presenceConnection = null; + proxyConnection = null; } @Override public TcpConnection getProxyConnection() { - return null; + return proxyConnection; } } diff --git a/src/main/java/fr/insa/clavardator/server/Presence.java b/src/main/java/fr/insa/clavardator/server/Presence.java index f90d5e9..dd0f9bc 100644 --- a/src/main/java/fr/insa/clavardator/server/Presence.java +++ b/src/main/java/fr/insa/clavardator/server/Presence.java @@ -5,6 +5,7 @@ import fr.insa.clavardator.users.UserInformation; import fr.insa.clavardator.util.ErrorCallback; import fr.insa.clavardator.util.ParametrizedCallback; import fr.insa.clavardator.util.SimpleCallback; +import org.jetbrains.annotations.Nullable; import java.util.ArrayList; @@ -23,7 +24,7 @@ public interface Presence { * * @param callback Called when subscription completes */ - void subscribe(ParametrizedCallback> callback, ErrorCallback errorCallback); + void subscribe(ParametrizedCallback> callback, @Nullable ErrorCallback errorCallback); /** * Stops subscription to the presence server by closing TCP connections. @@ -32,17 +33,7 @@ public interface Presence { * @implNote Call this before exiting the app. * If not, the presence server will wait for a tcp timeout before marking this user as disconnected. */ - void unsubscribe(SimpleCallback callback, ErrorCallback errorCallback); - - /** - * Updates the current user state on the server. - * This function must be called on app exit, - * or the server won't know this user is inactive. - * - * @param connected The new user state - */ - void publish(boolean connected, SimpleCallback callback, ErrorCallback errorCallback); - + void unsubscribe(SimpleCallback callback, @Nullable ErrorCallback errorCallback); /** * Gets a connection to the proxy. diff --git a/src/main/java/fr/insa/clavardator/ui/MainController.java b/src/main/java/fr/insa/clavardator/ui/MainController.java index 0c793b0..7729d80 100644 --- a/src/main/java/fr/insa/clavardator/ui/MainController.java +++ b/src/main/java/fr/insa/clavardator/ui/MainController.java @@ -2,6 +2,7 @@ package fr.insa.clavardator.ui; import com.jfoenix.controls.JFXSnackbar; import fr.insa.clavardator.config.Config; +import fr.insa.clavardator.config.ConfigLoader; import fr.insa.clavardator.db.DatabaseController; import fr.insa.clavardator.server.Presence; import fr.insa.clavardator.server.PresenceFactory; @@ -14,7 +15,6 @@ import fr.insa.clavardator.ui.dialogs.SnackbarController; import fr.insa.clavardator.ui.users.UserListController; import fr.insa.clavardator.users.CurrentUser; import fr.insa.clavardator.users.UserList; -import fr.insa.clavardator.config.ConfigLoader; import fr.insa.clavardator.util.Log; import javafx.application.Platform; import javafx.fxml.FXML; @@ -180,6 +180,7 @@ public class MainController implements Initializable { Log.v(this.getClass().getSimpleName(), "Chat started"); discoverActiveUsers(); startListening(); + subscribeToPresenceServer(); Platform.runLater(this::showChat); } @@ -190,9 +191,7 @@ public class MainController implements Initializable { online = false; listController.setRefreshButtonEnabled(false); Log.v(this.getClass().getSimpleName(), "Chat ended"); - if (userList != null) { - userList.destroy(); - } + stop(); } /** @@ -251,23 +250,71 @@ public class MainController implements Initializable { private void initBackend() { ConfigLoader.load( (Config config) -> new DatabaseController().initTables( - () -> userList.retrievedPreviousUsers( - () -> currentUser.init( - () -> userList.initPresenceServer(config), - this::onInitError), - this::onInitError - ), this::onInitError)); + () -> userList.retrievedPreviousUsers( + () -> currentUser.init( + () -> initPresenceServer(config), + this::onInitError), + this::onInitError + ), this::onInitError)); } /** - * Gets the current user list. + * Initializes the presence server based on user config * - * @implNote BE SURE TO CALL .destroy() BEFORE EXITING THE APP. - * - * @return The current user list + * @param config The user config */ - public UserList getUserList() { - return userList; + public void initPresenceServer(Config config) { + final Config.ServerConfig serverConfig = config.getServerConfig(); + if (serverConfig.isEnabled()) { + try { + final PresenceType type = serverConfig.getType(); + final String uri = serverConfig.getUri(); + final int presencePort = serverConfig.getPresencePort(); + final int proxyPort = serverConfig.getProxyPort(); + presenceServer = PresenceFactory.create(type, uri, presencePort, proxyPort); + Log.v(getClass().getSimpleName(), "Presence server support enabled: " + type + "@" + uri + ':' + presencePort + " / proxy:" + proxyPort); + subscribeToPresenceServer(); + } catch (UnknownPresenceException e) { + Log.e(getClass().getSimpleName(), "Presence server type not found", e); + presenceServer = null; + } + } else { + Log.v(getClass().getSimpleName(), "Presence server support disabled."); + } + } + + private void subscribeToPresenceServer() { + if (presenceServer != null && online) { + presenceServer.subscribe( + param -> userList.onReceivePresenceNotification( + param, + presenceServer.getProxyConnection()), + (e) -> Log.v( + getClass().getSimpleName(), + "Error subscribing to presence server", + e)); + } + } + + private void unsubscribeToPresenceServer() { + if (presenceServer != null) { + presenceServer.unsubscribe( + null, + (e) -> Log.v( + getClass().getSimpleName(), + "Error unsubscribing to presence server", + e)); + } + } + + /** + * Stop all threads and active connections before exiting + */ + public void stop() { + if (userList != null) { + userList.destroy(); + } + unsubscribeToPresenceServer(); } @Override diff --git a/src/main/java/fr/insa/clavardator/users/UserList.java b/src/main/java/fr/insa/clavardator/users/UserList.java index 9abf8ab..c410e39 100644 --- a/src/main/java/fr/insa/clavardator/users/UserList.java +++ b/src/main/java/fr/insa/clavardator/users/UserList.java @@ -1,20 +1,17 @@ package fr.insa.clavardator.users; -import fr.insa.clavardator.config.Config; import fr.insa.clavardator.db.DatabaseController; -import fr.insa.clavardator.network.TcpListener; import fr.insa.clavardator.network.NetDiscoverer; import fr.insa.clavardator.network.PeerHandshake; -import fr.insa.clavardator.server.Presence; -import fr.insa.clavardator.server.PresenceFactory; -import fr.insa.clavardator.server.PresenceType; -import fr.insa.clavardator.server.UnknownPresenceException; +import fr.insa.clavardator.network.TcpConnection; +import fr.insa.clavardator.network.TcpListener; import fr.insa.clavardator.util.ErrorCallback; import fr.insa.clavardator.util.Log; import javafx.application.Platform; import javafx.collections.FXCollections; import javafx.collections.ObservableList; +import java.util.ArrayList; import java.util.HashMap; import java.util.Map; import java.util.function.Predicate; @@ -29,8 +26,6 @@ public class UserList { private final NetDiscoverer netDiscoverer = new NetDiscoverer(); private final TcpListener tcpListener = new TcpListener(); - private Presence presenceServer; - public UserList() { } @@ -50,6 +45,20 @@ public class UserList { }, errorCallback); } + public void onReceivePresenceNotification(ArrayList newPresenceUsers, TcpConnection proxyConnection) { + newPresenceUsers.forEach((userInfo -> { + final PeerUser savedUser = userHashmap.get(userInfo.id); + if (savedUser != null) { + Log.v(getClass().getSimpleName(), "Received user from presence server already known"); + } else { + final PeerUser user = new PeerUser(); + user.init(proxyConnection, userInfo.id, userInfo.getUsername(), null); + userHashmap.put(user.id, user); + Platform.runLater(() -> userObservableList.add(user)); + } + })); + } + /** * Starts listening for other broadcasts. * Only answers and waits for them to initiate the connection. @@ -132,30 +141,6 @@ public class UserList { userHashmap.forEach((id, user) -> user.sendCurrentUser(Throwable::printStackTrace)); } - /** - * Initializes the presence server based on user config - * - * @param config The user config - */ - public void initPresenceServer(Config config) { - final Config.ServerConfig serverConfig = config.getServerConfig(); - if (serverConfig.isEnabled()) { - try { - final PresenceType type = serverConfig.getType(); - final String uri = serverConfig.getUri(); - final int presencePort = serverConfig.getPresencePort(); - final int proxyPort = serverConfig.getProxyPort(); - presenceServer = PresenceFactory.create(type, uri, presencePort, proxyPort); - Log.v(getClass().getSimpleName(), "Presence server support enabled: " + type + "@" + uri + ':' + presencePort + " / proxy:" + proxyPort); - } catch (UnknownPresenceException e) { - Log.e(getClass().getSimpleName(), "Presence server type not found", e); - presenceServer = null; - } - } else { - Log.v(getClass().getSimpleName(), "Presence server support disabled."); - } - } - /** * Closes all running threads, sockets and db connection. * Must be called before exiting the app.