Compare commits

...

21 commits

Author SHA1 Message Date
Arnaud Vergnet
e8807f28d3 Add report draft 2021-01-31 22:52:14 +01:00
Arnaud Vergnet
ada6167c25 Lock server port between 1024 and 64000 2021-01-31 22:47:55 +01:00
Arnaud Vergnet
b281b4b530 Reformat whole project 2021-01-31 20:28:54 +01:00
Arnaud Vergnet
547c94110e Remove unnecessary public attribute 2021-01-31 20:17:31 +01:00
Arnaud Vergnet
4d21742261 Allow using cli for server 2021-01-31 19:55:59 +01:00
Arnaud Vergnet
2fc3a00264 Merge remote-tracking branch 'origin/subprojects' into subprojects 2021-01-31 17:18:24 +01:00
Arnaud Vergnet
65872c33aa Abstract insa proxy with proxy interface 2021-01-31 17:18:11 +01:00
a4d2c48eb6 Remove not needed PeerUser reference in PeerConnection 2021-01-27 12:25:31 +01:00
599a4a1121 Fix user not being disconnected on local network 2021-01-27 12:19:30 +01:00
Arnaud Vergnet
298e01d09c Allow configuring local network
Can disable/enable local network and set port
2021-01-27 11:19:25 +01:00
Arnaud Vergnet
60fa5adfe4 Allow sending username updates 2021-01-27 10:13:13 +01:00
Arnaud Vergnet
d51cbc6d75 Remove unused dependency and fix stdin run error 2021-01-27 09:08:05 +01:00
172f9d48a3 Fix user disconnection via proxy and various other bugs 2021-01-27 01:30:06 +01:00
f41c7baa8b Fix client connection with the proxy 2021-01-27 00:32:36 +01:00
Arnaud Vergnet
ebd292e6eb Publish disconnected user info on unsubscribe 2021-01-25 15:02:00 +01:00
Arnaud Vergnet
33f355b90b move user state outside of peer user 2021-01-25 14:51:09 +01:00
32364461be [WIP] Integrate server to the project
Many bugs are yet to fix
2021-01-20 14:26:57 +01:00
Arnaud Vergnet
7bbaa4f8da update build and run instructions 2021-01-12 14:51:01 +01:00
Arnaud Vergnet
034fb9203d Move config file in client subproject 2021-01-12 13:36:37 +01:00
Arnaud Vergnet
04e7cfb5e3 Remove unnecessary gradle code 2021-01-12 13:33:21 +01:00
Arnaud Vergnet
b6d02eaaec Organize project into client and lib subprojects 2021-01-12 12:24:14 +01:00
104 changed files with 1666 additions and 865 deletions

3
.gitignore vendored
View file

@ -1,6 +1,6 @@
.gradle .gradle
**/build/ **/build/
!src/**/build/ !client/src/**/build/
# Ignore sqlite db files # Ignore sqlite db files
/*.db /*.db
@ -17,3 +17,4 @@ gradle-app.setting
# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898
# gradle/wrapper/gradle-wrapper.properties # gradle/wrapper/gradle-wrapper.properties
/out/ /out/
/clavardator_stored_files/

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="CompilerConfiguration"> <component name="CompilerConfiguration">
<bytecodeTargetLevel target="14" /> <bytecodeTargetLevel target="11" />
</component> </component>
</project> </project>

24
.idea/dataSources.xml Normal file
View file

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="clavardator_test" uuid="ef1353b8-2399-4195-b9fc-3ae9b9c61351">
<driver-ref>sqlite.xerial</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
<jdbc-url>jdbc:sqlite:$PROJECT_DIR$/clavardator_test.db</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
<data-source source="LOCAL" name="clavardator" uuid="9e12684a-a723-44a7-82c6-f07ab8912b44">
<driver-ref>sqlite.xerial</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
<jdbc-url>jdbc:sqlite:$PROJECT_DIR$/clavardator.db</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir>
<libraries>
<library>
<url>file://$APPLICATION_CONFIG_DIR$/jdbc-drivers/Xerial SQLiteJDBC/3.31.1/sqlite-jdbc-3.31.1.jar</url>
</library>
</libraries>
</data-source>
</component>
</project>

View file

@ -9,6 +9,9 @@
<option name="modules"> <option name="modules">
<set> <set>
<option value="$PROJECT_DIR$" /> <option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/client" />
<option value="$PROJECT_DIR$/lib" />
<option value="$PROJECT_DIR$/server" />
</set> </set>
</option> </option>
</GradleProjectSettings> </GradleProjectSettings>

View file

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<module external.system.module.type="sourceSet" type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager">
<output url="file://$MODULE_DIR$/../../build/classes/java/main" />
<exclude-output />
<content url="file://$MODULE_DIR$/../../client/src/main">
<sourceFolder url="file://$MODULE_DIR$/../../client/src/main/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/../../client/src/main/resources" type="java-resource" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="Gradle: com.jfoenix:jfoenix:9.0.10" level="project" />
<orderEntry type="library" name="Gradle: org.openjfx:javafx-fxml:linux:11.0.2" level="project" />
<orderEntry type="library" name="Gradle: org.openjfx:javafx-controls:11.0.2" level="project" />
<orderEntry type="library" name="Gradle: org.openjfx:javafx-controls:linux:11.0.2" level="project" />
<orderEntry type="library" name="Gradle: org.openjfx:javafx-graphics:11.0.2" level="project" />
<orderEntry type="library" name="Gradle: org.openjfx:javafx-graphics:linux:11.0.2" level="project" />
<orderEntry type="library" name="Gradle: org.openjfx:javafx-base:11.0.2" level="project" />
<orderEntry type="library" name="Gradle: org.openjfx:javafx-base:linux:11.0.2" level="project" />
<orderEntry type="library" name="Gradle: org.jetbrains:annotations:20.1.0" level="project" />
<orderEntry type="library" name="Gradle: org.xerial:sqlite-jdbc:3.32.3" level="project" />
<orderEntry type="library" name="Gradle: org.kordamp.ikonli:ikonli-javafx:12.0.0" level="project" />
<orderEntry type="library" name="Gradle: org.kordamp.ikonli:ikonli-fontawesome5-pack:12.0.0" level="project" />
<orderEntry type="library" name="Gradle: org.json:json:20201115" level="project" />
<orderEntry type="library" name="Gradle: org.kordamp.ikonli:ikonli-core:12.0.0" level="project" />
<orderEntry type="library" scope="RUNTIME" name="Gradle: org.openjfx:javafx-graphics:mac:11.0.2" level="project" />
<orderEntry type="library" scope="RUNTIME" name="Gradle: org.openjfx:javafx-graphics:win:11.0.2" level="project" />
</component>
</module>

View file

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<module external.system.module.type="sourceSet" type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager">
<output-test url="file://$MODULE_DIR$/../../build/classes/java/test" />
<exclude-output />
<content url="file://$MODULE_DIR$/../../client/src/test">
<sourceFolder url="file://$MODULE_DIR$/../../client/src/test/java" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/../../client/src/test/resources" type="java-test-resource" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="module" module-name="clavardator.main" />
<orderEntry type="library" name="Gradle: com.jfoenix:jfoenix:9.0.10" level="project" />
<orderEntry type="library" name="Gradle: org.openjfx:javafx-fxml:linux:11.0.2" level="project" />
<orderEntry type="library" name="Gradle: org.openjfx:javafx-controls:11.0.2" level="project" />
<orderEntry type="library" name="Gradle: org.openjfx:javafx-controls:linux:11.0.2" level="project" />
<orderEntry type="library" name="Gradle: org.openjfx:javafx-graphics:11.0.2" level="project" />
<orderEntry type="library" name="Gradle: org.openjfx:javafx-graphics:linux:11.0.2" level="project" />
<orderEntry type="library" name="Gradle: org.openjfx:javafx-base:11.0.2" level="project" />
<orderEntry type="library" name="Gradle: org.openjfx:javafx-base:linux:11.0.2" level="project" />
<orderEntry type="library" name="Gradle: org.jetbrains:annotations:20.1.0" level="project" />
<orderEntry type="library" name="Gradle: org.xerial:sqlite-jdbc:3.32.3" level="project" />
<orderEntry type="library" name="Gradle: org.kordamp.ikonli:ikonli-javafx:12.0.0" level="project" />
<orderEntry type="library" name="Gradle: org.kordamp.ikonli:ikonli-fontawesome5-pack:12.0.0" level="project" />
<orderEntry type="library" name="Gradle: org.json:json:20201115" level="project" />
<orderEntry type="library" name="Gradle: org.junit.jupiter:junit-jupiter-api:5.7.0" level="project" />
<orderEntry type="library" name="Gradle: org.kordamp.ikonli:ikonli-core:12.0.0" level="project" />
<orderEntry type="library" name="Gradle: org.junit.platform:junit-platform-commons:1.7.0" level="project" />
<orderEntry type="library" name="Gradle: org.apiguardian:apiguardian-api:1.1.0" level="project" />
<orderEntry type="library" name="Gradle: org.opentest4j:opentest4j:1.2.0" level="project" />
<orderEntry type="library" scope="RUNTIME" name="Gradle: org.openjfx:javafx-graphics:mac:11.0.2" level="project" />
<orderEntry type="library" scope="RUNTIME" name="Gradle: org.openjfx:javafx-graphics:win:11.0.2" level="project" />
<orderEntry type="library" scope="RUNTIME" name="Gradle: org.junit.jupiter:junit-jupiter-engine:5.7.0" level="project" />
<orderEntry type="library" scope="RUNTIME" name="Gradle: org.junit.platform:junit-platform-engine:1.7.0" level="project" />
</component>
</module>

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="SqlDialectMappings"> <component name="SqlDialectMappings">
<file url="file://$PROJECT_DIR$/src/main/java/fr/insa/clavardator/db/DatabaseController.java" dialect="GenericSQL" /> <file url="file://$PROJECT_DIR$/client/src/main/java/fr/insa/clavardator/client/db/DatabaseController.java" dialect="GenericSQL" />
<file url="PROJECT" dialect="SQLite" /> <file url="PROJECT" dialect="SQLite" />
</component> </component>
</project> </project>

View file

@ -2,6 +2,8 @@
This project contains the source for the 4IR Java project. This project contains the source for the 4IR Java project.
Full report available [here](report/report.pdf).
## Group ## Group
* SIMARD Yohan * SIMARD Yohan
@ -31,10 +33,10 @@ Follow [this link](https://gluonhq.com/products/scene-builder/) to download and
Run this command Run this command
```shell script ```shell script
./gradlew run ./gradlew runShadow
``` ```
Or in Intellij, open the gradle window and click on `clavardator -> Tasks -> application -> run`. Or in Intellij, open the gradle window and click on `clavardator -> Tasks -> application -> runShadow`.
#### Generate cross-platform jar #### Generate cross-platform jar
@ -44,11 +46,13 @@ Run this command
./gradlew build ./gradlew build
``` ```
This will generate a jar file under `build/libs`. This will generate a jar file under `client/build/libs/client-{VERSION}-all.jar`.
You can then copy this file and place it on any Linux/Windows/Mac environment with at least Java 11 installed. You can then copy this file and place it on any Linux/Windows/Mac environment with at least Java 11 installed.
You can then run this jar file with this command: #### Running a JAR file
You can run a jar file with this command:
```shell script ```shell script
java -jar <JAR-NAME>.jar java -jar <JAR-NAME>.jar

View file

@ -4,7 +4,7 @@ plugins {
id 'com.github.johnrengelman.shadow' version '6.1.0' id 'com.github.johnrengelman.shadow' version '6.1.0'
} }
group 'fr.insa.clavardator' group 'fr.insa.clavardator.client'
version '0.0.1' version '0.0.1'
repositories { repositories {
@ -17,6 +17,7 @@ javafx {
} }
dependencies { dependencies {
implementation project(':lib')
implementation 'org.jetbrains:annotations:20.1.0' implementation 'org.jetbrains:annotations:20.1.0'
runtimeOnly "org.openjfx:javafx-graphics:$javafx.version:win" runtimeOnly "org.openjfx:javafx-graphics:$javafx.version:win"
runtimeOnly "org.openjfx:javafx-graphics:$javafx.version:linux" runtimeOnly "org.openjfx:javafx-graphics:$javafx.version:linux"
@ -35,16 +36,7 @@ test {
useJUnitPlatform() useJUnitPlatform()
} }
mainClassName = 'fr.insa.clavardator.Launcher' mainClassName = 'fr.insa.clavardator.client.Launcher'
jar {
manifest {
attributes 'Main-Class': 'fr.insa.clavardator.Launcher'
}
from {
configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) }
}
}
shadowJar { shadowJar {
mergeServiceFiles() mergeServiceFiles()

View file

@ -1,11 +1,15 @@
{ {
"serveur": { "serveur": {
"actif": 1, "actif": 0,
"uri": "test", "uri": "localhost",
"type": "INSA", "type": "INSA",
"ports": { "ports": {
"presence": 35650, "presence": 35650,
"proxy": 35750 "proxy": 35750
} }
},
"local": {
"actif": 1,
"port": 31590
} }
} }

View file

@ -1,4 +1,4 @@
package fr.insa.clavardator; package fr.insa.clavardator.client;
public class Launcher { public class Launcher {
public static void main(String[] args) { public static void main(String[] args) {

View file

@ -1,8 +1,7 @@
package fr.insa.clavardator; package fr.insa.clavardator.client;
import fr.insa.clavardator.ui.MainController; import fr.insa.clavardator.client.ui.MainController;
import fr.insa.clavardator.users.UserList; import fr.insa.clavardator.lib.util.Log;
import fr.insa.clavardator.util.Log;
import javafx.application.Application; import javafx.application.Application;
import javafx.fxml.FXMLLoader; import javafx.fxml.FXMLLoader;
import javafx.scene.Parent; import javafx.scene.Parent;

View file

@ -1,11 +1,12 @@
package fr.insa.clavardator.chat; package fr.insa.clavardator.client.chat;
import fr.insa.clavardator.db.DatabaseController; import fr.insa.clavardator.client.db.DatabaseController;
import fr.insa.clavardator.users.CurrentUser; import fr.insa.clavardator.client.users.CurrentUser;
import fr.insa.clavardator.users.PeerUser; import fr.insa.clavardator.client.users.PeerUser;
import fr.insa.clavardator.users.UserInformation; import fr.insa.clavardator.lib.message.Message;
import fr.insa.clavardator.util.ErrorCallback; import fr.insa.clavardator.lib.users.UserInformation;
import fr.insa.clavardator.util.Log; import fr.insa.clavardator.lib.util.ErrorCallback;
import fr.insa.clavardator.lib.util.Log;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.collections.FXCollections; import javafx.collections.FXCollections;
import javafx.collections.ObservableList; import javafx.collections.ObservableList;

View file

@ -1,17 +1,20 @@
package fr.insa.clavardator.config; package fr.insa.clavardator.client.config;
import fr.insa.clavardator.server.PresenceType; import fr.insa.clavardator.client.server.ServerType;
import org.json.JSONException; import org.json.JSONException;
import org.json.JSONObject; import org.json.JSONObject;
@SuppressWarnings("FieldCanBeLocal") @SuppressWarnings("FieldCanBeLocal")
public class Config { public class Config {
private ServerConfig serverConfig;
private final String SERVER_KEY = "serveur"; private final String SERVER_KEY = "serveur";
private final String LOCAL_KEY = "local";
private ServerConfig serverConfig;
private LocalConfig localConfig;
public Config() { public Config() {
serverConfig = new ServerConfig(); serverConfig = new ServerConfig();
localConfig = new LocalConfig();
} }
public Config(JSONObject obj) { public Config(JSONObject obj) {
@ -19,29 +22,35 @@ public class Config {
if (obj.has(SERVER_KEY)) { if (obj.has(SERVER_KEY)) {
this.serverConfig = new ServerConfig(obj.getJSONObject(SERVER_KEY)); this.serverConfig = new ServerConfig(obj.getJSONObject(SERVER_KEY));
} }
if (obj.has(LOCAL_KEY)) {
this.localConfig = new LocalConfig(obj.getJSONObject(LOCAL_KEY));
}
} }
public ServerConfig getServerConfig() { public ServerConfig getServerConfig() {
return serverConfig; return serverConfig;
} }
public LocalConfig getLocalConfig() {
return localConfig;
}
@SuppressWarnings("FieldCanBeLocal") @SuppressWarnings("FieldCanBeLocal")
public static class ServerConfig { public static class ServerConfig {
public static final int DEFAULT_PRESENCE_PORT = 35650;
private JSONObject obj; public static final int DEFAULT_PROXY_PORT = 35750;
private boolean enabled;
private String uri;
private PresenceType type;
private int presencePort;
private int proxyPort;
private final String ENABLED_KEY = "actif"; private final String ENABLED_KEY = "actif";
private final String URI_KEY = "uri"; private final String URI_KEY = "uri";
private final String TYPE_KEY = "type"; private final String TYPE_KEY = "type";
private final String PORTS_KEY = "ports"; private final String PORTS_KEY = "ports";
private final String PORT_PRESENCE_KEY = "presence"; private final String PORT_PRESENCE_KEY = "presence";
private final String PORT_PROXY_KEY = "proxy"; private final String PORT_PROXY_KEY = "proxy";
private JSONObject obj;
private boolean enabled;
private String uri;
private ServerType type;
private int presencePort;
private int proxyPort;
/** /**
* Basic constructor setting the default server configuration * Basic constructor setting the default server configuration
@ -49,7 +58,7 @@ public class Config {
public ServerConfig() { public ServerConfig() {
enabled = false; enabled = false;
uri = ""; uri = "";
type = PresenceType.INSA; type = ServerType.INSA;
} }
/** /**
@ -102,9 +111,9 @@ public class Config {
*/ */
private void readServerType() { private void readServerType() {
try { try {
type = obj.getEnum(PresenceType.class, TYPE_KEY); type = obj.getEnum(ServerType.class, TYPE_KEY);
} catch (JSONException e) { } catch (JSONException e) {
type = PresenceType.INSA; type = ServerType.INSA;
} }
} }
@ -114,8 +123,8 @@ public class Config {
presencePort = portsObj.getInt(PORT_PRESENCE_KEY); presencePort = portsObj.getInt(PORT_PRESENCE_KEY);
proxyPort = portsObj.getInt(PORT_PROXY_KEY); proxyPort = portsObj.getInt(PORT_PROXY_KEY);
} catch (JSONException e) { } catch (JSONException e) {
presencePort = 30000; // TODO set default ports presencePort = DEFAULT_PRESENCE_PORT;
proxyPort = 300001; proxyPort = DEFAULT_PROXY_PORT;
} }
} }
@ -133,7 +142,7 @@ public class Config {
return uri; return uri;
} }
public PresenceType getType() { public ServerType getType() {
return type; return type;
} }
@ -145,4 +154,70 @@ public class Config {
return proxyPort; return proxyPort;
} }
} }
@SuppressWarnings("FieldCanBeLocal")
public static class LocalConfig {
public static final int DEFAULT_TCP_PORT = 31598;
private final String ENABLED_KEY = "actif";
private final String PORT_KEY = "port";
private JSONObject obj;
private boolean enabled;
private int port;
/**
* Basic constructor setting the default local configuration
*/
public LocalConfig() {
enabled = true;
port = DEFAULT_TCP_PORT;
}
/**
* Tries to read the given JSON object to extract custom user configuration
*
* @param obj THe JSON object to read config from
*/
public LocalConfig(JSONObject obj) {
this();
this.obj = obj;
readEnabled();
if (enabled) {
readServerPort();
}
}
/**
* Reads if local mode is enabled.
* Uses true by default.
*/
private void readEnabled() {
try {
enabled = obj.getInt(ENABLED_KEY) == 1;
} catch (JSONException e) {
enabled = true;
}
}
private void readServerPort() {
try {
port = obj.getInt(PORT_KEY);
} catch (JSONException e) {
port = DEFAULT_TCP_PORT;
}
}
/**
* Checks if the local mode is enabled.
* If this returns false, all other data returned by this object can be ignored.
*
* @return True if enabled, false otherwise.
*/
public boolean isEnabled() {
return enabled;
}
public int getPort() {
return port;
}
}
} }

View file

@ -1,6 +1,6 @@
package fr.insa.clavardator.config; package fr.insa.clavardator.client.config;
import fr.insa.clavardator.util.Log; import fr.insa.clavardator.lib.util.Log;
import org.json.JSONException; import org.json.JSONException;
import org.json.JSONObject; import org.json.JSONObject;
import org.json.JSONTokener; import org.json.JSONTokener;

View file

@ -1,11 +1,12 @@
package fr.insa.clavardator.db; package fr.insa.clavardator.client.db;
import fr.insa.clavardator.chat.FileMessage; import fr.insa.clavardator.client.users.CurrentUser;
import fr.insa.clavardator.chat.Message; import fr.insa.clavardator.lib.message.FileMessage;
import fr.insa.clavardator.users.User; import fr.insa.clavardator.lib.message.Message;
import fr.insa.clavardator.users.UserInformation; import fr.insa.clavardator.lib.users.User;
import fr.insa.clavardator.util.ErrorCallback; import fr.insa.clavardator.lib.users.UserInformation;
import fr.insa.clavardator.util.Log; import fr.insa.clavardator.lib.util.ErrorCallback;
import fr.insa.clavardator.lib.util.Log;
import org.intellij.lang.annotations.Language; import org.intellij.lang.annotations.Language;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
@ -274,8 +275,6 @@ public class DatabaseController {
chatHistory.add(new FileMessage(sender, recipient, date, text, filePath)); chatHistory.add(new FileMessage(sender, recipient, date, text, filePath));
} catch (IOException e) { } catch (IOException e) {
Log.e(getClass().getSimpleName(), "Error while opening the file", e); Log.e(getClass().getSimpleName(), "Error while opening the file", e);
errorCallback.onError(e);
return;
} }
} }
} }
@ -314,11 +313,17 @@ public class DatabaseController {
public void addMessage(Message message, @Nullable DatabaseController.UpdateCallback callback, ErrorCallback errorCallback) { public void addMessage(Message message, @Nullable DatabaseController.UpdateCallback callback, ErrorCallback errorCallback) {
// Insert the correspondent if not already in the database // Insert the correspondent if not already in the database
Log.v(getClass().getSimpleName(), "Inserting correspondent into db... "); Log.v(getClass().getSimpleName(), "Inserting correspondent into db... ");
addUser(message.getCorrespondent(), () -> { UserInformation correspondent;
if (CurrentUser.getInstance().getId() != null && CurrentUser.getInstance().getId().equals(message.getSender().id)) {
correspondent = message.getRecipient();
} else {
correspondent = message.getSender();
}
addUser(correspondent, () -> {
// Handle messages containing a file // Handle messages containing a file
String filePath = null; String filePath = null;
if (message instanceof FileMessage) { if (message instanceof FileMessage) {
filePath = "'" + ((FileMessage) message).getPath() + "'"; filePath = ((FileMessage) message).getPath();
} }
@Language("SQL") String sql = "INSERT INTO message " + @Language("SQL") String sql = "INSERT INTO message " +
"(timestamp, sender, recipient, text, file_path) VALUES (?, ?, ?, ?, ?)"; "(timestamp, sender, recipient, text, file_path) VALUES (?, ?, ?, ?, ?)";

View file

@ -1,13 +1,16 @@
package fr.insa.clavardator.network; package fr.insa.clavardator.client.network;
import fr.insa.clavardator.util.ErrorCallback; import fr.insa.clavardator.lib.util.ErrorCallback;
import fr.insa.clavardator.util.Log; import fr.insa.clavardator.lib.util.Log;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.io.IOException; import java.io.IOException;
import java.net.*; import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import static fr.insa.clavardator.network.NetUtil.isLocalAddress; import static fr.insa.clavardator.client.network.NetUtil.isLocalAddress;
public class NetDiscoverer { public class NetDiscoverer {
private static final short DISCOVERY_PORT = 31593; private static final short DISCOVERY_PORT = 31593;
@ -86,6 +89,14 @@ public class NetDiscoverer {
} }
private interface BroadcastReceivedCallback {
void onBroadcastReceived(InetAddress ipAddr, String data);
}
public interface ResponseReceivedCallback {
void onResponseReceived(InetAddress ipAddr, String data);
}
private static class BroadcastSender extends Thread { private static class BroadcastSender extends Thread {
private final String broadcastMessage; private final String broadcastMessage;
private final ErrorCallback errorCallback; private final ErrorCallback errorCallback;
@ -104,16 +115,22 @@ public class NetDiscoverer {
@Override @Override
public void run() { public void run() {
byte[] buf = broadcastMessage.getBytes(); byte[] buf = broadcastMessage.getBytes();
DatagramSocket broadcastSocket = null;
try { try {
for (InetAddress broadcastAddr : NetUtil.listAllBroadcastAddresses()) { for (InetAddress broadcastAddr : NetUtil.listAllBroadcastAddresses()) {
DatagramSocket broadcastSocket = new DatagramSocket(); broadcastSocket = new DatagramSocket();
broadcastSocket.setBroadcast(true); broadcastSocket.setBroadcast(true);
broadcastSocket.send(new DatagramPacket(buf, buf.length, broadcastAddr, DISCOVERY_PORT)); broadcastSocket.send(new DatagramPacket(buf, buf.length, broadcastAddr, DISCOVERY_PORT));
Log.v(this.getClass().getSimpleName(), "Broadcast sent on address " + broadcastAddr.toString()); Log.v(this.getClass().getSimpleName(), "Broadcast sent on address " + broadcastAddr.toString());
broadcastSocket.close();
} }
} catch (IOException e) { } catch (IOException e) {
Log.e(this.getClass().getSimpleName(), "Error sending broadcast", e); Log.e(this.getClass().getSimpleName(), "Error sending broadcast", e);
errorCallback.onError(e); errorCallback.onError(e);
} finally {
if (broadcastSocket != null) {
broadcastSocket.close();
}
} }
} }
} }
@ -138,15 +155,11 @@ public class NetDiscoverer {
@Override @Override
public void run() { public void run() {
try { try {
socket = new DatagramSocket(DISCOVERY_PORT);
byte[] buffer = new byte[BROADCAST_BUFFER_SIZE];
while (!shouldStop) { while (!shouldStop) {
socket = new DatagramSocket(null);
socket.setOption(StandardSocketOptions.SO_REUSEPORT, true);
socket.setOption(StandardSocketOptions.SO_REUSEADDR, true);
socket.bind(new InetSocketAddress((InetAddress) null, DISCOVERY_PORT));
byte[] buffer = new byte[BROADCAST_BUFFER_SIZE];
DatagramPacket receivedPacket = new DatagramPacket(buffer, BROADCAST_BUFFER_SIZE); DatagramPacket receivedPacket = new DatagramPacket(buffer, BROADCAST_BUFFER_SIZE);
socket.receive(receivedPacket); socket.receive(receivedPacket);
Log.v(this.getClass().getSimpleName(), "Broadcast received from ip " + receivedPacket.getAddress().toString()); Log.v(this.getClass().getSimpleName(), "Broadcast received from ip " + receivedPacket.getAddress().toString());
callback.onBroadcastReceived(receivedPacket.getAddress(), new String(receivedPacket.getData())); callback.onBroadcastReceived(receivedPacket.getAddress(), new String(receivedPacket.getData()));
@ -156,6 +169,9 @@ public class NetDiscoverer {
Log.e(this.getClass().getSimpleName(), "Error receiving broadcast message", e); Log.e(this.getClass().getSimpleName(), "Error receiving broadcast message", e);
errorCallback.onError(e); errorCallback.onError(e);
} }
} finally {
if (socket != null)
socket.close();
} }
} }
@ -186,8 +202,7 @@ public class NetDiscoverer {
@Override @Override
public void run() { public void run() {
byte[] buf = message.getBytes(); byte[] buf = message.getBytes();
try { try (DatagramSocket responseSocket = new DatagramSocket()) {
DatagramSocket responseSocket = new DatagramSocket();
responseSocket.send(new DatagramPacket(buf, buf.length, address, RESPONSE_PORT)); responseSocket.send(new DatagramPacket(buf, buf.length, address, RESPONSE_PORT));
Log.v(this.getClass().getSimpleName(), "Broadcast response sent to ip " + address.toString()); Log.v(this.getClass().getSimpleName(), "Broadcast response sent to ip " + address.toString());
} catch (IOException e) { } catch (IOException e) {
@ -217,12 +232,8 @@ public class NetDiscoverer {
@Override @Override
public void run() { public void run() {
try { try {
socket = new DatagramSocket(RESPONSE_PORT);
while (!shouldStop) { while (!shouldStop) {
socket = new DatagramSocket(null);
socket.setOption(StandardSocketOptions.SO_REUSEPORT, true);
socket.setOption(StandardSocketOptions.SO_REUSEADDR, true);
socket.bind(new InetSocketAddress((InetAddress) null, RESPONSE_PORT));
byte[] buffer = new byte[RESPONSE_BUFFER_SIZE]; byte[] buffer = new byte[RESPONSE_BUFFER_SIZE];
DatagramPacket receivedPacket = new DatagramPacket(buffer, RESPONSE_BUFFER_SIZE); DatagramPacket receivedPacket = new DatagramPacket(buffer, RESPONSE_BUFFER_SIZE);
socket.receive(receivedPacket); socket.receive(receivedPacket);
@ -243,13 +254,4 @@ public class NetDiscoverer {
} }
} }
private interface BroadcastReceivedCallback {
void onBroadcastReceived(InetAddress ipAddr, String data);
}
public interface ResponseReceivedCallback {
void onResponseReceived(InetAddress ipAddr, String data);
}
} }

View file

@ -1,6 +1,6 @@
package fr.insa.clavardator.network; package fr.insa.clavardator.client.network;
import fr.insa.clavardator.util.Log; import fr.insa.clavardator.lib.util.Log;
import java.net.InetAddress; import java.net.InetAddress;
import java.net.InterfaceAddress; import java.net.InterfaceAddress;

View file

@ -1,10 +1,11 @@
package fr.insa.clavardator.network; package fr.insa.clavardator.client.network;
import fr.insa.clavardator.errors.UsernameTakenException; import fr.insa.clavardator.client.users.CurrentUser;
import fr.insa.clavardator.users.CurrentUser; import fr.insa.clavardator.lib.errors.UsernameTakenException;
import fr.insa.clavardator.users.UserInformation; import fr.insa.clavardator.lib.network.TcpConnection;
import fr.insa.clavardator.util.ErrorCallback; import fr.insa.clavardator.lib.users.UserInformation;
import fr.insa.clavardator.util.Log; import fr.insa.clavardator.lib.util.ErrorCallback;
import fr.insa.clavardator.lib.util.Log;
import java.net.InetAddress; import java.net.InetAddress;
import java.net.Socket; import java.net.Socket;
@ -14,12 +15,13 @@ public class PeerHandshake {
private TcpConnection connection; private TcpConnection connection;
private UserInformation userInformation; private UserInformation userInformation;
public void createConnection(InetAddress ipAddr, UserConnectedCallback callback, ErrorCallback errorCallback) { public void createConnection(InetAddress ipAddr, int port, UserConnectedCallback callback, ErrorCallback errorCallback) {
closeConnection(); closeConnection();
Log.v(this.getClass().getSimpleName(), "Creating new TCP connection "); Log.v(this.getClass().getSimpleName(), "Creating new TCP connection ");
connection = new TcpConnection( connection = new TcpConnection(
ipAddr, ipAddr,
port,
(thisConnection) -> init(thisConnection, (thisConnection) -> init(thisConnection,
false, false,
callback, callback,
@ -70,7 +72,9 @@ public class PeerHandshake {
private void sendUsernameTaken(TcpConnection thisConnection) { private void sendUsernameTaken(TcpConnection thisConnection) {
Log.v(this.getClass().getSimpleName(), "Received username request using current username"); Log.v(this.getClass().getSimpleName(), "Received username request using current username");
thisConnection.send(new UsernameTakenException("Username taken"), this::closeConnection, null); UsernameTakenException exception = new UsernameTakenException("Username taken",
CurrentUser.getInstance().getId(), userInformation.id);
thisConnection.send(exception, this::closeConnection, null);
} }

View file

@ -0,0 +1,6 @@
package fr.insa.clavardator.client.server;
public enum ServerType {
INSA,
TEST,
}

View file

@ -1,12 +1,14 @@
package fr.insa.clavardator.server; package fr.insa.clavardator.client.server.presence;
import fr.insa.clavardator.network.TcpConnection; import fr.insa.clavardator.client.server.proxy.InsaProxy;
import fr.insa.clavardator.users.CurrentUser; import fr.insa.clavardator.client.server.proxy.Proxy;
import fr.insa.clavardator.users.UserInformation; import fr.insa.clavardator.lib.message.Message;
import fr.insa.clavardator.util.ErrorCallback; import fr.insa.clavardator.lib.network.TcpConnection;
import fr.insa.clavardator.util.Log; import fr.insa.clavardator.lib.users.UserInformation;
import fr.insa.clavardator.util.ParametrizedCallback; import fr.insa.clavardator.lib.util.ErrorCallback;
import fr.insa.clavardator.util.SimpleCallback; import fr.insa.clavardator.lib.util.Log;
import fr.insa.clavardator.lib.util.ParametrizedCallback;
import fr.insa.clavardator.lib.util.SimpleCallback;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.net.InetAddress; import java.net.InetAddress;
@ -26,31 +28,28 @@ import java.util.Arrays;
* such as subscribe, unsubscribe, publish, and receive notifications. * such as subscribe, unsubscribe, publish, and receive notifications.
* <br/> * <br/>
* On the proxy port, the client will be able to send regular * On the proxy port, the client will be able to send regular
* {@link fr.insa.clavardator.chat.Message messages} like on the local network. * {@link Message messages} like on the local network.
* The proxy will forward the message to the appropriate recipient using the provided id. * The proxy will forward the message to the appropriate recipient using the provided id.
*/ */
public class InsaPresence implements Presence { public class InsaPresence implements Presence {
private final String path; private final String path;
private final int presencePort; private final int presencePort;
private final int proxyPort; private final Proxy proxy;
private TcpConnection presenceConnection; private TcpConnection presenceConnection;
private TcpConnection proxyConnection;
public InsaPresence(String path, int presencePort, int proxyPort) { public InsaPresence(String path, int presencePort, int proxyPort) {
this.path = path; this.path = path;
this.presencePort = presencePort; this.presencePort = presencePort;
this.proxyPort = proxyPort; this.proxy = new InsaProxy(path, proxyPort);
} }
@Override @Override
public void subscribe(ParametrizedCallback<ArrayList<UserInformation>> callback, ErrorCallback errorCallback) { public void subscribe(UserInformation userInformation, ParametrizedCallback<ArrayList<UserInformation>> callback, ErrorCallback errorCallback) {
if (!isConnected()) { if (!isConnected()) {
connectToPresence( connectToPresence(
() -> connectToProxy(() -> { () -> proxy.connect(() -> {
sendSubscribeMessage(errorCallback); notify(userInformation, null, errorCallback);
receiveSubscribeNotifications(callback, errorCallback); receiveSubscribeNotifications(callback, errorCallback);
}, errorCallback), }, errorCallback),
errorCallback); errorCallback);
@ -59,15 +58,6 @@ public class InsaPresence implements Presence {
} }
} }
/**
* 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 * Waits for presence server response to the subscribe request
* *
@ -82,14 +72,19 @@ public class InsaPresence implements Presence {
final ArrayList<?> list = (ArrayList<?>) msg; final ArrayList<?> list = (ArrayList<?>) msg;
for (Object obj : list) { for (Object obj : list) {
if (obj instanceof UserInformation) { if (obj instanceof UserInformation) {
userList.add((UserInformation)obj); userList.add((UserInformation) obj);
} }
} }
} Log.v(getClass().getSimpleName(), "Receive subscribe response: " + msg);
Log.v(getClass().getSimpleName(), "Receive subscribe response: " + msg); Log.v(getClass().getSimpleName(), "Converted list: " + Arrays.toString(userList.toArray()));
Log.v(getClass().getSimpleName(), "Converted list: " + Arrays.toString(userList.toArray())); if (callback != null) {
if (callback != null) { callback.call(userList);
callback.call(userList); }
} else {
Log.e(getClass().getSimpleName(), "Unexpected object received: " + msg.getClass().getSimpleName());
if (errorCallback != null) {
errorCallback.onError(new RuntimeException("Unexpected object received"));
}
} }
}, errorCallback); }, errorCallback);
} }
@ -103,6 +98,11 @@ public class InsaPresence implements Presence {
} }
} }
@Override
public void notify(UserInformation newInformation, @Nullable SimpleCallback callback, @Nullable ErrorCallback errorCallback) {
presenceConnection.send(newInformation, callback, errorCallback);
}
/** /**
* Connects to the presence server by TCP * Connects to the presence server by TCP
* *
@ -124,27 +124,6 @@ public class InsaPresence implements Presence {
} }
} }
/**
* 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 * Closes the given connection
*/ */
@ -162,8 +141,7 @@ public class InsaPresence implements Presence {
private boolean isConnected() { private boolean isConnected() {
return presenceConnection != null && return presenceConnection != null &&
presenceConnection.isOpen() && presenceConnection.isOpen() &&
proxyConnection != null && proxy.isConnected();
proxyConnection.isOpen();
} }
/** /**
@ -172,13 +150,12 @@ public class InsaPresence implements Presence {
private void disconnect() { private void disconnect() {
Log.v(this.getClass().getSimpleName(), "Disconnecting presence server"); Log.v(this.getClass().getSimpleName(), "Disconnecting presence server");
closeConnection(presenceConnection); closeConnection(presenceConnection);
closeConnection(proxyConnection);
presenceConnection = null; presenceConnection = null;
proxyConnection = null; proxy.close();
} }
@Override @Override
public TcpConnection getProxyConnection() { public Proxy getProxy() {
return proxyConnection; return proxy;
} }
} }

View file

@ -0,0 +1,55 @@
package fr.insa.clavardator.client.server.presence;
import fr.insa.clavardator.client.server.proxy.Proxy;
import fr.insa.clavardator.lib.users.UserInformation;
import fr.insa.clavardator.lib.util.ErrorCallback;
import fr.insa.clavardator.lib.util.ParametrizedCallback;
import fr.insa.clavardator.lib.util.SimpleCallback;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
/**
* Interface exposing public methods necessary for any presence server.
*
* @implNote Implement this interface when creating your own presence server class,
* then update the {@link PresenceFactory factory}
* to add your new implementation.
*/
public interface Presence {
/**
* Subscribes to this presence server notifications and publish current status.
* A list of Ids representing the current active users is returned.
*
* @param callback Called when subscription completes
*/
void subscribe(UserInformation userInformation, ParametrizedCallback<ArrayList<UserInformation>> callback, @Nullable ErrorCallback errorCallback);
/**
* Stops subscription to the presence server by closing TCP connections.
* This will stop notifications.
*
* @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, @Nullable ErrorCallback errorCallback);
/**
* Notifies the presence server of any changes to the current user
*
* @param newInformation The new information to send
* @param callback Called when notify completes
*/
void notify(UserInformation newInformation, @Nullable SimpleCallback callback, @Nullable ErrorCallback errorCallback);
/**
* Gets the proxy.
* This can be used to initialize a
* {@link fr.insa.clavardator.client.users.PeerUser Peeruser}
* and send messages like on a local network.
*
* @return The proxy
*/
Proxy getProxy();
}

View file

@ -1,10 +1,12 @@
package fr.insa.clavardator.server; package fr.insa.clavardator.client.server.presence;
import fr.insa.clavardator.client.server.ServerType;
/** /**
* Static factory class used to create concrete presence server implementations. * Static factory class used to create concrete presence server implementations.
*/ */
public class PresenceFactory { public class PresenceFactory {
public static Presence create(PresenceType type, String uri, int serverPort, int proxyPort) throws UnknownPresenceException { public static Presence create(ServerType type, String uri, int serverPort, int proxyPort) throws UnknownPresenceException {
switch (type) { switch (type) {
case INSA: case INSA:
return new InsaPresence(uri, serverPort, proxyPort); return new InsaPresence(uri, serverPort, proxyPort);

View file

@ -1,4 +1,4 @@
package fr.insa.clavardator.server; package fr.insa.clavardator.client.server.presence;
public class UnknownPresenceException extends Exception { public class UnknownPresenceException extends Exception {
public UnknownPresenceException() { public UnknownPresenceException() {

View file

@ -0,0 +1,86 @@
package fr.insa.clavardator.client.server.proxy;
import fr.insa.clavardator.client.users.CurrentUser;
import fr.insa.clavardator.lib.message.SenderInfo;
import fr.insa.clavardator.lib.network.TcpConnection;
import fr.insa.clavardator.lib.users.UserInformation;
import fr.insa.clavardator.lib.util.ErrorCallback;
import fr.insa.clavardator.lib.util.Log;
import fr.insa.clavardator.lib.util.SimpleCallback;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.io.Serializable;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.HashMap;
import java.util.Map;
public class InsaProxy implements Proxy {
private final String path;
private final int proxyPort;
TcpConnection proxyConnection;
Map<String, TcpConnection.MessageReceivedCallback> callbackMap = new HashMap<>();
private boolean receiving = false;
public InsaProxy(String path, int proxyPort) {
this.path = path;
this.proxyPort = proxyPort;
}
public void connect(SimpleCallback callback, ErrorCallback errorCallback) {
try {
proxyConnection = new TcpConnection(InetAddress.getByName(path), proxyPort,
(newConnection) -> newConnection.send(new UserInformation(CurrentUser.getInstance()), () -> {
if (callback != null) {
callback.call();
}
}, errorCallback),
errorCallback);
} catch (UnknownHostException e) {
Log.e(getClass().getSimpleName(), "Could not connect to the proxy", e);
}
}
public void send(Serializable message, @Nullable SimpleCallback callback, @Nullable ErrorCallback errorCallback) {
proxyConnection.send(message, callback, errorCallback);
}
public void receive(String userId, TcpConnection.MessageReceivedCallback callback, ErrorCallback errorCallback) {
Log.v(getClass().getSimpleName(), "Registering a new receive request from " + userId);
callbackMap.put(userId, callback);
if (!receiving) {
proxyConnection.receive(this::onMessageReceived, errorCallback);
receiving = true;
}
}
private void onMessageReceived(Object msg) throws IOException {
if (msg instanceof SenderInfo) {
TcpConnection.MessageReceivedCallback callback = callbackMap.get(((SenderInfo) msg).getSenderId());
if (callback != null) {
callback.onMessageReceived(msg);
} else {
Log.w(getClass().getSimpleName(),
"Nobody is waiting for messages from " + ((SenderInfo) msg).getSenderId());
}
} else {
Log.e(getClass().getSimpleName(),
"Proxy sent a message that does not contain sender information: " +
msg.getClass().getSimpleName());
}
}
public boolean isConnected() {
return proxyConnection != null &&
proxyConnection.isOpen();
}
public void close() {
proxyConnection.close();
}
public void disconnectUser(String id) {
callbackMap.remove(id);
}
}

View file

@ -0,0 +1,22 @@
package fr.insa.clavardator.client.server.proxy;
import fr.insa.clavardator.lib.network.TcpConnection;
import fr.insa.clavardator.lib.util.ErrorCallback;
import fr.insa.clavardator.lib.util.SimpleCallback;
import org.jetbrains.annotations.Nullable;
import java.io.Serializable;
public interface Proxy {
void connect(SimpleCallback callback, ErrorCallback errorCallback);
void send(Serializable message, @Nullable SimpleCallback callback, @Nullable ErrorCallback errorCallback);
void receive(String userId, TcpConnection.MessageReceivedCallback callback, ErrorCallback errorCallback);
boolean isConnected();
void close();
void disconnectUser(String id);
}

View file

@ -1,8 +1,8 @@
package fr.insa.clavardator.ui; package fr.insa.clavardator.client.ui;
/** /**
* Interface used to create callbacks for button press events * Interface used to create callbacks for button press events
*/ */
public interface ButtonPressEvent { public interface ButtonPressEvent {
public void onPress(); void onPress();
} }

View file

@ -1,4 +1,4 @@
package fr.insa.clavardator.ui; package fr.insa.clavardator.client.ui;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.fxml.Initializable; import javafx.fxml.Initializable;

View file

@ -1,4 +1,4 @@
package fr.insa.clavardator.ui; package fr.insa.clavardator.client.ui;
import javafx.collections.ObservableList; import javafx.collections.ObservableList;
import javafx.fxml.FXML; import javafx.fxml.FXML;
@ -35,6 +35,7 @@ public class LoadingScreenController implements Initializable {
/** /**
* Instantly shows the loading screen * Instantly shows the loading screen
*
* @param label The text to display bellow the loading indicator * @param label The text to display bellow the loading indicator
*/ */
public void show(String label) { public void show(String label) {
@ -51,6 +52,7 @@ public class LoadingScreenController implements Initializable {
/** /**
* Sets the text to display bellow the loading indicator * Sets the text to display bellow the loading indicator
*
* @param label The text to use * @param label The text to use
*/ */
public void setLabel(@Nullable String label) { public void setLabel(@Nullable String label) {
@ -62,6 +64,7 @@ public class LoadingScreenController implements Initializable {
/** /**
* Gets the main container styles to apply custom styling * Gets the main container styles to apply custom styling
*
* @return The style list * @return The style list
*/ */
public ObservableList<String> getRootStyle() { public ObservableList<String> getRootStyle() {

View file

@ -1,21 +1,22 @@
package fr.insa.clavardator.ui; package fr.insa.clavardator.client.ui;
import com.jfoenix.controls.JFXSnackbar; import com.jfoenix.controls.JFXSnackbar;
import fr.insa.clavardator.config.Config; import fr.insa.clavardator.client.config.Config;
import fr.insa.clavardator.config.ConfigLoader; import fr.insa.clavardator.client.config.ConfigLoader;
import fr.insa.clavardator.db.DatabaseController; import fr.insa.clavardator.client.db.DatabaseController;
import fr.insa.clavardator.server.Presence; import fr.insa.clavardator.client.server.ServerType;
import fr.insa.clavardator.server.PresenceFactory; import fr.insa.clavardator.client.server.presence.Presence;
import fr.insa.clavardator.server.PresenceType; import fr.insa.clavardator.client.server.presence.PresenceFactory;
import fr.insa.clavardator.server.UnknownPresenceException; import fr.insa.clavardator.client.server.presence.UnknownPresenceException;
import fr.insa.clavardator.ui.chat.ChatController; import fr.insa.clavardator.client.ui.chat.ChatController;
import fr.insa.clavardator.ui.dialogs.AboutDialogController; import fr.insa.clavardator.client.ui.dialogs.AboutDialogController;
import fr.insa.clavardator.ui.dialogs.EditUsernameDialogController; import fr.insa.clavardator.client.ui.dialogs.EditUsernameDialogController;
import fr.insa.clavardator.ui.dialogs.SnackbarController; import fr.insa.clavardator.client.ui.dialogs.SnackbarController;
import fr.insa.clavardator.ui.users.UserListController; import fr.insa.clavardator.client.ui.users.UserListController;
import fr.insa.clavardator.users.CurrentUser; import fr.insa.clavardator.client.users.CurrentUser;
import fr.insa.clavardator.users.UserList; import fr.insa.clavardator.client.users.UserList;
import fr.insa.clavardator.util.Log; import fr.insa.clavardator.lib.users.UserInformation;
import fr.insa.clavardator.lib.util.Log;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader; import javafx.fxml.FXMLLoader;
@ -31,6 +32,7 @@ import java.util.TimerTask;
public class MainController implements Initializable { public class MainController implements Initializable {
private final CurrentUser currentUser; private final CurrentUser currentUser;
private final int DEFAULT_PORT = 31598;
@FXML @FXML
private StackPane root; private StackPane root;
@FXML @FXML
@ -49,23 +51,42 @@ public class MainController implements Initializable {
private LoadingScreenController loadingController; private LoadingScreenController loadingController;
@FXML @FXML
private ErrorScreenController errorController; private ErrorScreenController errorController;
private JFXSnackbar snackbar; private JFXSnackbar snackbar;
private UserList userList; private UserList userList;
private Presence presenceServer; private Presence presenceServer;
private boolean online; private boolean online;
private int port = DEFAULT_PORT;
private boolean localEnabled = true;
public MainController() { public MainController() {
online = false; online = false;
currentUser = CurrentUser.getInstance(); currentUser = CurrentUser.getInstance();
currentUser.addObserver(propertyChangeEvent -> { currentUser.addObserver(propertyChangeEvent -> {
if (propertyChangeEvent.getPropertyName().equals("state")) { final String propertyName = propertyChangeEvent.getPropertyName();
if (propertyName.equals("state")) {
final CurrentUser.State newState = (CurrentUser.State) propertyChangeEvent.getNewValue(); final CurrentUser.State newState = (CurrentUser.State) propertyChangeEvent.getNewValue();
onCurrentUserStateChange(newState); onCurrentUserStateChange(newState);
} else if (propertyName.equals("username")) {
final String newUsername = (String) propertyChangeEvent.getNewValue();
onCurrentUserNameChange(newUsername);
} }
}); });
} }
private void onCurrentUserNameChange(String newUsername) {
if (online) {
if (userList != null) {
userList.propagateUsernameChange();
}
if (presenceServer != null) {
presenceServer.notify(
new UserInformation(CurrentUser.getInstance().getId(), newUsername),
null,
null);
}
}
}
/** /**
* If the current user becomes valid, start the chat. * If the current user becomes valid, start the chat.
* If it is invalid or not set, show the login screen. * If it is invalid or not set, show the login screen.
@ -149,11 +170,13 @@ public class MainController implements Initializable {
* If any error happens, disconnect the user from chat and ask for reconnection. * If any error happens, disconnect the user from chat and ask for reconnection.
*/ */
private void discoverActiveUsers() { private void discoverActiveUsers() {
if (userList != null && online) { if (userList != null && online && localEnabled) {
userList.discoverActiveUsers((e) -> { userList.discoverActiveUsers(
Log.e(this.getClass().getSimpleName(), "Error discovering users", e); port,
CurrentUser.getInstance().setState(CurrentUser.State.INVALID); (e) -> {
}); Log.e(this.getClass().getSimpleName(), "Error discovering users", e);
CurrentUser.getInstance().setState(CurrentUser.State.INVALID);
});
} }
} }
@ -162,12 +185,14 @@ public class MainController implements Initializable {
* If any error happens, disconnect the user from chat and ask for reconnection. * If any error happens, disconnect the user from chat and ask for reconnection.
*/ */
private void startListening() { private void startListening() {
if (userList != null) { if (userList != null && online && localEnabled) {
userList.startDiscoveryListening(); userList.startDiscoveryListening();
userList.startUserListening((e) -> { userList.startUserListening(
Log.e(this.getClass().getSimpleName(), "Error listening to users", e); port,
CurrentUser.getInstance().setState(CurrentUser.State.INVALID); (e) -> {
}); Log.e(this.getClass().getSimpleName(), "Error listening to users", e);
CurrentUser.getInstance().setState(CurrentUser.State.INVALID);
});
} }
} }
@ -249,25 +274,37 @@ public class MainController implements Initializable {
*/ */
private void initBackend() { private void initBackend() {
ConfigLoader.load( ConfigLoader.load(
(Config config) -> new DatabaseController().initTables( (Config config) -> {
() -> userList.retrievedPreviousUsers( initLocalNetwork(config.getLocalConfig());
() -> currentUser.init( new DatabaseController().initTables(
() -> initPresenceServer(config), () -> userList.retrievedPreviousUsers(
this::onInitError), () -> currentUser.init(
this::onInitError () -> initPresenceServer(config.getServerConfig()),
), this::onInitError)); this::onInitError),
this::onInitError
), this::onInitError);
});
}
private void initLocalNetwork(Config.LocalConfig localConfig) {
port = localConfig.getPort();
localEnabled = localConfig.isEnabled();
if (localEnabled) {
Log.v(getClass().getSimpleName(), "Local network support enabled on port: " + port);
} else {
Log.v(getClass().getSimpleName(), "Local network support disabled");
}
} }
/** /**
* Initializes the presence server based on user config * Initializes the presence server based on user config
* *
* @param config The user config * @param serverConfig The server config
*/ */
public void initPresenceServer(Config config) { private void initPresenceServer(Config.ServerConfig serverConfig) {
final Config.ServerConfig serverConfig = config.getServerConfig();
if (serverConfig.isEnabled()) { if (serverConfig.isEnabled()) {
try { try {
final PresenceType type = serverConfig.getType(); final ServerType type = serverConfig.getType();
final String uri = serverConfig.getUri(); final String uri = serverConfig.getUri();
final int presencePort = serverConfig.getPresencePort(); final int presencePort = serverConfig.getPresencePort();
final int proxyPort = serverConfig.getProxyPort(); final int proxyPort = serverConfig.getProxyPort();
@ -286,13 +323,14 @@ public class MainController implements Initializable {
private void subscribeToPresenceServer() { private void subscribeToPresenceServer() {
if (presenceServer != null && online) { if (presenceServer != null && online) {
presenceServer.subscribe( presenceServer.subscribe(
new UserInformation(CurrentUser.getInstance()),
param -> userList.onReceivePresenceNotification( param -> userList.onReceivePresenceNotification(
param, param,
presenceServer.getProxyConnection()), presenceServer.getProxy()),
(e) -> Log.v( (e) -> Log.v(
getClass().getSimpleName(), getClass().getSimpleName(),
"Error subscribing to presence server", "Error subscribing to presence server",
e)); e));
} }
} }

View file

@ -1,4 +1,4 @@
package fr.insa.clavardator.ui; package fr.insa.clavardator.client.ui;
import javafx.collections.FXCollections; import javafx.collections.FXCollections;
import javafx.collections.ObservableList; import javafx.collections.ObservableList;

View file

@ -1,14 +1,12 @@
package fr.insa.clavardator.ui; package fr.insa.clavardator.client.ui;
import fr.insa.clavardator.users.CurrentUser; import fr.insa.clavardator.client.users.CurrentUser;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.fxml.Initializable; import javafx.fxml.Initializable;
import javafx.scene.control.Label; import javafx.scene.control.Label;
import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeEvent;
import java.net.URL; import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.ResourceBundle; import java.util.ResourceBundle;
/** /**
@ -25,6 +23,7 @@ public class ToolbarController implements Initializable {
public void setEditListener(ButtonPressEvent listener) { public void setEditListener(ButtonPressEvent listener) {
editListeners = listener; editListeners = listener;
} }
public void setAboutListener(ButtonPressEvent listener) { public void setAboutListener(ButtonPressEvent listener) {
aboutListeners = listener; aboutListeners = listener;
} }
@ -34,6 +33,7 @@ public class ToolbarController implements Initializable {
editListeners.onPress(); editListeners.onPress();
} }
} }
public void onAboutPress() { public void onAboutPress() {
if (aboutListeners != null) { if (aboutListeners != null) {
aboutListeners.onPress(); aboutListeners.onPress();

View file

@ -0,0 +1,7 @@
package fr.insa.clavardator.client.ui;
import fr.insa.clavardator.client.users.PeerUser;
public interface UserSelectedEvent {
void onSelected(PeerUser user);
}

View file

@ -1,12 +1,12 @@
package fr.insa.clavardator.ui.chat; package fr.insa.clavardator.client.ui.chat;
import fr.insa.clavardator.chat.ChatHistory; import fr.insa.clavardator.client.chat.ChatHistory;
import fr.insa.clavardator.chat.Message; import fr.insa.clavardator.client.ui.LoadingScreenController;
import fr.insa.clavardator.ui.LoadingScreenController; import fr.insa.clavardator.client.ui.NoSelectionModel;
import fr.insa.clavardator.ui.NoSelectionModel; import fr.insa.clavardator.client.users.PeerUser;
import fr.insa.clavardator.users.PeerUser; import fr.insa.clavardator.lib.message.Message;
import fr.insa.clavardator.util.ErrorCallback; import fr.insa.clavardator.lib.util.ErrorCallback;
import fr.insa.clavardator.util.Log; import fr.insa.clavardator.lib.util.Log;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.collections.ListChangeListener; import javafx.collections.ListChangeListener;
import javafx.fxml.FXML; import javafx.fxml.FXML;
@ -36,18 +36,19 @@ public class ChatController implements Initializable {
private VBox emptyContainer; private VBox emptyContainer;
private PeerUser remoteUser; private PeerUser remoteUser;
// public void setAttachmentListener(ButtonPressEvent listener) { // public void setAttachmentListener(ButtonPressEvent listener) {
// chatFooterController.setAttachmentListener(listener); // chatFooterController.setAttachmentListener(listener);
// } // }
public void setSendErrorListener(ErrorCallback listener) { public void setSendErrorListener(ErrorCallback listener) {
chatFooterController.setSendErrorListener(listener); chatFooterController.setSendErrorListener(listener);
} }
/** /**
* Check the user that finished loading is the right one then set the chat state to done * Check the user that finished loading is the right one then set the chat state to done
*
* @param user The user that finished loading * @param user The user that finished loading
*/ */
private void onHistoryLoaded (PeerUser user) { private void onHistoryLoaded(PeerUser user) {
if (user.equals(remoteUser)) { if (user.equals(remoteUser)) {
setState(State.DONE); setState(State.DONE);
scrollToEnd(); scrollToEnd();

View file

@ -1,10 +1,11 @@
package fr.insa.clavardator.ui.chat; package fr.insa.clavardator.client.ui.chat;
import com.jfoenix.controls.JFXButton; import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXTextField; import com.jfoenix.controls.JFXTextField;
import fr.insa.clavardator.users.PeerUser; import fr.insa.clavardator.client.users.PeerUser;
import fr.insa.clavardator.util.ErrorCallback; import fr.insa.clavardator.lib.users.UserState;
import fr.insa.clavardator.util.Log; import fr.insa.clavardator.lib.util.ErrorCallback;
import fr.insa.clavardator.lib.util.Log;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.beans.value.ObservableValue; import javafx.beans.value.ObservableValue;
import javafx.fxml.FXML; import javafx.fxml.FXML;
@ -23,6 +24,8 @@ import java.util.ResourceBundle;
* Controller for the chat input field and associated buttons * Controller for the chat input field and associated buttons
*/ */
public class ChatFooterController implements Initializable { public class ChatFooterController implements Initializable {
FileChooser fileChooser = new FileChooser();
File attachedFile;
@FXML @FXML
private HBox container; private HBox container;
@FXML @FXML
@ -31,15 +34,10 @@ public class ChatFooterController implements Initializable {
private JFXButton sendButton; private JFXButton sendButton;
@FXML @FXML
private JFXButton attachButton; private JFXButton attachButton;
private ErrorCallback sendErrorListeners; private ErrorCallback sendErrorListeners;
private PeerUser remoteUser; private PeerUser remoteUser;
private HashMap<PeerUser, String> savedText; private HashMap<PeerUser, String> savedText;
FileChooser fileChooser = new FileChooser();
File attachedFile;
public void setSendErrorListener(ErrorCallback listener) { public void setSendErrorListener(ErrorCallback listener) {
sendErrorListeners = listener; sendErrorListeners = listener;
} }
@ -157,8 +155,8 @@ public class ChatFooterController implements Initializable {
private void onUserStateChange(PropertyChangeEvent propertyChangeEvent) { private void onUserStateChange(PropertyChangeEvent propertyChangeEvent) {
final String propName = propertyChangeEvent.getPropertyName(); final String propName = propertyChangeEvent.getPropertyName();
if (propName.equals("state")) { if (propName.equals("state")) {
final PeerUser.State newState = (PeerUser.State) propertyChangeEvent.getNewValue(); final UserState newState = (UserState) propertyChangeEvent.getNewValue();
Platform.runLater(() -> setEnabled(newState == PeerUser.State.CONNECTED)); Platform.runLater(() -> setEnabled(newState == UserState.CONNECTED));
} }
} }

View file

@ -1,7 +1,7 @@
package fr.insa.clavardator.ui.chat; package fr.insa.clavardator.client.ui.chat;
import fr.insa.clavardator.ui.users.UserActiveIndicatorController; import fr.insa.clavardator.client.ui.users.UserActiveIndicatorController;
import fr.insa.clavardator.users.PeerUser; import fr.insa.clavardator.client.users.PeerUser;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.fxml.Initializable; import javafx.fxml.Initializable;
@ -9,7 +9,6 @@ import javafx.scene.control.Label;
import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeEvent;
import java.net.URL; import java.net.URL;
import java.util.ArrayList;
import java.util.ResourceBundle; import java.util.ResourceBundle;
/** /**
@ -53,5 +52,6 @@ public class ChatHeaderController implements Initializable {
} }
@Override @Override
public void initialize(URL location, ResourceBundle resources) {} public void initialize(URL location, ResourceBundle resources) {
}
} }

View file

@ -1,7 +1,6 @@
package fr.insa.clavardator.ui.chat; package fr.insa.clavardator.client.ui.chat;
import fr.insa.clavardator.chat.Message; import fr.insa.clavardator.lib.message.Message;
import javafx.application.Platform;
import javafx.fxml.FXMLLoader; import javafx.fxml.FXMLLoader;
import javafx.scene.Node; import javafx.scene.Node;
import javafx.scene.control.ListCell; import javafx.scene.control.ListCell;

View file

@ -1,9 +1,9 @@
package fr.insa.clavardator.ui.chat; package fr.insa.clavardator.client.ui.chat;
import com.jfoenix.controls.JFXButton; import com.jfoenix.controls.JFXButton;
import fr.insa.clavardator.chat.FileMessage; import fr.insa.clavardator.client.users.CurrentUser;
import fr.insa.clavardator.chat.Message; import fr.insa.clavardator.lib.message.FileMessage;
import fr.insa.clavardator.users.CurrentUser; import fr.insa.clavardator.lib.message.Message;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.fxml.Initializable; import javafx.fxml.Initializable;
import javafx.geometry.Pos; import javafx.geometry.Pos;
@ -30,6 +30,7 @@ public class MessageListItemController implements Initializable {
private Label timestamp; private Label timestamp;
@FXML @FXML
private FontIcon attachmentIcon; private FontIcon attachmentIcon;
/** /**
* Sets the message to display * Sets the message to display
* *
@ -108,5 +109,6 @@ public class MessageListItemController implements Initializable {
} }
@Override @Override
public void initialize(URL url, ResourceBundle rb) {} public void initialize(URL url, ResourceBundle rb) {
}
} }

View file

@ -1,4 +1,4 @@
package fr.insa.clavardator.ui.dialogs; package fr.insa.clavardator.client.ui.dialogs;
import com.jfoenix.controls.JFXDialog; import com.jfoenix.controls.JFXDialog;
import javafx.fxml.FXML; import javafx.fxml.FXML;

View file

@ -1,12 +1,12 @@
package fr.insa.clavardator.ui.dialogs; package fr.insa.clavardator.client.ui.dialogs;
import com.jfoenix.controls.JFXButton; import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXDialog; import com.jfoenix.controls.JFXDialog;
import com.jfoenix.controls.JFXTextField; 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.client.ui.ButtonPressEvent;
import fr.insa.clavardator.users.CurrentUser; import fr.insa.clavardator.client.users.CurrentUser;
import fr.insa.clavardator.users.UserList; import fr.insa.clavardator.client.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;
@ -67,6 +67,7 @@ public class EditUsernameDialogController implements Initializable {
public void setOnSuccessListener(ButtonPressEvent listener) { public void setOnSuccessListener(ButtonPressEvent listener) {
this.successListener = listener; this.successListener = listener;
} }
public void setOnCancelListener(ButtonPressEvent listener) { public void setOnCancelListener(ButtonPressEvent listener) {
this.cancelListener = listener; this.cancelListener = listener;
} }
@ -74,12 +75,11 @@ public class EditUsernameDialogController implements Initializable {
/** /**
* Plays the dialog show animation * Plays the dialog show animation
* *
* @param root The dialog's root component
* @param mode The dialog display mode
* @implNote WARNING: Do not try to open the dialog instantly after closing it. * @implNote WARNING: Do not try to open the dialog instantly after closing it.
* Due to JFoenix animations, the dialog will not reopen. * Due to JFoenix animations, the dialog will not reopen.
* If you must, wait at least 500ms before reopening. * If you must, wait at least 500ms before reopening.
*
* @param root The dialog's root component
* @param mode The dialog display mode
*/ */
public void show(StackPane root, Mode mode) { public void show(StackPane root, Mode mode) {
setLocked(false); setLocked(false);
@ -179,9 +179,6 @@ public class EditUsernameDialogController implements Initializable {
if (successListener != null) { if (successListener != null) {
successListener.onPress(); successListener.onPress();
} }
if (userList != null) {
userList.propagateUsernameChange();
}
} }
/** /**
@ -203,7 +200,7 @@ public class EditUsernameDialogController implements Initializable {
textField.setOnKeyPressed(event -> { textField.setOnKeyPressed(event -> {
if (event.getCode() == KeyCode.ENTER) { if (event.getCode() == KeyCode.ENTER) {
event.consume(); event.consume();
if(!textField.getText().isEmpty()){ if (!textField.getText().isEmpty()) {
onConfirm(); onConfirm();
} }
} }

View file

@ -1,4 +1,4 @@
package fr.insa.clavardator.ui.dialogs; package fr.insa.clavardator.client.ui.dialogs;
import com.jfoenix.effects.JFXDepthManager; import com.jfoenix.effects.JFXDepthManager;
import javafx.collections.ObservableList; import javafx.collections.ObservableList;
@ -9,7 +9,6 @@ import javafx.scene.layout.HBox;
import org.kordamp.ikonli.javafx.FontIcon; import org.kordamp.ikonli.javafx.FontIcon;
import java.net.URL; import java.net.URL;
import java.util.Arrays;
import java.util.ResourceBundle; import java.util.ResourceBundle;
/** /**

View file

@ -1,6 +1,7 @@
package fr.insa.clavardator.ui.users; package fr.insa.clavardator.client.ui.users;
import fr.insa.clavardator.users.PeerUser; import fr.insa.clavardator.client.users.PeerUser;
import fr.insa.clavardator.lib.users.UserState;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.fxml.Initializable; import javafx.fxml.Initializable;
@ -41,8 +42,8 @@ public class UserActiveIndicatorController implements Initializable {
*/ */
private void onStateChange(PropertyChangeEvent propertyChangeEvent) { private void onStateChange(PropertyChangeEvent propertyChangeEvent) {
if (propertyChangeEvent.getPropertyName().equals("state")) { if (propertyChangeEvent.getPropertyName().equals("state")) {
final PeerUser.State newState = (PeerUser.State) propertyChangeEvent.getNewValue(); final UserState newState = (UserState) propertyChangeEvent.getNewValue();
Platform.runLater(() -> updateState(newState == PeerUser.State.CONNECTED)); Platform.runLater(() -> updateState(newState == UserState.CONNECTED));
} }
} }
@ -71,6 +72,7 @@ public class UserActiveIndicatorController implements Initializable {
} }
@Override @Override
public void initialize(URL location, ResourceBundle resources) {} public void initialize(URL location, ResourceBundle resources) {
}
} }

View file

@ -1,11 +1,11 @@
package fr.insa.clavardator.ui.users; package fr.insa.clavardator.client.ui.users;
import com.jfoenix.controls.JFXButton; import com.jfoenix.controls.JFXButton;
import fr.insa.clavardator.ui.ButtonPressEvent; import fr.insa.clavardator.client.ui.ButtonPressEvent;
import fr.insa.clavardator.ui.UserSelectedEvent; import fr.insa.clavardator.client.ui.UserSelectedEvent;
import fr.insa.clavardator.users.PeerUser; import fr.insa.clavardator.client.users.PeerUser;
import fr.insa.clavardator.users.User; import fr.insa.clavardator.client.users.UserList;
import fr.insa.clavardator.users.UserList; import fr.insa.clavardator.lib.users.User;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.fxml.Initializable; import javafx.fxml.Initializable;
import javafx.scene.control.ListView; import javafx.scene.control.ListView;
@ -22,14 +22,17 @@ public class UserListController implements Initializable {
private ButtonPressEvent refreshUserListener; private ButtonPressEvent refreshUserListener;
private UserSelectedEvent userSelectedListener; private UserSelectedEvent userSelectedListener;
public UserListController() {} public UserListController() {
}
public void setRefreshUserListener(ButtonPressEvent listener) { public void setRefreshUserListener(ButtonPressEvent listener) {
refreshUserListener = listener; refreshUserListener = listener;
} }
public void setUserSelectedListener(UserSelectedEvent listener) { public void setUserSelectedListener(UserSelectedEvent listener) {
userSelectedListener = listener; userSelectedListener = listener;
} }
public void onRefreshUserListPress() { public void onRefreshUserListPress() {
if (refreshUserListener != null) { if (refreshUserListener != null) {
refreshUserListener.onPress(); refreshUserListener.onPress();
@ -38,6 +41,7 @@ public class UserListController implements Initializable {
/** /**
* Enables or disables the refresh button * Enables or disables the refresh button
*
* @param enabled True to enable, false otherwise * @param enabled True to enable, false otherwise
*/ */
public void setRefreshButtonEnabled(boolean enabled) { public void setRefreshButtonEnabled(boolean enabled) {
@ -63,6 +67,7 @@ public class UserListController implements Initializable {
/** /**
* Sets the user list to subscribe to * Sets the user list to subscribe to
*
* @param userList The user list to use * @param userList The user list to use
*/ */
public void setUserList(UserList userList) { public void setUserList(UserList userList) {

View file

@ -1,7 +1,7 @@
package fr.insa.clavardator.ui.users; package fr.insa.clavardator.client.ui.users;
import fr.insa.clavardator.ui.UserSelectedEvent; import fr.insa.clavardator.client.ui.UserSelectedEvent;
import fr.insa.clavardator.users.PeerUser; import fr.insa.clavardator.client.users.PeerUser;
import javafx.fxml.FXMLLoader; import javafx.fxml.FXMLLoader;
import javafx.scene.Node; import javafx.scene.Node;
import javafx.scene.control.ListCell; import javafx.scene.control.ListCell;

View file

@ -1,8 +1,9 @@
package fr.insa.clavardator.ui.users; package fr.insa.clavardator.client.ui.users;
import com.jfoenix.controls.JFXButton; import com.jfoenix.controls.JFXButton;
import fr.insa.clavardator.ui.ButtonPressEvent; import fr.insa.clavardator.client.ui.ButtonPressEvent;
import fr.insa.clavardator.users.PeerUser; import fr.insa.clavardator.client.users.PeerUser;
import fr.insa.clavardator.lib.users.UserState;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.fxml.Initializable; import javafx.fxml.Initializable;
@ -56,8 +57,8 @@ public class UserListItemController implements Initializable {
final String newUsername = (String) propertyChangeEvent.getNewValue(); final String newUsername = (String) propertyChangeEvent.getNewValue();
Platform.runLater(() -> button.setText(newUsername)); Platform.runLater(() -> button.setText(newUsername));
} else if (propName.equals("state") && !selected) { } else if (propName.equals("state") && !selected) {
final PeerUser.State newState = (PeerUser.State) propertyChangeEvent.getNewValue(); final UserState newState = (UserState) propertyChangeEvent.getNewValue();
Platform.runLater(() -> resetBackground(newState == PeerUser.State.CONNECTED)); Platform.runLater(() -> resetBackground(newState == UserState.CONNECTED));
} }
} }
@ -110,5 +111,6 @@ public class UserListItemController implements Initializable {
} }
@Override @Override
public void initialize(URL location, ResourceBundle resources) {} public void initialize(URL location, ResourceBundle resources) {
}
} }

View file

@ -1,8 +1,10 @@
package fr.insa.clavardator.users; package fr.insa.clavardator.client.users;
import fr.insa.clavardator.db.DatabaseController; import fr.insa.clavardator.client.db.DatabaseController;
import fr.insa.clavardator.util.ErrorCallback; import fr.insa.clavardator.lib.users.User;
import fr.insa.clavardator.util.Log; import fr.insa.clavardator.lib.users.UserInformation;
import fr.insa.clavardator.lib.util.ErrorCallback;
import fr.insa.clavardator.lib.util.Log;
import java.util.UUID; import java.util.UUID;
@ -33,8 +35,8 @@ public class CurrentUser extends User {
final DatabaseController db = new DatabaseController(); final DatabaseController db = new DatabaseController();
db.getCurrentUser((user) -> { db.getCurrentUser((user) -> {
if (user == null) { if (user == null) {
id = generateUniqueId(); setId(generateUniqueId());
Log.v(getClass().getSimpleName(), "No previous user found, generating id: " + id); Log.v(getClass().getSimpleName(), "No previous user found, generating id: " + getId());
db.addCurrentUser( db.addCurrentUser(
new UserInformation(this), new UserInformation(this),
() -> { () -> {
@ -43,9 +45,9 @@ public class CurrentUser extends User {
}, },
errorCallback); errorCallback);
} else { } else {
id = user.id; setId(user.id);
if (user.getUsername() != null) { if (user.getUsername() != null) {
Log.v(getClass().getSimpleName(), "Last user found : " + id + " / " + getUsername()); Log.v(getClass().getSimpleName(), "Last user found : " + getId() + " / " + getUsername());
setUsername(user.getUsername()); setUsername(user.getUsername());
} else { } else {
Log.v(getClass().getSimpleName(), "No username found, asking user"); Log.v(getClass().getSimpleName(), "No username found, asking user");
@ -75,7 +77,7 @@ public class CurrentUser extends User {
public void setState(State state) { public void setState(State state) {
Log.v(this.getClass().getSimpleName(), Log.v(this.getClass().getSimpleName(),
"State changed from " + this.state.toString() + " to " + state.toString()); "State changed from " + this.state.toString() + " to " + state.toString());
instance.pcs.firePropertyChange("state", this.state, state); instance.getPcs().firePropertyChange("state", this.state, state);
this.state = state; this.state = state;
} }

View file

@ -0,0 +1,35 @@
package fr.insa.clavardator.client.users;
import fr.insa.clavardator.lib.network.TcpConnection;
import fr.insa.clavardator.lib.util.ErrorCallback;
import fr.insa.clavardator.lib.util.SimpleCallback;
import org.jetbrains.annotations.Nullable;
import java.io.Serializable;
public class DirectPeerConnection extends PeerConnection {
private final TcpConnection connection;
public DirectPeerConnection(TcpConnection connection) {
this.connection = connection;
}
@Override
protected void send(Serializable message, @Nullable SimpleCallback callback, @Nullable ErrorCallback errorCallback) {
connection.send(message, callback, errorCallback);
}
@Override
protected void receive(TcpConnection.MessageReceivedCallback callback, ErrorCallback errorCallback) {
connection.receive(callback, errorCallback);
}
@Override
public void disconnect() {
if (connection != null && connection.isOpen()) {
connection.close();
}
}
}

View file

@ -0,0 +1,28 @@
package fr.insa.clavardator.client.users;
import fr.insa.clavardator.lib.network.TcpConnection;
import fr.insa.clavardator.lib.util.ErrorCallback;
import fr.insa.clavardator.lib.util.SimpleCallback;
import org.jetbrains.annotations.Nullable;
import java.io.Serializable;
public abstract class PeerConnection {
protected abstract void send(Serializable message, @Nullable SimpleCallback calback, @Nullable ErrorCallback errorCallback);
/**
* 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
*/
protected abstract void receive(TcpConnection.MessageReceivedCallback callback, ErrorCallback errorCallback);
/**
* Close the connection and set state to disconnected
*/
public abstract void disconnect();
}

View file

@ -0,0 +1,218 @@
package fr.insa.clavardator.client.users;
import fr.insa.clavardator.client.chat.ChatHistory;
import fr.insa.clavardator.client.db.DatabaseController;
import fr.insa.clavardator.client.server.proxy.Proxy;
import fr.insa.clavardator.lib.errors.UsernameTakenException;
import fr.insa.clavardator.lib.message.FileMessage;
import fr.insa.clavardator.lib.message.Message;
import fr.insa.clavardator.lib.network.TcpConnection;
import fr.insa.clavardator.lib.users.User;
import fr.insa.clavardator.lib.users.UserInformation;
import fr.insa.clavardator.lib.users.UserState;
import fr.insa.clavardator.lib.util.ErrorCallback;
import fr.insa.clavardator.lib.util.Log;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.EOFException;
import java.io.File;
import java.io.IOException;
import java.util.Date;
public class PeerUser extends User implements Comparable<PeerUser> {
private final transient ChatHistory history;
private UserState state = UserState.DISCONNECTED;
private PeerConnection connection;
public PeerUser(String id, String username) {
super(id, username);
history = new ChatHistory(this);
}
public PeerUser() {
super();
history = new ChatHistory(this);
}
private void init(String id, String username, ErrorCallback errorCallback) {
setId(id);
setUsername(username);
setState(UserState.CONNECTED);
connection.receive(msg -> onMessageReceived(msg, errorCallback), e -> {
disconnect();
if (!(e instanceof EOFException)) {
Log.e(this.getClass().getSimpleName(), "Error receiving message from " + getId(), e);
errorCallback.onError(e);
}
});
}
public void init(TcpConnection tcpConnection, String id, String username, ErrorCallback errorCallback) {
connection = new DirectPeerConnection(tcpConnection);
init(id, username, errorCallback);
}
public void init(Proxy proxy, String id, String username, ErrorCallback errorCallback) {
connection = new ProxyPeerConnection(proxy, getId());
init(id, username, 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 " + getUsername() + " / " + getId() + ": " + msg);
final Message message = new Message(CurrentUser.getInstance(), this, new Date(), msg);
connection.send(message, () -> addMessageToHistory(message, errorCallback), errorCallback);
} else {
Log.e(this.getClass().getSimpleName(), "Could not send message: connection is not initialized");
}
}
/**
* Sends a message containing a file to this user
*
* @param msg The text message to send
* @param errorCallback Callback on error
*/
public void sendFileMessage(String msg, File file, @Nullable ErrorCallback errorCallback) {
if (connection != null) {
Log.v(this.getClass().getSimpleName(),
"Sending file message to " + this.getUsername() + " / " + this.getId() + ": " + msg);
try {
final FileMessage message = new FileMessage(CurrentUser.getInstance(), this, new Date(), msg, file.getPath());
message.storeFile();
connection.send(message, () -> addMessageToHistory(message, errorCallback), errorCallback);
} catch (IOException e) {
Log.e(this.getClass().getSimpleName(), "Could not send message: error while opening file", e);
if (errorCallback != null) {
errorCallback.onError(e);
}
}
} else {
Log.e(this.getClass().getSimpleName(), "Could not send message: connection is not initialized");
}
}
/**
* 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");
}
}
protected void sendUsernameTaken() {
if (connection != null) {
Log.v(this.getClass().getSimpleName(), "Received username request using current username");
UsernameTakenException exception = new UsernameTakenException("Username taken",
CurrentUser.getInstance().getId(), getId());
connection.send(exception, null, null);
} else {
Log.e(this.getClass().getSimpleName(), "Could not send UsernameTaken: connection is not initialized");
}
}
protected void addMessageToHistory(Message message, @Nullable ErrorCallback errorCallback) {
history.addMessage(message, errorCallback);
}
protected void onMessageReceived(Object msg, ErrorCallback errorCallback) throws IOException {
Log.v(this.getClass().getSimpleName(), "Received message from " + getId());
if (msg instanceof UserInformation) {
assert ((UserInformation) msg).id.equals(getId());
final String receivedUsername = ((UserInformation) msg).getUsername();
Log.v(this.getClass().getSimpleName(), "Message username: " + receivedUsername);
if (CurrentUser.getInstance().getUsername().equals(receivedUsername)) {
sendUsernameTaken();
} else {
setUsername(receivedUsername);
}
} else if (msg instanceof Message) {
assert !((Message) msg).getRecipient().id.equals(getId());
Log.v(this.getClass().getSimpleName(), "Message text: " + ((Message) msg).getText());
if (msg instanceof FileMessage) {
((FileMessage) msg).storeFile();
}
history.addMessage((Message) msg, errorCallback);
} else if (msg instanceof UsernameTakenException) {
CurrentUser.getInstance().setState(CurrentUser.State.INVALID);
errorCallback.onError(new Exception("Received username already taken message"));
}
}
public void disconnect() {
if (connection != null) {
Log.v(this.getClass().getSimpleName(), "Disconnecting from user: " + getId());
connection.disconnect();
connection = null;
}
setState(UserState.DISCONNECTED);
}
/**
* Gets the value of history
*
* @return the value of history
*/
public ChatHistory getHistory() {
return history;
}
/**
* Sets this user state
*
* @param state The new state
*/
protected void setState(UserState state) {
getPcs().firePropertyChange("state", this.state, state);
this.state = state;
}
@Override
protected void setUsername(String newUsername) {
super.setUsername(newUsername);
final DatabaseController db = new DatabaseController();
db.updateUsername(new UserInformation(this),
null,
e -> Log.e(getClass().getSimpleName(), "Unable to update the username", e));
}
/**
* Check if this user is active.
*
* @return True id active, false otherwise
*/
public boolean isActive() {
return state == UserState.CONNECTED;
}
@Override
public int compareTo(@NotNull PeerUser peerUser) {
if (peerUser.isActive() && !this.isActive()) {
return 1;
} else if (!peerUser.isActive() && this.isActive()) {
return -1;
}
return getUsername().compareTo(peerUser.getUsername());
}
}

View file

@ -0,0 +1,35 @@
package fr.insa.clavardator.client.users;
import fr.insa.clavardator.client.server.proxy.Proxy;
import fr.insa.clavardator.lib.network.TcpConnection;
import fr.insa.clavardator.lib.util.ErrorCallback;
import fr.insa.clavardator.lib.util.SimpleCallback;
import org.jetbrains.annotations.Nullable;
import java.io.Serializable;
public class ProxyPeerConnection extends PeerConnection {
private final Proxy proxy;
private final String userId;
public ProxyPeerConnection(Proxy proxy, String userId) {
this.proxy = proxy;
this.userId = userId;
}
@Override
protected void send(Serializable message, @Nullable SimpleCallback callback, @Nullable ErrorCallback errorCallback) {
proxy.send(message, callback, errorCallback);
}
@Override
protected void receive(TcpConnection.MessageReceivedCallback callback, ErrorCallback errorCallback) {
proxy.receive(userId, callback, errorCallback);
}
@Override
public void disconnect() {
proxy.disconnectUser(userId);
}
}

View file

@ -1,12 +1,15 @@
package fr.insa.clavardator.users; package fr.insa.clavardator.client.users;
import fr.insa.clavardator.db.DatabaseController; import fr.insa.clavardator.client.db.DatabaseController;
import fr.insa.clavardator.network.NetDiscoverer; import fr.insa.clavardator.client.network.NetDiscoverer;
import fr.insa.clavardator.network.PeerHandshake; import fr.insa.clavardator.client.network.PeerHandshake;
import fr.insa.clavardator.network.TcpConnection; import fr.insa.clavardator.client.server.proxy.Proxy;
import fr.insa.clavardator.network.TcpListener; import fr.insa.clavardator.lib.network.TcpListener;
import fr.insa.clavardator.util.ErrorCallback; import fr.insa.clavardator.lib.users.User;
import fr.insa.clavardator.util.Log; import fr.insa.clavardator.lib.users.UserInformation;
import fr.insa.clavardator.lib.users.UserState;
import fr.insa.clavardator.lib.util.ErrorCallback;
import fr.insa.clavardator.lib.util.Log;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.collections.FXCollections; import javafx.collections.FXCollections;
import javafx.collections.ObservableList; import javafx.collections.ObservableList;
@ -35,32 +38,53 @@ public class UserList {
* *
* @param errorCallback The function to call on error * @param errorCallback The function to call on error
*/ */
public void discoverActiveUsers(ErrorCallback errorCallback) { public void discoverActiveUsers(int port, ErrorCallback errorCallback) {
netDiscoverer.discoverActiveUsers("CLAVARDATOR_BROADCAST", (ipAddr, data) -> { netDiscoverer.discoverActiveUsers("CLAVARDATOR_BROADCAST", (ipAddr, data) -> {
Log.v(this.getClass().getSimpleName(), "Discovered new user: " + data); Log.v(this.getClass().getSimpleName(), "Discovered new user: " + data);
new PeerHandshake().createConnection( new PeerHandshake().createConnection(
ipAddr, ipAddr,
port,
this::onUserConnectionSuccess, this::onUserConnectionSuccess,
errorCallback); errorCallback);
}, errorCallback); }, errorCallback);
} }
public void onReceivePresenceNotification(ArrayList<UserInformation> newPresenceUsers, TcpConnection proxyConnection) { public void onReceivePresenceNotification(ArrayList<UserInformation> newPresenceUsers, Proxy proxy) {
newPresenceUsers.forEach((userInfo -> { newPresenceUsers.forEach((userInfo -> {
final PeerUser savedUser = userHashmap.get(userInfo.id); final PeerUser savedUser = userHashmap.get(userInfo.id);
if (savedUser != null) { if (savedUser != null) {
if (savedUser.isActive()) { if (savedUser.isActive()) {
Log.v(getClass().getSimpleName(), "Received user from presence server already known and connected"); if (userInfo.getState() == UserState.DISCONNECTED) {
Log.v(getClass().getSimpleName(), "Received disconnected user from presence server was already known and connected, disconnecting...");
savedUser.disconnect();
} else {
Log.v(getClass().getSimpleName(), "Received user from presence server was already known and connected, updating info...");
savedUser.setState(userInfo.getState());
savedUser.setUsername(userInfo.getUsername());
}
} else { } else {
Log.v(getClass().getSimpleName(), "Received user from presence server already known and connected"); if (userInfo.getState() == UserState.CONNECTED) {
savedUser.init(proxyConnection, userInfo.id, userInfo.getUsername(), null); Log.v(getClass().getSimpleName(), "Received user from presence server was already known but not connected, connecting...");
savedUser.init(proxy, userInfo.id, userInfo.getUsername(),
e -> Log.e(getClass().getSimpleName(), "Error with user " + userInfo.getUsername(), e));
} else {
Log.v(getClass().getSimpleName(), "Received disconnected user from presence server was already known and disconnected.");
}
} }
} else { } else {
Log.v(getClass().getSimpleName(), "Received new user from presence server"); if (userInfo.getState() == UserState.CONNECTED) {
final PeerUser user = new PeerUser(); Log.v(getClass().getSimpleName(), "Received new connected user from presence server");
user.init(proxyConnection, userInfo.id, userInfo.getUsername(), null); final PeerUser user = new PeerUser();
userHashmap.put(user.id, user); user.init(proxy, userInfo.id, userInfo.getUsername(),
Platform.runLater(() -> userObservableList.add(user)); e -> Log.e(getClass().getSimpleName(), "Error with user " + userInfo.getUsername(), e));
userHashmap.put(user.getId(), user);
Platform.runLater(() -> userObservableList.add(user));
} else {
Log.v(getClass().getSimpleName(), "Received new disconnected user from presence server");
final PeerUser user = new PeerUser(userInfo.id, userInfo.getUsername());
userHashmap.put(user.getId(), user);
Platform.runLater(() -> userObservableList.add(user));
}
} }
})); }));
} }
@ -78,7 +102,7 @@ public class UserList {
public void retrievedPreviousUsers(UserListLoadedCallback onFinish, ErrorCallback errorCallback) { public void retrievedPreviousUsers(UserListLoadedCallback onFinish, ErrorCallback errorCallback) {
db.getAllUsers(users -> { db.getAllUsers(users -> {
users.forEach(user -> createNewInactiveUser(user.id, user.getUsername())); users.forEach(user -> createNewInactiveUser(user.getId(), user.getUsername()));
onFinish.onLoaded(); onFinish.onLoaded();
}, errorCallback); }, errorCallback);
} }
@ -88,8 +112,9 @@ public class UserList {
* *
* @param errorCallback Callback on error * @param errorCallback Callback on error
*/ */
public void startUserListening(ErrorCallback errorCallback) { public void startUserListening(int port, ErrorCallback errorCallback) {
tcpListener.acceptConnection( tcpListener.acceptConnection(
port,
(clientSocket) -> { (clientSocket) -> {
Log.v(this.getClass().getSimpleName(), Log.v(this.getClass().getSimpleName(),
"new connection from user at address: " + clientSocket.getInetAddress().toString()); "new connection from user at address: " + clientSocket.getInetAddress().toString());
@ -106,11 +131,14 @@ public class UserList {
final UserInformation userInfo = handshake.getUserInformation(); final UserInformation userInfo = handshake.getUserInformation();
final PeerUser savedUser = userHashmap.get(userInfo.id); final PeerUser savedUser = userHashmap.get(userInfo.id);
if (savedUser != null) { if (savedUser != null) {
savedUser.init(handshake.getConnection(), userInfo.id, userInfo.getUsername(), null); savedUser.init(handshake.getConnection(), userInfo.id, userInfo.getUsername(),
e -> Log.e(getClass().getSimpleName(), "Error with user " + userInfo.getUsername(), e));
} else { } else {
final PeerUser user = new PeerUser(); final PeerUser user = new PeerUser();
user.init(handshake.getConnection(), userInfo.id, userInfo.getUsername(), null); user.init(handshake.getConnection(), userInfo.id, userInfo.getUsername(),
userHashmap.put(user.id, user); e -> Log.e(getClass().getSimpleName(), "Error with user " + userInfo.getUsername(), e));
userHashmap.put(user.getId(), user);
Platform.runLater(() -> userObservableList.add(user)); Platform.runLater(() -> userObservableList.add(user));
} }
} }

View file

@ -1,13 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!--suppress JavaFxUnresolvedFxIdReference -->
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.ListView?> <?import javafx.scene.control.ListView?>
<?import javafx.scene.layout.*?> <?import javafx.scene.layout.*?>
<!--suppress JavaFxUnresolvedFxIdReference --> <AnchorPane xmlns:fx="http://javafx.com/fxml/1"
<?import javafx.scene.control.Label?> xmlns="http://javafx.com/javafx/11.0.1" fx:controller="fr.insa.clavardator.client.ui.chat.ChatController"
<?import com.jfoenix.controls.JFXSpinner?>
<?import javafx.geometry.Insets?>
<AnchorPane xmlns="http://javafx.com/javafx/11.0.1"
xmlns:fx="http://javafx.com/fxml/1" fx:controller="fr.insa.clavardator.ui.chat.ChatController"
stylesheets="@../styles.css" styleClass="container"> stylesheets="@../styles.css" styleClass="container">
<StackPane AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" <StackPane AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0"
AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0"> AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">

View file

@ -5,9 +5,9 @@
<?import javafx.geometry.Insets?> <?import javafx.geometry.Insets?>
<?import javafx.scene.layout.HBox?> <?import javafx.scene.layout.HBox?>
<?import org.kordamp.ikonli.javafx.FontIcon?> <?import org.kordamp.ikonli.javafx.FontIcon?>
<HBox xmlns="http://javafx.com/javafx" <HBox xmlns:fx="http://javafx.com/fxml"
xmlns:fx="http://javafx.com/fxml" xmlns="http://javafx.com/javafx"
fx:controller="fr.insa.clavardator.ui.chat.ChatFooterController" fx:controller="fr.insa.clavardator.client.ui.chat.ChatFooterController"
stylesheets="@../styles.css" styleClass="container" alignment="CENTER" spacing="10.0" fx:id="container"> stylesheets="@../styles.css" styleClass="container" alignment="CENTER" spacing="10.0" fx:id="container">
<padding> <padding>
<Insets bottom="20.0" left="20.0" right="20.0" top="20.0"/> <Insets bottom="20.0" left="20.0" right="20.0" top="20.0"/>

View file

@ -6,7 +6,7 @@
<?import javafx.scene.layout.VBox?> <?import javafx.scene.layout.VBox?>
<VBox xmlns:fx="http://javafx.com/fxml" <VBox xmlns:fx="http://javafx.com/fxml"
xmlns="http://javafx.com/javafx" xmlns="http://javafx.com/javafx"
fx:controller="fr.insa.clavardator.ui.chat.ChatHeaderController" fx:controller="fr.insa.clavardator.client.ui.chat.ChatHeaderController"
stylesheets="@../styles.css" styleClass="container"> stylesheets="@../styles.css" styleClass="container">
<HBox alignment="CENTER_LEFT" spacing="5" prefHeight="64"> <HBox alignment="CENTER_LEFT" spacing="5" prefHeight="64">
<padding> <padding>

View file

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import com.jfoenix.controls.JFXButton?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.VBox?>
<?import org.kordamp.ikonli.javafx.FontIcon?>
<VBox xmlns:fx="http://javafx.com/fxml/1"
xmlns="http://javafx.com/javafx/11.0.1"
fx:controller="fr.insa.clavardator.client.ui.chat.MessageListItemController"
stylesheets="@../styles.css"
styleClass="inner"
fx:id="container"
spacing="10">
<padding>
<Insets left="50" right="50" top="10" bottom="10"/>
</padding>
<JFXButton fx:id="button"
mnemonicParsing="false"
text="Message"
textAlignment="CENTER">
<graphic>
<FontIcon iconLiteral="fas-paperclip" iconSize="24" fx:id="attachmentIcon"/>
</graphic>
</JFXButton>
<Label fx:id="timestamp"
text="Timestamp"
styleClass="timestamp"/>
</VBox>

View file

@ -2,14 +2,13 @@
<?import com.jfoenix.controls.JFXButton?> <?import com.jfoenix.controls.JFXButton?>
<?import com.jfoenix.controls.JFXDialog?> <?import com.jfoenix.controls.JFXDialog?>
<?import com.jfoenix.controls.JFXTextField?>
<?import javafx.geometry.Insets?> <?import javafx.geometry.Insets?>
<?import javafx.scene.control.Label?> <?import javafx.scene.control.Label?>
<?import javafx.scene.layout.*?> <?import javafx.scene.layout.*?>
<?import org.kordamp.ikonli.javafx.FontIcon?> <?import org.kordamp.ikonli.javafx.FontIcon?>
<JFXDialog xmlns:fx="http://javafx.com/fxml" <JFXDialog xmlns:fx="http://javafx.com/fxml"
xmlns="http://javafx.com/javafx" xmlns="http://javafx.com/javafx"
fx:controller="fr.insa.clavardator.ui.dialogs.AboutDialogController" fx:controller="fr.insa.clavardator.client.ui.dialogs.AboutDialogController"
stylesheets="@../styles.css" fx:id="dialog"> stylesheets="@../styles.css" fx:id="dialog">
<AnchorPane <AnchorPane
prefWidth="600" prefWidth="600"
@ -22,7 +21,7 @@
<padding> <padding>
<Insets bottom="10"/> <Insets bottom="10"/>
</padding> </padding>
<Label styleClass="dialog-title" >À Propos</Label> <Label styleClass="dialog-title">À Propos</Label>
<VBox VBox.vgrow="ALWAYS" alignment="CENTER" spacing="30"> <VBox VBox.vgrow="ALWAYS" alignment="CENTER" spacing="30">
<Label VBox.vgrow="ALWAYS">Application réalisée par Yohan SIMARD et Arnaud VERGNET</Label> <Label VBox.vgrow="ALWAYS">Application réalisée par Yohan SIMARD et Arnaud VERGNET</Label>
<Label VBox.vgrow="ALWAYS">INSA Toulouse 2020-2021</Label> <Label VBox.vgrow="ALWAYS">INSA Toulouse 2020-2021</Label>

View file

@ -9,7 +9,7 @@
<?import org.kordamp.ikonli.javafx.FontIcon?> <?import org.kordamp.ikonli.javafx.FontIcon?>
<JFXDialog xmlns:fx="http://javafx.com/fxml" <JFXDialog xmlns:fx="http://javafx.com/fxml"
xmlns="http://javafx.com/javafx" xmlns="http://javafx.com/javafx"
fx:controller="fr.insa.clavardator.ui.dialogs.EditUsernameDialogController" fx:controller="fr.insa.clavardator.client.ui.dialogs.EditUsernameDialogController"
stylesheets="@../styles.css" fx:id="dialog"> stylesheets="@../styles.css" fx:id="dialog">
<AnchorPane <AnchorPane
prefWidth="600" prefWidth="600"

View file

@ -1,13 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.layout.HBox?>
<?import org.kordamp.ikonli.javafx.FontIcon?>
<?import javafx.geometry.Insets?> <?import javafx.geometry.Insets?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.*?>
<?import org.kordamp.ikonli.javafx.FontIcon?>
<AnchorPane xmlns:fx="http://javafx.com/fxml" <AnchorPane xmlns:fx="http://javafx.com/fxml"
xmlns="http://javafx.com/javafx" xmlns="http://javafx.com/javafx"
fx:controller="fr.insa.clavardator.ui.dialogs.SnackbarController" fx:controller="fr.insa.clavardator.client.ui.dialogs.SnackbarController"
stylesheets="@../styles.css"> stylesheets="@../styles.css">
<HBox fx:id="container" alignment="CENTER_LEFT" prefHeight="50.0" prefWidth="400" AnchorPane.rightAnchor="0" <HBox fx:id="container" alignment="CENTER_LEFT" prefHeight="50.0" prefWidth="400" AnchorPane.rightAnchor="0"
AnchorPane.leftAnchor="0" AnchorPane.topAnchor="0" AnchorPane.bottomAnchor="100" AnchorPane.leftAnchor="0" AnchorPane.topAnchor="0" AnchorPane.bottomAnchor="100"

View file

@ -1,12 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<?import com.jfoenix.controls.JFXButton?>
<?import javafx.scene.control.Label?> <?import javafx.scene.control.Label?>
<?import javafx.scene.layout.VBox?> <?import javafx.scene.layout.VBox?>
<?import com.jfoenix.controls.JFXButton?>
<?import org.kordamp.ikonli.javafx.FontIcon?> <?import org.kordamp.ikonli.javafx.FontIcon?>
<VBox xmlns:fx="http://javafx.com/fxml" <VBox xmlns:fx="http://javafx.com/fxml"
xmlns="http://javafx.com/javafx" xmlns="http://javafx.com/javafx"
fx:controller="fr.insa.clavardator.ui.ErrorScreenController" fx:controller="fr.insa.clavardator.client.ui.ErrorScreenController"
stylesheets="@styles.css" stylesheets="@styles.css"
styleClass="container" styleClass="container"
alignment="CENTER" alignment="CENTER"
@ -14,9 +14,9 @@
fx:id="container"> fx:id="container">
<Label>Une erreur est survenue et Clavardator ne peut pas démarrer</Label> <Label>Une erreur est survenue et Clavardator ne peut pas démarrer</Label>
<JFXButton styleClass="background-danger" onAction="#onPress"> <JFXButton styleClass="background-danger" onAction="#onPress">
<graphic> <graphic>
<FontIcon iconLiteral="fas-times"/> <FontIcon iconLiteral="fas-times"/>
</graphic> </graphic>
Quitter Quitter
</JFXButton> </JFXButton>
</VBox> </VBox>

View file

@ -6,7 +6,7 @@
<?import javafx.scene.layout.VBox?> <?import javafx.scene.layout.VBox?>
<StackPane xmlns:fx="http://javafx.com/fxml" <StackPane xmlns:fx="http://javafx.com/fxml"
xmlns="http://javafx.com/javafx" xmlns="http://javafx.com/javafx"
fx:controller="fr.insa.clavardator.ui.LoadingScreenController" fx:controller="fr.insa.clavardator.client.ui.LoadingScreenController"
stylesheets="@styles.css" stylesheets="@styles.css"
styleClass="container" styleClass="container"
fx:id="container"> fx:id="container">

View file

@ -1,11 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!--suppress JavaFxUnresolvedFxIdReference --> <!--suppress JavaFxUnresolvedFxIdReference -->
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.*?> <?import javafx.scene.layout.*?>
<StackPane xmlns:fx="http://javafx.com/fxml/1" <StackPane xmlns:fx="http://javafx.com/fxml/1"
xmlns="http://javafx.com/javafx/11.0.1" xmlns="http://javafx.com/javafx/11.0.1"
fx:controller="fr.insa.clavardator.ui.MainController" fx:controller="fr.insa.clavardator.client.ui.MainController"
stylesheets="@styles.css" stylesheets="@styles.css"
styleClass="container" styleClass="container"
fx:id="root"> fx:id="root">

View file

@ -1,4 +1,3 @@
/******************************************************** /********************************************************
THEME THEME
*********************************************************/ *********************************************************/
@ -17,7 +16,6 @@
} }
/******************************************************** /********************************************************
STYLE STYLE
*********************************************************/ *********************************************************/

View file

@ -3,12 +3,14 @@
<?import com.jfoenix.controls.JFXButton?> <?import com.jfoenix.controls.JFXButton?>
<?import javafx.geometry.Insets?> <?import javafx.geometry.Insets?>
<?import javafx.scene.control.Label?> <?import javafx.scene.control.Label?>
<?import javafx.scene.layout.*?> <?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.Pane?>
<?import javafx.scene.layout.VBox?>
<?import org.kordamp.ikonli.javafx.FontIcon?> <?import org.kordamp.ikonli.javafx.FontIcon?>
<VBox xmlns="http://javafx.com/javafx" <VBox xmlns:fx="http://javafx.com/fxml"
xmlns:fx="http://javafx.com/fxml" xmlns="http://javafx.com/javafx"
fx:controller="fr.insa.clavardator.ui.ToolbarController" fx:controller="fr.insa.clavardator.client.ui.ToolbarController"
stylesheets="@styles.css" styleClass="container"> stylesheets="@styles.css" styleClass="container">
<HBox alignment="CENTER_LEFT" prefHeight="64.0" spacing="10"> <HBox alignment="CENTER_LEFT" prefHeight="64.0" spacing="10">
<padding> <padding>
<Insets left="20" right="20"/> <Insets left="20" right="20"/>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.shape.Circle?>
<HBox xmlns:fx="http://javafx.com/fxml"
xmlns="http://javafx.com/javafx"
fx:controller="fr.insa.clavardator.client.ui.users.UserActiveIndicatorController"
stylesheets="@../styles.css" alignment="CENTER">
<Circle fx:id="circle" radius="5.0" styleClass="active-user-dot"/>
</HBox>

View file

@ -4,13 +4,14 @@
<?import javafx.scene.control.ListView?> <?import javafx.scene.control.ListView?>
<?import javafx.scene.layout.*?> <?import javafx.scene.layout.*?>
<?import org.kordamp.ikonli.javafx.FontIcon?> <?import org.kordamp.ikonli.javafx.FontIcon?>
<AnchorPane xmlns="http://javafx.com/javafx/11.0.1" <AnchorPane xmlns:fx="http://javafx.com/fxml/1"
xmlns:fx="http://javafx.com/fxml/1" fx:controller="fr.insa.clavardator.ui.users.UserListController" xmlns="http://javafx.com/javafx/11.0.1" fx:controller="fr.insa.clavardator.client.ui.users.UserListController"
stylesheets="@../styles.css" styleClass="container"> stylesheets="@../styles.css" styleClass="container">
<VBox AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" <VBox AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0"
AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0"> AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
<HBox alignment="CENTER" prefHeight="64.0" spacing="10.0"> <HBox alignment="CENTER" prefHeight="64.0" spacing="10.0">
<JFXButton mnemonicParsing="false" text="Utilisateurs" onMouseClicked="#onRefreshUserListPress" fx:id="refreshButton"> <JFXButton mnemonicParsing="false" text="Utilisateurs" onMouseClicked="#onRefreshUserListPress"
fx:id="refreshButton">
<graphic> <graphic>
<FontIcon iconLiteral="fas-sync-alt" iconSize="24"/> <FontIcon iconLiteral="fas-sync-alt" iconSize="24"/>
</graphic> </graphic>

View file

@ -1,11 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!--suppress JavaFxUnresolvedFxIdReference -->
<?import com.jfoenix.controls.JFXButton?> <?import com.jfoenix.controls.JFXButton?>
<?import javafx.scene.layout.*?> <?import javafx.scene.layout.*?>
<!--suppress JavaFxUnresolvedFxIdReference --> <AnchorPane xmlns:fx="http://javafx.com/fxml/1" prefHeight="40.0" xmlns="http://javafx.com/javafx/11.0.1"
<AnchorPane prefHeight="40.0" xmlns="http://javafx.com/javafx/11.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="fr.insa.clavardator.client.ui.users.UserListItemController" stylesheets="@../styles.css"
fx:controller="fr.insa.clavardator.ui.users.UserListItemController" stylesheets="@../styles.css"
styleClass="inner"> styleClass="inner">
<!--suppress JavaFxRedundantPropertyValue -->
<JFXButton fx:id="button" alignment="CENTER_LEFT" AnchorPane.bottomAnchor="0" AnchorPane.leftAnchor="0" <JFXButton fx:id="button" alignment="CENTER_LEFT" AnchorPane.bottomAnchor="0" AnchorPane.leftAnchor="0"
AnchorPane.rightAnchor="0" AnchorPane.topAnchor="0" onAction="#onPress"> AnchorPane.rightAnchor="0" AnchorPane.topAnchor="0" onAction="#onPress">
<graphic> <graphic>

View file

@ -1,8 +1,8 @@
package fr.insa.clavardator; package fr.insa.clavardator;
import fr.insa.clavardator.chat.Message; import fr.insa.clavardator.client.db.DatabaseController;
import fr.insa.clavardator.db.DatabaseController; import fr.insa.clavardator.lib.message.Message;
import fr.insa.clavardator.users.UserInformation; import fr.insa.clavardator.lib.users.UserInformation;
import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@ -30,9 +30,9 @@ public class DatabaseTest {
db.getChatHistory(new UserInformation("1", "Yohan"), db.getChatHistory(new UserInformation("1", "Yohan"),
new UserInformation("0", "Arnaud"), new Date(0), new Date(), history -> { new UserInformation("0", "Arnaud"), new Date(0), new Date(), history -> {
assertEquals(history.size(), 0); assertEquals(history.size(), 0);
latch2.countDown(); latch2.countDown();
}, Assertions::fail); }, Assertions::fail);
latch2.await(); latch2.await();
CountDownLatch latch8 = new CountDownLatch(1); CountDownLatch latch8 = new CountDownLatch(1);
@ -67,8 +67,8 @@ public class DatabaseTest {
db.getChatHistory(new UserInformation("1", "Yohan"), db.getChatHistory(new UserInformation("1", "Yohan"),
new UserInformation("2", "Arnaud"), new Date(0), new Date(), history -> { new UserInformation("2", "Arnaud"), new Date(0), new Date(), history -> {
assertEquals(5, history.size()); assertEquals(5, history.size());
assertEquals("Coucou Arnaud !", history.get(0).getText()); assertEquals("Coucou Arnaud !", history.get(0).getText());
assertEquals("1", history.get(0).getSender().id); assertEquals("1", history.get(0).getSender().id);
assertEquals("2", history.get(0).getRecipient().id); assertEquals("2", history.get(0).getRecipient().id);
assertEquals("Yohan", history.get(0).getSender().getUsername()); assertEquals("Yohan", history.get(0).getSender().getUsername());

14
lib/build.gradle Normal file
View file

@ -0,0 +1,14 @@
plugins {
id 'java'
}
group 'fr.insa.clavardator.lib'
version '0.0.1'
repositories {
mavenCentral()
}
dependencies {
implementation 'org.jetbrains:annotations:20.1.0'
}

View file

@ -0,0 +1,27 @@
package fr.insa.clavardator.lib.errors;
import fr.insa.clavardator.lib.message.RecipientInfo;
import fr.insa.clavardator.lib.message.SenderInfo;
import java.io.Serializable;
public class UsernameTakenException extends Exception implements Serializable, RecipientInfo, SenderInfo {
public final String recipient;
public final String sender;
public UsernameTakenException(String message, String sender, String recipient) {
super(message);
this.sender = sender;
this.recipient = recipient;
}
@Override
public String getRecipientId() {
return recipient;
}
@Override
public String getSenderId() {
return sender;
}
}

View file

@ -1,7 +1,7 @@
package fr.insa.clavardator.chat; package fr.insa.clavardator.lib.message;
import fr.insa.clavardator.users.User; import fr.insa.clavardator.lib.users.User;
import fr.insa.clavardator.users.UserInformation; import fr.insa.clavardator.lib.users.UserInformation;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
@ -14,8 +14,8 @@ public class FileMessage extends Message {
public static final String STORED_FILES_FOLDER = "clavardator_stored_files"; public static final String STORED_FILES_FOLDER = "clavardator_stored_files";
private final String fileName; private final String fileName;
private String path;
byte[] rawFile = null; byte[] rawFile = null;
private String path;
/** /**
* Constructs a FileMessage * Constructs a FileMessage
@ -32,13 +32,13 @@ public class FileMessage extends Message {
File file = new File(filePath); File file = new File(filePath);
if (!file.exists()) if (!file.exists())
throw new IOException("The file " + filePath + " does not exist"); throw new IOException("The file " + file.getAbsolutePath() + " does not exist");
if (!file.canRead()) if (!file.canRead())
throw new IOException("The file " + filePath + " is not readable"); throw new IOException("The file " + file.getAbsolutePath() + " is not readable");
if (!file.isFile()) if (!file.isFile())
throw new IOException("The path " + filePath + " does not lead to a file"); throw new IOException("The path " + file.getAbsolutePath() + " does not lead to a file");
if (file.length() > MAX_FILE_SIZE) if (file.length() > MAX_FILE_SIZE)
throw new IOException("The file " + filePath + " is too large"); throw new IOException("The file " + file.getAbsolutePath() + " is too large");
fileName = file.getName(); fileName = file.getName();
path = filePath; path = filePath;
@ -63,12 +63,20 @@ public class FileMessage extends Message {
* @return the path to the file * @return the path to the file
*/ */
public String storeFile() throws IOException { public String storeFile() throws IOException {
path = STORED_FILES_FOLDER + File.separatorChar + fileName;
// Create directory // Create clavardator directory
File dir = new File(STORED_FILES_FOLDER); File dir = new File(STORED_FILES_FOLDER);
dir.mkdirs(); dir.mkdirs();
// Copy the source file to rawFile
if (rawFile == null) {
FileInputStream istream = new FileInputStream(path);
rawFile = istream.readAllBytes();
istream.close();
}
path = STORED_FILES_FOLDER + File.separatorChar + fileName;
// Create new file // Create new file
int extensionBeginning = fileName.lastIndexOf('.'); int extensionBeginning = fileName.lastIndexOf('.');
String name = fileName; String name = fileName;
@ -84,12 +92,7 @@ public class FileMessage extends Message {
file = new File(path); file = new File(path);
} }
// write to the file // Write the file in clavardator dir
if (rawFile == null) {
FileInputStream stream = new FileInputStream(fileName);
rawFile = stream.readAllBytes();
}
FileOutputStream ostream = new FileOutputStream(file); FileOutputStream ostream = new FileOutputStream(file);
ostream.write(rawFile); ostream.write(rawFile);
ostream.close(); ostream.close();

View file

@ -1,6 +1,6 @@
package fr.insa.clavardator.chat; package fr.insa.clavardator.lib.message;
import fr.insa.clavardator.users.UserInformation; import fr.insa.clavardator.lib.users.UserInformation;
import java.io.IOException; import java.io.IOException;
import java.util.Date; import java.util.Date;

View file

@ -1,13 +1,12 @@
package fr.insa.clavardator.chat; package fr.insa.clavardator.lib.message;
import fr.insa.clavardator.users.CurrentUser; import fr.insa.clavardator.lib.users.User;
import fr.insa.clavardator.users.User; import fr.insa.clavardator.lib.users.UserInformation;
import fr.insa.clavardator.users.UserInformation;
import java.io.Serializable; import java.io.Serializable;
import java.util.Date; import java.util.Date;
public class Message implements Serializable { public class Message implements Serializable, SenderInfo, RecipientInfo {
private final String text; private final String text;
private final Date date; private final Date date;
private final UserInformation sender; private final UserInformation sender;
@ -18,7 +17,7 @@ public class Message implements Serializable {
} }
public Message(User sender, User recipient, Date date) { public Message(User sender, User recipient, Date date) {
this(sender, recipient, date,""); this(sender, recipient, date, "");
} }
public Message(UserInformation sender, UserInformation recipient, Date date, String text) { public Message(UserInformation sender, UserInformation recipient, Date date, String text) {
@ -47,14 +46,6 @@ public class Message implements Serializable {
return recipient; return recipient;
} }
public UserInformation getCorrespondent() {
if (CurrentUser.getInstance().getId() != null && CurrentUser.getInstance().getId().equals(sender.id)) {
return recipient;
} else {
return sender;
}
}
public Date getDate() { public Date getDate() {
return date; return date;
} }
@ -68,4 +59,14 @@ public class Message implements Serializable {
", recipient=" + recipient + ", recipient=" + recipient +
'}'; '}';
} }
@Override
public String getSenderId() {
return sender.id;
}
@Override
public String getRecipientId() {
return recipient.id;
}
} }

View file

@ -0,0 +1,5 @@
package fr.insa.clavardator.lib.message;
public interface RecipientInfo {
String getRecipientId();
}

View file

@ -0,0 +1,5 @@
package fr.insa.clavardator.lib.message;
public interface SenderInfo {
String getSenderId();
}

View file

@ -1,6 +1,7 @@
package fr.insa.clavardator.network; package fr.insa.clavardator.lib.network;
import fr.insa.clavardator.util.ErrorCallback; import fr.insa.clavardator.lib.util.ErrorCallback;
import fr.insa.clavardator.lib.util.SimpleCallback;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
@ -12,24 +13,13 @@ import java.net.InetAddress;
import java.net.Socket; import java.net.Socket;
public class TcpConnection { public class TcpConnection {
public static final int TCP_PORT = 31598;
private final int port;
private final Object outputStreamGuard = new Object();
private Socket socket; private Socket socket;
private ObjectOutputStream outputStream; private ObjectOutputStream outputStream;
private ObjectInputStream inputStream; private ObjectInputStream inputStream;
private boolean shouldStop = false; private boolean shouldStop = false;
private final int port;
/**
* Creates a new connection, and connects to the peer
*
* @param ipAddr The IP address of the peer
* @param callback The function to call when connected
* @param errorCallback The function to call on error
*/
public TcpConnection(InetAddress ipAddr, SocketConnectedCallback callback, ErrorCallback errorCallback) {
this(ipAddr, TCP_PORT, callback, errorCallback);
}
/** /**
* Creates a new connection, and connects to the peer * Creates a new connection, and connects to the peer
@ -61,7 +51,7 @@ public class TcpConnection {
* @param callback The function to call on success * @param callback The function to call on success
* @param errorCallback The function to call on error * @param errorCallback The function to call on error
*/ */
public void send(Serializable message, @Nullable MessageSentCallback callback, @Nullable ErrorCallback errorCallback) { public void send(Serializable message, @Nullable SimpleCallback callback, @Nullable ErrorCallback errorCallback) {
Sender sender = new Sender(message, callback, errorCallback); Sender sender = new Sender(message, callback, errorCallback);
sender.start(); sender.start();
} }
@ -93,22 +83,35 @@ public class TcpConnection {
*/ */
public void close() { public void close() {
shouldStop = true; shouldStop = true;
if (socket != null) { try {
try { if (outputStream != null)
outputStream.close();
if (inputStream != null)
inputStream.close();
if (socket != null)
socket.close(); socket.close();
} catch (IOException ignored) { } catch (IOException ignored) {
}
} }
} }
/** /**
* Checks if the current connection is open * Checks if the current connection is open
* @return True if the socket is still open *
* @return True if the socket is still open
*/ */
public boolean isOpen() { public boolean isOpen() {
return socket != null && socket.isConnected() && !socket.isClosed(); return socket != null && socket.isConnected() && !socket.isClosed();
} }
public interface SocketConnectedCallback {
void onSocketConnected(TcpConnection connection);
}
public interface MessageReceivedCallback {
void onMessageReceived(Object msg) throws IOException;
}
private class Connector extends Thread { private class Connector extends Thread {
private final InetAddress ipAddr; private final InetAddress ipAddr;
private final SocketConnectedCallback callback; private final SocketConnectedCallback callback;
@ -135,34 +138,35 @@ public class TcpConnection {
} }
} }
private class Sender extends Thread { private class Sender extends Thread {
private final Serializable message; private final Serializable message;
private final MessageSentCallback callback; private final SimpleCallback callback;
private final ErrorCallback errorCallback; private final ErrorCallback errorCallback;
/** /**
* Constructs a thread that sends a message using the socket of the outer class * Constructs a thread that sends a message using the socket of the outer class
* *
* @param message The message to send * @param message The message to send
* @param callback The function to call on success * @param callback The function to call on success
* @param errorCallback The function to call on error * @param errorCallback The function to call on error
*/ */
public Sender(Serializable message, @Nullable MessageSentCallback callback, @Nullable ErrorCallback errorCallback) { public Sender(Serializable message, @Nullable SimpleCallback callback, @Nullable ErrorCallback errorCallback) {
this.message = message; this.message = message;
this.callback = callback; this.callback = callback;
this.errorCallback = errorCallback; this.errorCallback = errorCallback;
} }
@Override @Override
synchronized public void run() { public void run() {
try { try {
if (outputStream == null) { synchronized (outputStreamGuard) {
outputStream = new ObjectOutputStream(socket.getOutputStream()); if (outputStream == null) {
outputStream = new ObjectOutputStream(socket.getOutputStream());
}
outputStream.writeObject(message);
} }
outputStream.writeObject(message);
if (callback != null) if (callback != null)
callback.onMessageSent(); callback.call();
} catch (IOException e) { } catch (IOException e) {
if (errorCallback != null && !shouldStop) if (errorCallback != null && !shouldStop)
errorCallback.onError(e); errorCallback.onError(e);
@ -202,18 +206,4 @@ public class TcpConnection {
} }
} }
} }
public interface SocketConnectedCallback {
void onSocketConnected(TcpConnection connection);
}
public interface MessageReceivedCallback {
void onMessageReceived(Object msg) throws IOException;
}
public interface MessageSentCallback {
void onMessageSent();
}
} }

View file

@ -1,30 +1,22 @@
package fr.insa.clavardator.network; package fr.insa.clavardator.lib.network;
import fr.insa.clavardator.util.ErrorCallback; import fr.insa.clavardator.lib.util.ErrorCallback;
import java.io.IOException; import java.io.IOException;
import java.net.ServerSocket; import java.net.ServerSocket;
import java.net.Socket; import java.net.Socket;
import static fr.insa.clavardator.network.TcpConnection.TCP_PORT;
public class TcpListener { public class TcpListener {
Acceptor acceptor = null; Acceptor acceptor = null;
public TcpListener() { public TcpListener() {
} }
/** public void acceptConnection(int port, NewConnectionCallback callback, ErrorCallback errorCallback) {
* Start accepting incoming connections
*
* @param callback The function to call when a user connects
* @param errorCallback The function to call on error
*/
public void acceptConnection(NewConnectionCallback callback, ErrorCallback errorCallback) {
if (acceptor != null) { if (acceptor != null) {
acceptor.stopAccepting(); acceptor.stopAccepting();
} }
acceptor = new Acceptor(callback, errorCallback); acceptor = new Acceptor(port, callback, errorCallback);
acceptor.start(); acceptor.start();
} }
@ -38,13 +30,19 @@ public class TcpListener {
} }
public interface NewConnectionCallback {
void onNewConnection(Socket clientSocket);
}
private static class Acceptor extends Thread { private static class Acceptor extends Thread {
private boolean shouldStop = false;
private final NewConnectionCallback callback; private final NewConnectionCallback callback;
private final ErrorCallback errorCallback; private final ErrorCallback errorCallback;
private final int port;
private boolean shouldStop = false;
private ServerSocket server; private ServerSocket server;
public Acceptor(NewConnectionCallback callback, ErrorCallback errorCallback) { public Acceptor(int port, NewConnectionCallback callback, ErrorCallback errorCallback) {
this.port = port;
this.callback = callback; this.callback = callback;
this.errorCallback = errorCallback; this.errorCallback = errorCallback;
} }
@ -52,7 +50,7 @@ public class TcpListener {
@Override @Override
public void run() { public void run() {
try { try {
server = new ServerSocket(TCP_PORT); server = new ServerSocket(port);
while (!shouldStop) { while (!shouldStop) {
Socket clientSocket = server.accept(); Socket clientSocket = server.accept();
callback.onNewConnection(clientSocket); callback.onNewConnection(clientSocket);
@ -61,20 +59,15 @@ public class TcpListener {
if (!shouldStop) if (!shouldStop)
errorCallback.onError(e); errorCallback.onError(e);
} }
} }
public void stopAccepting() { public void stopAccepting() {
shouldStop = true; shouldStop = true;
// TODO: call interrupt() with ChannelServerSocket instead of closing the socket?
try { try {
server.close(); server.close();
} catch (IOException ignored) { } catch (IOException ignored) {
} }
} }
} }
public interface NewConnectionCallback {
void onNewConnection(Socket clientSocket);
}
} }

View file

@ -1,24 +1,14 @@
package fr.insa.clavardator.users; package fr.insa.clavardator.lib.users;
import fr.insa.clavardator.db.DatabaseController;
import fr.insa.clavardator.util.Log;
import java.beans.PropertyChangeListener; import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport; import java.beans.PropertyChangeSupport;
import java.io.Serializable; import java.io.Serializable;
public class User implements Serializable { public class User implements Serializable {
private String username;
protected String id;
// Make this class observable // Make this class observable
protected final transient PropertyChangeSupport pcs = new PropertyChangeSupport(this); private final transient PropertyChangeSupport pcs = new PropertyChangeSupport(this);
public void addObserver(PropertyChangeListener listener) { private String username;
pcs.addPropertyChangeListener(listener); private String id;
}
public void removeObserver(PropertyChangeListener listener) {
pcs.removePropertyChangeListener(listener);
}
public User() { public User() {
} }
@ -32,6 +22,17 @@ public class User implements Serializable {
this.username = username; this.username = username;
} }
public void addObserver(PropertyChangeListener listener) {
pcs.addPropertyChangeListener(listener);
}
public void removeObserver(PropertyChangeListener listener) {
pcs.removePropertyChangeListener(listener);
}
public PropertyChangeSupport getPcs() {
return pcs;
}
public String getId() { public String getId() {
return id; return id;

View file

@ -0,0 +1,56 @@
package fr.insa.clavardator.lib.users;
import fr.insa.clavardator.lib.message.SenderInfo;
import java.io.Serializable;
/**
* Class used to serialize useful user information
*/
public class UserInformation implements Serializable, SenderInfo {
public final String id;
private final String username;
private final UserState state;
public UserInformation(String id, String username) {
this.id = id;
this.username = username;
this.state = UserState.CONNECTED;
}
public UserInformation(String id, String username, UserState state) {
this.id = id;
this.username = username;
this.state = state;
}
public UserInformation(User user) {
this.id = user.getId();
this.username = user.getUsername();
this.state = UserState.CONNECTED;
}
public UserInformation(User user, UserState state) {
this.id = user.getId();
this.username = user.getUsername();
this.state = state;
}
public String getUsername() {
return username;
}
public UserState getState() {
return state;
}
@Override
public String toString() {
return "UserInfo " + id + '(' + username + ')';
}
@Override
public String getSenderId() {
return id;
}
}

View file

@ -0,0 +1,6 @@
package fr.insa.clavardator.lib.users;
public enum UserState {
CONNECTED,
DISCONNECTED,
}

View file

@ -1,4 +1,4 @@
package fr.insa.clavardator.util; package fr.insa.clavardator.lib.util;
public interface ErrorCallback { public interface ErrorCallback {
void onError(Exception e); void onError(Exception e);

View file

@ -1,4 +1,4 @@
package fr.insa.clavardator.util; package fr.insa.clavardator.lib.util;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
@ -16,13 +16,13 @@ public class Log {
public static int verboseLevel = 4; public static int verboseLevel = 4;
private static void print(String prefix, String message, String mode, int requiredLevel, @Nullable Exception e) { private static void print(String prefix, String message, String mode, int requiredLevel, @Nullable Exception e) {
if (verboseLevel >= requiredLevel) { if (verboseLevel >= requiredLevel) {
Date date = new Date(); Date date = new Date();
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss.SSS"); SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss.SSS");
System.out.println(sdf.format(date) + " | " + mode + " | " + prefix + ": " + message); System.out.println(sdf.format(date) + " | " + mode + " | " + prefix + ": " + message);
if (e != null) if (e != null)
e.printStackTrace(); e.printStackTrace();
} }
} }
public static void v(String prefix, String message) { public static void v(String prefix, String message) {

View file

@ -1,4 +1,4 @@
package fr.insa.clavardator.util; package fr.insa.clavardator.lib.util;
public interface ParametrizedCallback<T> { public interface ParametrizedCallback<T> {
void call(T param); void call(T param);

View file

@ -1,4 +1,4 @@
package fr.insa.clavardator.util; package fr.insa.clavardator.lib.util;
public interface SimpleCallback { public interface SimpleCallback {
void call(); void call();

View file

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 38 KiB

View file

Before

Width:  |  Height:  |  Size: 142 KiB

After

Width:  |  Height:  |  Size: 142 KiB

View file

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 37 KiB

View file

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View file

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View file

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

BIN
report/report.pdf Normal file

Binary file not shown.

26
server/build.gradle Normal file
View file

@ -0,0 +1,26 @@
plugins {
id 'application'
id 'com.github.johnrengelman.shadow' version '6.1.0'
}
group 'fr.insa.clavardator.server'
version '0.0.1'
repositories {
mavenCentral()
}
dependencies {
implementation project(':lib')
implementation 'org.jetbrains:annotations:20.1.0'
}
mainClassName = 'fr.insa.clavardator.server.Main'
shadowJar {
mergeServiceFiles()
}
run {
standardInput = System.in
}

View file

@ -0,0 +1,93 @@
package fr.insa.clavardator.server;
import fr.insa.clavardator.lib.util.Log;
import java.util.List;
import java.util.Scanner;
public class Main {
private static final int PROXY_PORT = 35750;
private static final int PRESENCE_PORT = 35650;
private static final List<String> HELP_ARGS = List.of("-h", "--help");
private static final List<String> VERSION_ARGS = List.of("-v", "--version");
private static final List<String> PROXY_ARGS = List.of("-x", "--proxy");
private static final List<String> PRESENCE_ARGS = List.of("-p", "--presence");
private static int proxyPort = PROXY_PORT;
private static int presencePort = PRESENCE_PORT;
private static void printHelp() {
System.out.println("\nClavardator INSA presence server v1.0.0\n");
System.out.println("usage: <command> [options]\n");
System.out.println("Options:");
System.out.println(" -h, --help Display help message.");
System.out.println(" -v, --version Display version information.");
System.out.println(" -X <port>, --proxy <port> Set the proxy port.");
System.out.println(" -p <port>, --presence <port> Set the presence server port.");
}
private static void printVersion() {
System.out.println("\nClavardator INSA presence server v1.0.0\n");
}
private static int getPort(String value, int defaultValue) {
int port;
try {
port = Integer.parseInt(value.trim());
if (port <= 1024 || port >= 64000) {
System.out.println("Port should be between 1024 and 64000. Falling back to default.");
port = defaultValue;
}
} catch (NumberFormatException e) {
System.out.println("Could not read port " + value + ". Falling back to default.");
port = defaultValue;
}
return port;
}
private static void readArgs(String[] args) {
if (args.length > 0) {
if (HELP_ARGS.contains(args[0])) {
printHelp();
System.exit(0);
}
if (VERSION_ARGS.contains(args[0])) {
printVersion();
System.exit(0);
}
for (int i = 0; i < args.length - 1; i++) {
if (PROXY_ARGS.contains(args[i])) {
proxyPort = getPort(args[i + 1], PROXY_PORT);
}
if (PRESENCE_ARGS.contains(args[i])) {
presencePort = getPort(args[i + 1], PRESENCE_PORT);
}
}
if (presencePort == proxyPort) {
System.out.println("Proxy and presence ports must be different");
System.exit(1);
}
}
}
public static void main(String[] args) {
readArgs(args);
System.out.println("\nType q then press enter to exit\n");
Log.v(Main.class.getSimpleName(), "Starting proxy on port " + proxyPort);
Proxy proxy = new Proxy(proxyPort);
Log.v(Main.class.getSimpleName(), "Starting presence server on port " + presencePort);
Presence presence = new Presence(presencePort);
String line;
Scanner scanner = new Scanner(System.in);
do {
line = scanner.nextLine();
} while (!line.equals("q"));
proxy.stop();
presence.stop();
}
}

View file

@ -0,0 +1,110 @@
package fr.insa.clavardator.server;
import fr.insa.clavardator.lib.network.TcpConnection;
import fr.insa.clavardator.lib.network.TcpListener;
import fr.insa.clavardator.lib.users.UserInformation;
import fr.insa.clavardator.lib.users.UserState;
import fr.insa.clavardator.lib.util.Log;
import java.io.EOFException;
import java.io.Serializable;
import java.net.Socket;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
public class Presence {
private final Map<String, TcpConnection> subscribers = new HashMap<>();
private final ArrayList<UserInformation> connectedUsers = new ArrayList<>();
private final TcpListener presenceListener;
public Presence(int port) {
presenceListener = new TcpListener();
presenceListener.acceptConnection(
port,
this::subscribe,
e -> Log.e(getClass().getSimpleName(), "Error while registering a user", e));
}
public void publish(UserInformation info) {
ArrayList<UserInformation> msg = new ArrayList<>(1);
msg.add(info);
for (Map.Entry<String, TcpConnection> entry : subscribers.entrySet()) {
// Do not send update to self
if (!entry.getKey().equals(info.id)) {
entry.getValue().send(msg, null,
e -> Log.e(getClass().getSimpleName(), "Error while publishing user information", e));
}
}
}
private void updateUser(UserInformation userInformation) {
connectedUsers.removeIf(ui -> userInformation.id.equals(ui.id));
connectedUsers.add(userInformation);
}
private void adduser(UserInformation userInformation, TcpConnection userConnection) {
subscribers.put(userInformation.id, userConnection);
connectedUsers.add(userInformation);
}
public void subscribe(Socket socket) {
TcpConnection userConnection = new TcpConnection(socket);
// Receive user information
userConnection.receiveOne(o -> {
if (o instanceof UserInformation) {
UserInformation userInformation = ((UserInformation) o);
Log.v(getClass().getSimpleName(), "Registering user " + userInformation.id +
" (" + userInformation.getUsername() + ")");
// publish new user info to all subscribers
publish(userInformation);
// Send the list of connected users to the new subscriber. We're cloning it because we're modifying it
// just after this call, while the sender thread might not have send it yet
userConnection.send((Serializable) connectedUsers.clone(), null,
e -> Log.e(getClass().getSimpleName(), "Error while receiving user information", e));
adduser(userInformation, userConnection);
userConnection.receive(msg -> {
if (msg instanceof UserInformation) {
UserInformation newUserInformation = ((UserInformation) msg);
Log.v(getClass().getSimpleName(), "Receiving user status update " + newUserInformation.id +
" (" + newUserInformation.getUsername() + ")");
updateUser(newUserInformation);
publish(newUserInformation);
}
},
e -> {
if (e instanceof EOFException) {
unsubscribe(userInformation);
}
});
} else {
Log.e(getClass().getSimpleName(), "Received unexpected message type: " + o.getClass().getSimpleName());
}
}, e -> {
if (!(e instanceof EOFException)) {
Log.e(getClass().getSimpleName(), "Error while receiving user information", e);
}
});
}
public void unsubscribe(UserInformation userInformation) {
Log.v(getClass().getSimpleName(), "Unsubscribing user " + userInformation.id + "(" + userInformation.getUsername() + ")");
subscribers.get(userInformation.id).close();
subscribers.remove(userInformation.id);
publish(new UserInformation(userInformation.id, userInformation.getUsername(), UserState.DISCONNECTED));
connectedUsers.removeIf(ui -> userInformation.id.equals(ui.id));
}
public void stop() {
presenceListener.stopAccepting();
for (TcpConnection connection : subscribers.values()) {
connection.close();
}
}
}

View file

@ -0,0 +1,78 @@
package fr.insa.clavardator.server;
import fr.insa.clavardator.lib.message.RecipientInfo;
import fr.insa.clavardator.lib.network.TcpConnection;
import fr.insa.clavardator.lib.network.TcpListener;
import fr.insa.clavardator.lib.users.UserInformation;
import fr.insa.clavardator.lib.util.Log;
import java.io.EOFException;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
public class Proxy {
private final HashMap<String, TcpConnection> users = new HashMap<>();
private final TcpListener proxyListener;
public Proxy(int port) {
proxyListener = new TcpListener();
proxyListener.acceptConnection(
port,
clientSocket -> {
Log.v(getClass().getSimpleName(), "Accepting a new user");
TcpConnection connection = new TcpConnection(clientSocket);
connection.receive(msg -> {
if (msg instanceof UserInformation) {
// Send UserInformation to all other connected users
Log.v(getClass().getSimpleName(), "Received UserInformation: " + msg);
users.put(((UserInformation) msg).id, connection);
for (String userId : users.keySet()) {
UserInformation userInfo = ((UserInformation) msg);
if (!userId.equals(userInfo.id)) {
transmitMessage((Serializable) msg, userId);
}
}
} else if (msg instanceof RecipientInfo) {
// Send other messages only to the recipient
transmitMessage((Serializable) msg, ((RecipientInfo) msg).getRecipientId());
} else {
Log.e(getClass().getSimpleName(), "Unexpected message received: " + msg.toString());
}
}, e -> {
if (!(e instanceof EOFException)) {
Log.e(getClass().getSimpleName(), "Error while receiving message to transmit", e);
}
for (Map.Entry<String, TcpConnection> user : users.entrySet()) {
if (user.getValue().equals(connection)) {
Log.v(getClass().getSimpleName(), "Disconnecting user " + user.getKey());
users.remove(user.getKey());
break;
}
}
});
},
e -> Log.e(getClass().getSimpleName(), "Error while accepting a user", e));
}
void transmitMessage(Serializable msg, String recipientId) {
TcpConnection user = users.get(recipientId);
if (user == null) {
Log.e(getClass().getSimpleName(), "Cannot find the recipient in the connected users");
} else {
Log.v(getClass().getSimpleName(), "Transmitting message: " + msg);
user.send(msg, null, e -> Log.e(getClass().getSimpleName(), "Error while sending message", e));
}
}
public void stop() {
proxyListener.stopAccepting();
for (TcpConnection connection : users.values()) {
connection.close();
}
}
}

View file

@ -1,2 +1,4 @@
rootProject.name = 'clavardator' rootProject.name = 'clavardator'
include 'client'
include 'lib'
include 'server'

View file

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

View file

@ -1,47 +0,0 @@
package fr.insa.clavardator.server;
import fr.insa.clavardator.network.TcpConnection;
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;
/**
* Interface exposing public methods necessary for any presence server.
*
* @implNote Implement this interface when creating your own presence server class,
* then update the {@link fr.insa.clavardator.server.PresenceFactory factory}
* to add your new implementation.
*/
public interface Presence {
/**
* Subscribes to this presence server notifications.
* A list of Ids representing the current active users is returned.
*
* @param callback Called when subscription completes
*/
void subscribe(ParametrizedCallback<ArrayList<UserInformation>> callback, @Nullable ErrorCallback errorCallback);
/**
* Stops subscription to the presence server by closing TCP connections.
* This will stop notifications.
*
* @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, @Nullable ErrorCallback errorCallback);
/**
* Gets a connection to the proxy.
* This can be used to initialize a
* {@link fr.insa.clavardator.users.PeerUser Peeruser}
* and send messages like on a local network.
*
* @return The server address
*/
TcpConnection getProxyConnection();
}

View file

@ -1,6 +0,0 @@
package fr.insa.clavardator.server;
public enum PresenceType {
INSA,
TEST,
}

View file

@ -1,7 +0,0 @@
package fr.insa.clavardator.ui;
import fr.insa.clavardator.users.PeerUser;
public interface UserSelectedEvent {
void onSelected(PeerUser user);
}

Some files were not shown because too many files have changed in this diff Show more