2020-02-05 20:58:30 +01:00
|
|
|
package envoy.client.data;
|
2020-02-03 21:52:48 +01:00
|
|
|
|
2020-09-16 15:41:00 +02:00
|
|
|
import java.io.*;
|
2020-09-19 15:28:04 +02:00
|
|
|
import java.nio.channels.*;
|
|
|
|
import java.nio.file.StandardOpenOption;
|
2020-07-16 17:04:35 +02:00
|
|
|
import java.time.Instant;
|
2020-02-03 21:52:48 +01:00
|
|
|
import java.util.*;
|
2020-09-22 14:42:51 +02:00
|
|
|
import java.util.logging.Level;
|
2020-02-03 21:52:48 +01:00
|
|
|
|
2020-09-22 14:42:51 +02:00
|
|
|
import envoy.client.event.EnvoyCloseEvent;
|
2020-04-02 09:23:47 +02:00
|
|
|
import envoy.data.*;
|
2020-09-16 15:41:00 +02:00
|
|
|
import envoy.event.*;
|
2020-09-19 15:28:04 +02:00
|
|
|
import envoy.exception.EnvoyException;
|
2020-09-22 14:42:51 +02:00
|
|
|
import envoy.util.*;
|
2020-02-03 21:52:48 +01:00
|
|
|
|
2020-09-19 11:37:42 +02:00
|
|
|
import dev.kske.eventbus.Event;
|
|
|
|
import dev.kske.eventbus.EventBus;
|
|
|
|
import dev.kske.eventbus.EventListener;
|
|
|
|
|
2020-02-03 21:52:48 +01:00
|
|
|
/**
|
|
|
|
* Stores information about the current {@link User} and their {@link Chat}s.
|
2020-06-26 23:36:14 +02:00
|
|
|
* For message ID generation a {@link IDGenerator} is stored as well.
|
|
|
|
* <p>
|
2020-09-16 15:41:00 +02:00
|
|
|
* The managed objects are stored inside a folder in the local file system.
|
|
|
|
* <p>
|
2020-02-03 21:52:48 +01:00
|
|
|
* Project: <strong>envoy-client</strong><br>
|
2020-03-24 18:38:47 +01:00
|
|
|
* File: <strong>LocalDB.java</strong><br>
|
2020-02-03 21:52:48 +01:00
|
|
|
* Created: <strong>3 Feb 2020</strong><br>
|
|
|
|
*
|
|
|
|
* @author Kai S. K. Engelbart
|
2020-03-23 21:52:33 +01:00
|
|
|
* @since Envoy Client v0.3-alpha
|
2020-02-03 21:52:48 +01:00
|
|
|
*/
|
2020-09-19 11:37:42 +02:00
|
|
|
public final class LocalDB implements EventListener {
|
2020-09-16 15:41:00 +02:00
|
|
|
|
2020-09-20 11:12:33 +02:00
|
|
|
// Data
|
2020-09-16 15:41:00 +02:00
|
|
|
private User user;
|
2020-09-20 11:12:33 +02:00
|
|
|
private Map<String, User> users = Collections.synchronizedMap(new HashMap<>());
|
|
|
|
private List<Chat> chats = Collections.synchronizedList(new ArrayList<>());
|
2020-09-16 15:41:00 +02:00
|
|
|
private IDGenerator idGenerator;
|
|
|
|
private CacheMap cacheMap = new CacheMap();
|
2020-09-19 11:37:42 +02:00
|
|
|
private String authToken;
|
2020-09-20 11:12:33 +02:00
|
|
|
|
|
|
|
// State management
|
|
|
|
private Instant lastSync = Instant.EPOCH;
|
|
|
|
|
|
|
|
// Persistence
|
2020-09-22 14:42:51 +02:00
|
|
|
private File userFile;
|
2020-09-20 11:12:33 +02:00
|
|
|
private FileLock instanceLock;
|
2020-09-16 15:41:00 +02:00
|
|
|
|
2020-09-22 14:42:51 +02:00
|
|
|
private final File dbDir, idGeneratorFile, lastLoginFile, usersFile;
|
|
|
|
|
2020-09-16 15:41:00 +02:00
|
|
|
/**
|
2020-09-22 14:42:51 +02:00
|
|
|
* Constructs an empty local database.
|
2020-09-16 15:41:00 +02:00
|
|
|
*
|
|
|
|
* @param dbDir the directory in which to persist data
|
2020-09-20 11:12:33 +02:00
|
|
|
* @throws IOException if {@code dbDir} is a file (and not a directory)
|
|
|
|
* @throws EnvoyException if {@code dbDir} is in use by another Envoy instance
|
2020-09-16 15:41:00 +02:00
|
|
|
* @since Envoy Client v0.2-beta
|
|
|
|
*/
|
2020-09-20 11:12:33 +02:00
|
|
|
public LocalDB(File dbDir) throws IOException, EnvoyException {
|
2020-09-16 15:41:00 +02:00
|
|
|
this.dbDir = dbDir;
|
2020-09-19 11:37:42 +02:00
|
|
|
EventBus.getInstance().registerListener(this);
|
2020-02-03 21:52:48 +01:00
|
|
|
|
2020-09-20 09:08:09 +02:00
|
|
|
// Ensure that the database directory exists
|
2020-09-22 14:42:51 +02:00
|
|
|
if (!dbDir.exists()) dbDir.mkdirs();
|
|
|
|
else if (!dbDir.isDirectory()) throw new IOException(String.format("LocalDBDir '%s' is not a directory!", dbDir.getAbsolutePath()));
|
2020-07-09 10:53:27 +02:00
|
|
|
|
2020-09-20 11:12:33 +02:00
|
|
|
// Lock the directory
|
|
|
|
lock();
|
2020-09-20 09:08:09 +02:00
|
|
|
|
2020-09-16 15:41:00 +02:00
|
|
|
// Initialize global files
|
|
|
|
idGeneratorFile = new File(dbDir, "id_gen.db");
|
2020-09-19 11:37:42 +02:00
|
|
|
lastLoginFile = new File(dbDir, "last_login.db");
|
2020-09-16 15:41:00 +02:00
|
|
|
usersFile = new File(dbDir, "users.db");
|
|
|
|
|
2020-09-20 11:12:33 +02:00
|
|
|
// Load global files
|
|
|
|
loadGlobalData();
|
|
|
|
|
2020-09-16 15:41:00 +02:00
|
|
|
// Initialize offline caches
|
2020-07-09 10:53:27 +02:00
|
|
|
cacheMap.put(Message.class, new Cache<>());
|
|
|
|
cacheMap.put(MessageStatusChange.class, new Cache<>());
|
|
|
|
}
|
2020-02-03 21:52:48 +01:00
|
|
|
|
2020-09-19 15:28:04 +02:00
|
|
|
/**
|
|
|
|
* Ensured that only one Envoy instance is using this local database by creating
|
|
|
|
* a lock file.
|
|
|
|
* The lock file is deleted on application exit.
|
|
|
|
*
|
|
|
|
* @throws EnvoyException if the lock cannot by acquired
|
|
|
|
* @since Envoy Client v0.2-beta
|
|
|
|
*/
|
2020-09-20 11:12:33 +02:00
|
|
|
private synchronized void lock() throws EnvoyException {
|
2020-09-22 14:42:51 +02:00
|
|
|
final var file = new File(dbDir, "instance.lock");
|
2020-09-19 15:28:04 +02:00
|
|
|
try {
|
2020-09-22 14:42:51 +02:00
|
|
|
final var fc = FileChannel.open(file.toPath(), StandardOpenOption.CREATE, StandardOpenOption.WRITE);
|
2020-09-19 15:28:04 +02:00
|
|
|
instanceLock = fc.tryLock();
|
|
|
|
if (instanceLock == null) throw new EnvoyException("Another Envoy instance is using this local database!");
|
2020-09-22 14:42:51 +02:00
|
|
|
} catch (final IOException e) {
|
2020-09-19 15:28:04 +02:00
|
|
|
throw new EnvoyException("Could not create lock file!", e);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-02-03 21:52:48 +01:00
|
|
|
/**
|
2020-09-20 11:12:33 +02:00
|
|
|
* Loads the local user registry {@code users.db}, the id generator
|
|
|
|
* {@code id_gen.db} and last login file {@code last_login.db}.
|
2020-02-03 21:52:48 +01:00
|
|
|
*
|
2020-09-20 11:12:33 +02:00
|
|
|
* @since Envoy Client v0.2-beta
|
2020-02-03 21:52:48 +01:00
|
|
|
*/
|
2020-09-20 11:12:33 +02:00
|
|
|
private synchronized void loadGlobalData() {
|
|
|
|
try {
|
|
|
|
try (var in = new ObjectInputStream(new FileInputStream(usersFile))) {
|
|
|
|
users = (Map<String, User>) in.readObject();
|
|
|
|
}
|
|
|
|
idGenerator = SerializationUtils.read(idGeneratorFile, IDGenerator.class);
|
|
|
|
try (var in = new ObjectInputStream(new FileInputStream(lastLoginFile))) {
|
|
|
|
user = (User) in.readObject();
|
|
|
|
authToken = (String) in.readObject();
|
|
|
|
}
|
|
|
|
} catch (IOException | ClassNotFoundException e) {}
|
2020-09-16 15:41:00 +02:00
|
|
|
}
|
2020-02-03 21:52:48 +01:00
|
|
|
|
|
|
|
/**
|
2020-02-06 21:42:17 +01:00
|
|
|
* Loads all data of the client user.
|
2020-02-03 21:52:48 +01:00
|
|
|
*
|
2020-09-16 15:41:00 +02:00
|
|
|
* @throws ClassNotFoundException if the loading process failed
|
|
|
|
* @throws IOException if the loading process failed
|
2020-03-23 21:52:33 +01:00
|
|
|
* @since Envoy Client v0.3-alpha
|
2020-02-03 21:52:48 +01:00
|
|
|
*/
|
2020-09-20 11:12:33 +02:00
|
|
|
public synchronized void loadUserData() throws ClassNotFoundException, IOException {
|
|
|
|
if (user == null) throw new IllegalStateException("Client user is null, cannot initialize user storage");
|
|
|
|
userFile = new File(dbDir, user.getID() + ".db");
|
2020-09-16 15:41:00 +02:00
|
|
|
try (var in = new ObjectInputStream(new FileInputStream(userFile))) {
|
2020-09-20 11:12:33 +02:00
|
|
|
chats = (List<Chat>) in.readObject();
|
2020-09-16 15:41:00 +02:00
|
|
|
cacheMap = (CacheMap) in.readObject();
|
|
|
|
lastSync = (Instant) in.readObject();
|
2020-09-22 16:06:19 +02:00
|
|
|
} finally {
|
|
|
|
synchronize();
|
2020-09-16 15:41:00 +02:00
|
|
|
}
|
2020-09-19 11:37:42 +02:00
|
|
|
}
|
|
|
|
|
2020-06-26 09:08:41 +02:00
|
|
|
/**
|
|
|
|
* Synchronizes the contact list of the client user with the chat and user
|
|
|
|
* storage.
|
2020-09-19 11:37:42 +02:00
|
|
|
*
|
2020-06-26 09:08:41 +02:00
|
|
|
* @since Envoy Client v0.1-beta
|
|
|
|
*/
|
2020-09-20 11:12:33 +02:00
|
|
|
private void synchronize() {
|
2020-07-16 17:04:35 +02:00
|
|
|
user.getContacts().stream().filter(u -> u instanceof User && !users.containsKey(u.getName())).forEach(u -> users.put(u.getName(), (User) u));
|
2020-06-26 09:08:41 +02:00
|
|
|
users.put(user.getName(), user);
|
|
|
|
|
|
|
|
// Synchronize user status data
|
2020-09-22 14:42:51 +02:00
|
|
|
for (final var contact : users.values())
|
|
|
|
if (contact instanceof User) getChat(contact.getID()).ifPresent(chat -> { ((User) chat.getRecipient()).setStatus(contact.getStatus()); });
|
2020-06-26 09:08:41 +02:00
|
|
|
|
|
|
|
// Create missing chats
|
2020-07-05 14:38:19 +02:00
|
|
|
user.getContacts()
|
|
|
|
.stream()
|
|
|
|
.filter(c -> !c.equals(user) && getChat(c.getID()).isEmpty())
|
|
|
|
.map(c -> c instanceof User ? new Chat(c) : new GroupChat(user, c))
|
|
|
|
.forEach(chats::add);
|
2020-06-26 09:08:41 +02:00
|
|
|
}
|
|
|
|
|
2020-09-22 16:37:43 +02:00
|
|
|
/**
|
|
|
|
* Initializes a timer that automatically saves this local database after a
|
|
|
|
* period of time specified in the settings.
|
|
|
|
*
|
|
|
|
* @since Envoy Client v0.2-beta
|
|
|
|
*/
|
|
|
|
public void initAutoSave() {
|
|
|
|
new Timer("LocalDB Autosave", true).schedule(new TimerTask() {
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void run() { save(); }
|
|
|
|
}, 2000, ClientConfig.getInstance().getLocalDBSaveInterval() * 60000);
|
|
|
|
}
|
|
|
|
|
2020-09-20 11:12:33 +02:00
|
|
|
/**
|
|
|
|
* 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.
|
|
|
|
*
|
|
|
|
* @throws IOException if the saving process failed
|
|
|
|
* @since Envoy Client v0.3-alpha
|
|
|
|
*/
|
2020-09-22 14:42:51 +02:00
|
|
|
@Event(eventType = EnvoyCloseEvent.class, priority = 1000)
|
|
|
|
private synchronized void save() {
|
|
|
|
EnvoyLog.getLogger(LocalDB.class).log(Level.INFO, "Saving local database...");
|
2020-09-20 11:12:33 +02:00
|
|
|
|
|
|
|
// Save users
|
2020-09-22 14:42:51 +02:00
|
|
|
try {
|
|
|
|
SerializationUtils.write(usersFile, users);
|
2020-09-20 11:12:33 +02:00
|
|
|
|
2020-09-22 14:42:51 +02:00
|
|
|
// Save user data and last sync time stamp
|
|
|
|
if (user != null)
|
|
|
|
SerializationUtils.write(userFile, chats, cacheMap, Context.getInstance().getClient().isOnline() ? Instant.now() : lastSync);
|
2020-09-20 11:12:33 +02:00
|
|
|
|
2020-09-22 14:42:51 +02:00
|
|
|
// Save last login information
|
|
|
|
if (authToken != null) SerializationUtils.write(lastLoginFile, user, authToken);
|
2020-09-20 11:12:33 +02:00
|
|
|
|
2020-09-22 14:42:51 +02:00
|
|
|
// Save ID generator
|
|
|
|
if (hasIDGenerator()) SerializationUtils.write(idGeneratorFile, idGenerator);
|
|
|
|
} catch (final IOException e) {
|
|
|
|
EnvoyLog.getLogger(LocalDB.class).log(Level.SEVERE, "Unable to save local database: ", e);
|
|
|
|
}
|
2020-09-19 11:37:42 +02:00
|
|
|
}
|
|
|
|
|
2020-09-22 16:06:19 +02:00
|
|
|
/**
|
|
|
|
* Stores a new authentication token.
|
|
|
|
*
|
|
|
|
* @param evt the event containing the authentication token
|
|
|
|
* @since Envoy Client v0.2-beta
|
|
|
|
*/
|
2020-09-20 11:12:33 +02:00
|
|
|
@Event
|
|
|
|
private void onNewAuthToken(NewAuthToken evt) { authToken = evt.get(); }
|
|
|
|
|
2020-02-03 21:52:48 +01:00
|
|
|
/**
|
|
|
|
* @return a {@code Map<String, User>} of all users stored locally with their
|
|
|
|
* user names as keys
|
2020-03-23 21:52:33 +01:00
|
|
|
* @since Envoy Client v0.2-alpha
|
2020-02-03 21:52:48 +01:00
|
|
|
*/
|
2020-07-16 17:04:35 +02:00
|
|
|
public Map<String, User> getUsers() { return users; }
|
2020-02-03 21:52:48 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @return all saved {@link Chat} objects that list the client user as the
|
|
|
|
* sender
|
2020-03-23 21:52:33 +01:00
|
|
|
* @since Envoy Client v0.1-alpha
|
2020-02-03 21:52:48 +01:00
|
|
|
**/
|
|
|
|
public List<Chat> getChats() { return chats; }
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param chats the chats to set
|
|
|
|
*/
|
|
|
|
public void setChats(List<Chat> chats) { this.chats = chats; }
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return the {@link User} who initialized the local database
|
2020-03-23 21:52:33 +01:00
|
|
|
* @since Envoy Client v0.2-alpha
|
2020-02-03 21:52:48 +01:00
|
|
|
*/
|
|
|
|
public User getUser() { return user; }
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param user the user to set
|
2020-03-23 21:52:33 +01:00
|
|
|
* @since Envoy Client v0.2-alpha
|
2020-02-03 21:52:48 +01:00
|
|
|
*/
|
|
|
|
public void setUser(User user) { this.user = user; }
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return the message ID generator
|
2020-03-23 21:52:33 +01:00
|
|
|
* @since Envoy Client v0.3-alpha
|
2020-02-03 21:52:48 +01:00
|
|
|
*/
|
2020-03-26 16:06:18 +01:00
|
|
|
public IDGenerator getIDGenerator() { return idGenerator; }
|
2020-02-03 21:52:48 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @param idGenerator the message ID generator to set
|
2020-03-23 21:52:33 +01:00
|
|
|
* @since Envoy Client v0.3-alpha
|
2020-02-03 21:52:48 +01:00
|
|
|
*/
|
2020-03-26 16:06:18 +01:00
|
|
|
public void setIDGenerator(IDGenerator idGenerator) { this.idGenerator = idGenerator; }
|
2020-02-03 21:52:48 +01:00
|
|
|
|
|
|
|
/**
|
2020-03-26 16:06:18 +01:00
|
|
|
* @return {@code true} if an {@link IDGenerator} is present
|
2020-03-23 21:52:33 +01:00
|
|
|
* @since Envoy Client v0.3-alpha
|
2020-02-03 21:52:48 +01:00
|
|
|
*/
|
2020-03-26 16:06:18 +01:00
|
|
|
public boolean hasIDGenerator() { return idGenerator != null; }
|
2020-02-06 21:03:08 +01:00
|
|
|
|
|
|
|
/**
|
2020-07-09 10:53:27 +02:00
|
|
|
* @return the cache map for messages and message status changes
|
|
|
|
* @since Envoy Client v0.1-beta
|
2020-02-06 21:03:08 +01:00
|
|
|
*/
|
2020-07-09 10:53:27 +02:00
|
|
|
public CacheMap getCacheMap() { return cacheMap; }
|
2020-03-14 16:58:19 +01:00
|
|
|
|
2020-07-16 17:04:35 +02:00
|
|
|
/**
|
|
|
|
* @return the time stamp when the database was last saved
|
|
|
|
* @since Envoy Client v0.2-beta
|
|
|
|
*/
|
|
|
|
public Instant getLastSync() { return lastSync; }
|
|
|
|
|
2020-09-19 11:37:42 +02:00
|
|
|
/**
|
|
|
|
* @return the authentication token of the user
|
|
|
|
* @since Envoy Client v0.2-beta
|
|
|
|
*/
|
|
|
|
public String getAuthToken() { return authToken; }
|
|
|
|
|
2020-03-14 16:58:19 +01:00
|
|
|
/**
|
|
|
|
* Searches for a message by ID.
|
2020-03-26 16:06:18 +01:00
|
|
|
*
|
2020-03-14 16:58:19 +01:00
|
|
|
* @param id the ID of the message to search for
|
2020-06-10 13:05:44 +02:00
|
|
|
* @return an optional containing the message
|
2020-03-23 21:52:33 +01:00
|
|
|
* @since Envoy Client v0.1-beta
|
2020-03-14 16:58:19 +01:00
|
|
|
*/
|
2020-06-10 13:05:44 +02:00
|
|
|
public Optional<Message> getMessage(long id) {
|
|
|
|
return chats.stream().map(Chat::getMessages).flatMap(List::stream).filter(m -> m.getID() == id).findAny();
|
|
|
|
}
|
2020-06-26 09:08:41 +02:00
|
|
|
|
2020-06-10 13:05:44 +02:00
|
|
|
/**
|
|
|
|
* Searches for a chat by recipient ID.
|
2020-09-19 11:37:42 +02:00
|
|
|
*
|
2020-06-10 13:05:44 +02:00
|
|
|
* @param recipientID the ID of the chat's recipient
|
|
|
|
* @return an optional containing the chat
|
|
|
|
* @since Envoy Client v0.1-beta
|
|
|
|
*/
|
2020-06-26 09:08:41 +02:00
|
|
|
public Optional<Chat> getChat(long recipientID) { return chats.stream().filter(c -> c.getRecipient().getID() == recipientID).findAny(); }
|
2020-04-02 09:23:47 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Performs a contact name change if the corresponding contact is present.
|
|
|
|
*
|
2020-06-20 10:00:38 +02:00
|
|
|
* @param event the {@link NameChange} to process
|
2020-04-02 09:23:47 +02:00
|
|
|
* @since Envoy Client v0.1-beta
|
|
|
|
*/
|
2020-06-20 10:00:38 +02:00
|
|
|
public void replaceContactName(NameChange event) {
|
2020-04-02 22:03:43 +02:00
|
|
|
chats.stream().map(Chat::getRecipient).filter(c -> c.getID() == event.getID()).findAny().ifPresent(c -> c.setName(event.get()));
|
2020-04-02 09:23:47 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Performs a group resize operation if the corresponding group is present.
|
|
|
|
*
|
2020-06-20 10:00:38 +02:00
|
|
|
* @param event the {@link GroupResize} to process
|
2020-04-02 09:23:47 +02:00
|
|
|
* @since Envoy Client v0.1-beta
|
|
|
|
*/
|
2020-06-20 10:00:38 +02:00
|
|
|
public void updateGroup(GroupResize event) {
|
2020-04-02 09:23:47 +02:00
|
|
|
chats.stream()
|
|
|
|
.map(Chat::getRecipient)
|
|
|
|
.filter(Group.class::isInstance)
|
|
|
|
.filter(g -> g.getID() == event.getGroupID() && g.getID() != user.getID())
|
|
|
|
.map(Group.class::cast)
|
|
|
|
.findAny()
|
|
|
|
.ifPresent(group -> {
|
|
|
|
switch (event.getOperation()) {
|
|
|
|
case ADD:
|
2020-04-02 22:03:43 +02:00
|
|
|
group.getContacts().add(event.get());
|
2020-04-02 09:23:47 +02:00
|
|
|
break;
|
|
|
|
case REMOVE:
|
2020-04-02 22:03:43 +02:00
|
|
|
group.getContacts().remove(event.get());
|
2020-04-02 09:23:47 +02:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
2020-02-03 21:52:48 +01:00
|
|
|
}
|