Prepare handshake synchronization

Common
* Replace LocalDateTime with Instant everywhere

Client
* Display message creation date with system time zone in MessageControl
* LocalDB#users now strictly contains Users
* lastSync time stamp in LocalDB (saved per user)
* isOnline parameter in save function (lastSync updated if true)
* lastSync time stamp in LoginCredentials
* No ClientConfig#getLoginCredentials because of missing information,
  moved to LoginScene
* Pass LocalDB#lastSync to LoginCredentials in LoginScene

Server
* Explicit lastSync parameter for
  PersistenceManager#getPending(Group)Messages

This sends the correct time stamp to the server, however the JPQL
queries have yet to be adjusted.
This commit is contained in:
2020-07-16 17:04:35 +02:00
parent abd0113588
commit 07c4ccf3c8
23 changed files with 189 additions and 159 deletions

View File

@ -5,10 +5,8 @@ import static java.util.function.Function.identity;
import java.io.File;
import java.util.logging.Level;
import envoy.client.ui.Startup;
import envoy.data.Config;
import envoy.data.ConfigItem;
import envoy.data.LoginCredentials;
/**
* Implements a configuration specific to the Envoy Client with default values
@ -105,11 +103,4 @@ public class ClientConfig extends Config {
* @since Envoy Client v0.3-alpha
*/
public boolean hasLoginCredentials() { return getUser() != null && getPassword() != null; }
/**
* @return login credentials for the specified user name and password, without
* the registration option
* @since Envoy Client v0.3-alpha
*/
public LoginCredentials getLoginCredentials() { return new LoginCredentials(getUser(), getPassword(), false, Startup.VERSION); }
}

View File

@ -1,7 +1,7 @@
package envoy.client.data;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.Instant;
import envoy.client.net.WriteProxy;
import envoy.data.Contact;
@ -46,7 +46,7 @@ public class GroupChat extends Chat {
else {
gmsg.getMemberStatuses().replace(sender.getID(), MessageStatus.READ);
writeProxy
.writeMessageStatusChange(new GroupMessageStatusChange(gmsg.getID(), MessageStatus.READ, LocalDateTime.now(), sender.getID()));
.writeMessageStatusChange(new GroupMessageStatusChange(gmsg.getID(), MessageStatus.READ, Instant.now(), sender.getID()));
}
}
}

View File

@ -1,5 +1,6 @@
package envoy.client.data;
import java.time.Instant;
import java.util.*;
import envoy.data.*;
@ -20,11 +21,12 @@ import envoy.event.NameChange;
*/
public abstract class LocalDB {
protected User user;
protected Map<String, Contact> users = new HashMap<>();
protected List<Chat> chats = new ArrayList<>();
protected IDGenerator idGenerator;
protected CacheMap cacheMap = new CacheMap();
protected User user;
protected Map<String, User> users = new HashMap<>();
protected List<Chat> chats = new ArrayList<>();
protected IDGenerator idGenerator;
protected CacheMap cacheMap = new CacheMap();
protected Instant lastSync = Instant.EPOCH;
{
cacheMap.put(Message.class, new Cache<>());
@ -42,10 +44,11 @@ public abstract class LocalDB {
* Stores all users. If the client user is specified, their chats will be stored
* as well. The message id generator will also be saved if present.
*
* @param isOnline determines which {@code lastSync} time stamp is saved
* @throws Exception if the saving process failed
* @since Envoy Client v0.3-alpha
*/
public void save() throws Exception {}
public void save(boolean isOnline) throws Exception {}
/**
* Loads all user data.
@ -77,7 +80,7 @@ public abstract class LocalDB {
* @since Envoy Client v0.1-beta
*/
public void synchronize() {
user.getContacts().stream().filter(u -> u instanceof User && !users.containsKey(u.getName())).forEach(u -> users.put(u.getName(), u));
user.getContacts().stream().filter(u -> u instanceof User && !users.containsKey(u.getName())).forEach(u -> users.put(u.getName(), (User) u));
users.put(user.getName(), user);
// Synchronize user status data
@ -98,7 +101,7 @@ public abstract class LocalDB {
* user names as keys
* @since Envoy Client v0.2-alpha
*/
public Map<String, Contact> getUsers() { return users; }
public Map<String, User> getUsers() { return users; }
/**
* @return all saved {@link Chat} objects that list the client user as the
@ -148,6 +151,12 @@ public abstract class LocalDB {
*/
public CacheMap getCacheMap() { return cacheMap; }
/**
* @return the time stamp when the database was last saved
* @since Envoy Client v0.2-beta
*/
public Instant getLastSync() { return lastSync; }
/**
* Searches for a message by ID.
*

View File

@ -1,6 +1,7 @@
package envoy.client.data;
import java.io.*;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HashMap;
@ -26,7 +27,7 @@ public final class PersistentLocalDB extends LocalDB {
/**
* Constructs an empty local database. To serialize any user-specific data to
* the file system, call {@link PersistentLocalDB#initializeUserStorage()} first
* and then {@link PersistentLocalDB#save()}.
* and then {@link PersistentLocalDB#save(boolean)}.
*
* @param dbDir the directory in which to persist data
* @throws IOException if {@code dbDir} is a file (and not a directory)
@ -57,12 +58,12 @@ public final class PersistentLocalDB extends LocalDB {
}
@Override
public void save() throws IOException {
public void save(boolean isOnline) throws IOException {
// Save users
SerializationUtils.write(usersFile, users);
// Save user data
if (user != null) SerializationUtils.write(userFile, chats, cacheMap);
// Save user data and last sync time stamp
if (user != null) SerializationUtils.write(userFile, chats, cacheMap, isOnline ? Instant.now() : lastSync);
// Save id generator
if (hasIDGenerator()) SerializationUtils.write(idGeneratorFile, idGenerator);
@ -76,6 +77,7 @@ public final class PersistentLocalDB extends LocalDB {
try (var in = new ObjectInputStream(new FileInputStream(userFile))) {
chats = (ArrayList<Chat>) in.readObject();
cacheMap = (CacheMap) in.readObject();
lastSync = (Instant) in.readObject();
}
}

View File

@ -121,12 +121,13 @@ public final class Startup extends Application {
@Override
public void stop() {
try {
logger.log(Level.INFO, "Saving local database and settings...");
localDB.save(client.isOnline());
Settings.getInstance().save();
logger.log(Level.INFO, "Closing connection...");
client.close();
logger.log(Level.INFO, "Saving local database and settings...");
localDB.save();
Settings.getInstance().save();
logger.log(Level.INFO, "Envoy was terminated by its user");
} catch (final Exception e) {
logger.log(Level.SEVERE, "Unable to save local files: ", e);

View File

@ -183,12 +183,11 @@ public final class ChatScene implements Restorable {
final var contact = e.get();
switch (e.getOperationType()) {
case ADD:
localDB.getUsers().put(contact.getName(), contact);
if (contact instanceof User) localDB.getUsers().put(contact.getName(), (User) contact);
Chat chat = contact instanceof User ? new Chat(contact) : new GroupChat(client.getSender(), contact);
Platform.runLater(() -> chatList.getItems().add(chat));
break;
case REMOVE:
localDB.getUsers().remove(contact.getName());
Platform.runLater(() -> chatList.getItems().removeIf(c -> c.getRecipient().equals(contact)));
break;
}

View File

@ -2,6 +2,7 @@ package envoy.client.ui.controller;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.time.Instant;
import java.util.concurrent.TimeoutException;
import java.util.logging.Level;
import java.util.logging.Logger;
@ -94,7 +95,8 @@ public final class LoginScene {
userTextField.requestFocus();
// Perform automatic login if configured
if (config.hasLoginCredentials()) performHandshake(config.getLoginCredentials());
if (config.hasLoginCredentials())
performHandshake(new LoginCredentials(config.getUser(), config.getPassword(), false, Startup.VERSION, loadLastSync(config.getUser())));
}
@FXML
@ -108,12 +110,13 @@ public final class LoginScene {
new Alert(AlertType.ERROR, "The entered user name is not valid (" + Bounds.CONTACT_NAME_PATTERN + ")").showAndWait();
userTextField.getTextField().clear();
} else performHandshake(new LoginCredentials(userTextField.getTextField().getText(), passwordField.getText(), registerCheckBox.isSelected(),
Startup.VERSION));
Startup.VERSION, loadLastSync(userTextField.getTextField().getText())));
}
@FXML
private void offlineModeButtonPressed() {
attemptOfflineMode(new LoginCredentials(userTextField.getTextField().getText(), passwordField.getText(), false, Startup.VERSION));
attemptOfflineMode(
new LoginCredentials(userTextField.getTextField().getText(), passwordField.getText(), false, Startup.VERSION, localDB.getLastSync()));
}
@FXML
@ -130,6 +133,18 @@ public final class LoginScene {
System.exit(0);
}
private Instant loadLastSync(String identifier) {
try {
localDB.loadUsers();
localDB.setUser(localDB.getUsers().get(identifier));
localDB.initializeUserStorage();
localDB.loadUserData();
} catch (Exception e) {
// User storage empty, wrong user name etc. -> default lastSync
}
return localDB.getLastSync();
}
private void performHandshake(LoginCredentials credentials) {
try {
client.performHandshake(credentials, cacheMap);

View File

@ -3,6 +3,7 @@ package envoy.client.ui.listcell;
import java.awt.Toolkit;
import java.awt.datatransfer.StringSelection;
import java.io.ByteArrayInputStream;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Map;
import java.util.logging.Level;
@ -35,11 +36,12 @@ import envoy.util.EnvoyLog;
*/
public class MessageControl extends Label {
private static User client;
private static final DateTimeFormatter dateFormat = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm");
private static final Map<MessageStatus, Image> statusImages = IconUtil.loadByEnum(MessageStatus.class, 16);
private static User client;
private static final Logger logger = EnvoyLog.getLogger(MessageControl.class);
private static final DateTimeFormatter dateFormat = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm:ss")
.withZone(ZoneId.systemDefault());
private static final Map<MessageStatus, Image> statusImages = IconUtil.loadByEnum(MessageStatus.class, 16);
private static final Logger logger = EnvoyLog.getLogger(MessageControl.class);
/**
*
@ -68,7 +70,8 @@ public class MessageControl extends Label {
if (message.hasAttachment()) {
switch (message.getAttachment().getType()) {
case PICTURE:
vbox.getChildren().add(new ImageView(new Image(new ByteArrayInputStream(message.getAttachment().getData()), 256, 256, true, true)));
vbox.getChildren()
.add(new ImageView(new Image(new ByteArrayInputStream(message.getAttachment().getData()), 256, 256, true, true)));
break;
case VIDEO:
break;