Merge pull request #103 from informatik-ag-ngl/f/offline_cache

Implemented offline caching of messages and message status change events
This commit is contained in:
Kai S. K. Engelbart 2020-02-07 13:44:48 +01:00 committed by GitHub
commit 412b52ad00
13 changed files with 369 additions and 143 deletions

View File

@ -6,6 +6,7 @@ import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.prefs.Preferences; import java.util.prefs.Preferences;
import envoy.client.data.Config;
import envoy.client.ui.Color; import envoy.client.ui.Color;
import envoy.client.ui.Theme; import envoy.client.ui.Theme;
import envoy.util.SerializationUtils; import envoy.util.SerializationUtils;

View File

@ -0,0 +1,61 @@
package envoy.client.data;
import java.io.Serializable;
import java.util.LinkedList;
import java.util.Queue;
import java.util.function.Consumer;
import java.util.logging.Logger;
import envoy.client.util.EnvoyLog;
/**
* Stores elements in a queue to process them later.<br>
* <br>
* Project: <strong>envoy-client</strong><br>
* File: <strong>Cache.java</strong><br>
* Created: <strong>6 Feb 2020</strong><br>
*
* @param <T> the type of cached elements
* @author Kai S. K. Engelbart
* @since Envoy v0.3-alpha
*/
public class Cache<T> implements Consumer<T>, Serializable {
private final Queue<T> elements = new LinkedList<>();
private transient Consumer<T> processor;
private static final Logger logger = EnvoyLog.getLogger(Cache.class.getSimpleName());
private static final long serialVersionUID = 7343544675545545076L;
/**
* Adds an element to the cache.
*
* @param element the element to add
* @since Envoy v0.3-alpha
*/
@Override
public void accept(T element) {
logger.info(String.format("Adding element %s to cache", element));
elements.offer(element);
}
/**
* Sets the processor to which cached elements are relayed.
*
* @param processor the processor to set
* @since Envoy v0.3-alpha
*/
public void setProcessor(Consumer<T> processor) { this.processor = processor; }
/**
* Relays all cached elements to the processor.
*
* @throws IllegalStateException if the processor is not initialized
* @since Envoy v0.3-alpha
*/
public void relay() {
if (processor == null) throw new IllegalStateException("Processor is not defined");
elements.forEach(processor::accept);
elements.clear();
}
}

View File

@ -3,7 +3,7 @@ package envoy.client.data;
import java.io.IOException; import java.io.IOException;
import java.io.Serializable; import java.io.Serializable;
import envoy.client.net.Client; import envoy.client.net.WriteProxy;
import envoy.client.ui.list.ComponentListModel; import envoy.client.ui.list.ComponentListModel;
import envoy.data.Message; import envoy.data.Message;
import envoy.data.Message.MessageStatus; import envoy.data.Message.MessageStatus;
@ -52,22 +52,20 @@ public class Chat implements Serializable {
* {@code READ} starting from the bottom and stopping once a read message is * {@code READ} starting from the bottom and stopping once a read message is
* found. * found.
* *
* @param client the client instance used to notify the server about the message * @param writeProxy the write proxy instance used to notify the server about
* status changes * the message status changes
* @throws IOException if a {@link MessageStatusChangeEvent} could not be * @throws IOException if a {@link MessageStatusChangeEvent} could not be
* delivered to the server * delivered to the server
* @since Envoy v0.3-alpha * @since Envoy v0.3-alpha
*/ */
public void read(Client client) throws IOException { public void read(WriteProxy writeProxy) throws IOException {
for (int i = model.size() - 1; i >= 0; --i) { for (int i = model.size() - 1; i >= 0; --i) {
final Message m = model.get(i); final Message m = model.get(i);
if (m.getSenderId() == recipient.getId()) { if (m.getSenderId() == recipient.getId()) {
if (m.getStatus() == MessageStatus.READ) break; if (m.getStatus() == MessageStatus.READ) break;
else { else {
m.setStatus(MessageStatus.READ); m.setStatus(MessageStatus.READ);
writeProxy.writeMessageStatusChangeEvent(new MessageStatusChangeEvent(m));
// TODO: Cache events in offline mode
client.sendEvent(new MessageStatusChangeEvent(m));
} }
} }
} }

View File

@ -1,9 +1,12 @@
package envoy.client; package envoy.client.data;
import java.io.File; import java.io.File;
import java.security.NoSuchAlgorithmException;
import java.util.*; import java.util.*;
import java.util.function.Function;
import java.util.logging.Level; import java.util.logging.Level;
import envoy.data.LoginCredentials;
import envoy.exception.EnvoyException; import envoy.exception.EnvoyException;
/** /**
@ -26,14 +29,15 @@ public class Config {
private static Config config; private static Config config;
private Config() { private Config() {
items.put("server", new ConfigItem<>("server", "s", (input) -> input, null)); items.put("server", new ConfigItem<>("server", "s", Function.identity(), null, true));
items.put("port", new ConfigItem<>("port", "p", (input) -> Integer.parseInt(input), null)); items.put("port", new ConfigItem<>("port", "p", Integer::parseInt, null, true));
items.put("localDB", new ConfigItem<>("localDB", "db", (input) -> new File(input), new File("localDB"))); items.put("localDB", new ConfigItem<>("localDB", "db", File::new, new File("localDB"), true));
items.put("ignoreLocalDB", new ConfigItem<>("ignoreLocalDB", "nodb", (input) -> Boolean.parseBoolean(input), false)); items.put("ignoreLocalDB", new ConfigItem<>("ignoreLocalDB", "nodb", Boolean::parseBoolean, false, false));
items.put("homeDirectory", items.put("homeDirectory", new ConfigItem<>("homeDirectory", "h", File::new, new File(System.getProperty("user.home"), ".envoy"), true));
new ConfigItem<>("homeDirectory", "h", (input) -> new File(input), new File(System.getProperty("user.home"), ".envoy"))); items.put("fileLevelBarrier", new ConfigItem<>("fileLevelBarrier", "fb", Level::parse, Level.CONFIG, true));
items.put("fileLevelBarrier", new ConfigItem<>("fileLevelBarrier", "fb", (input) -> Level.parse(input), Level.CONFIG)); items.put("consoleLevelBarrier", new ConfigItem<>("consoleLevelBarrier", "cb", Level::parse, Level.FINEST, true));
items.put("consoleLevelBarrier", new ConfigItem<>("consoleLevelBarrier", "cb", (input) -> Level.parse(input), Level.FINEST)); items.put("user", new ConfigItem<>("user", "u", Function.identity()));
items.put("password", new ConfigItem<>("password", "pw", String::toCharArray));
} }
/** /**
@ -95,10 +99,12 @@ public class Config {
} }
/** /**
* @return {@code true} if server, port and localDB directory are known. * @return {@code true} if all mandatory config items are initialized
* @since Envoy v0.1-alpha * @since Envoy v0.1-alpha
*/ */
public boolean isInitialized() { return items.values().stream().map(ConfigItem::get).noneMatch(Objects::isNull); } public boolean isInitialized() {
return items.values().stream().filter(ConfigItem::isMandatory).map(ConfigItem::get).noneMatch(Objects::isNull);
}
/** /**
* @return the host name of the Envoy server * @return the host name of the Envoy server
@ -141,4 +147,35 @@ public class Config {
* @since Envoy v0.2-alpha * @since Envoy v0.2-alpha
*/ */
public Level getConsoleLevelBarrier() { return (Level) items.get("consoleLevelBarrier").get(); } public Level getConsoleLevelBarrier() { return (Level) items.get("consoleLevelBarrier").get(); }
/**
* @return the user name
* @since Envoy v0.3-alpha
*/
public String getUser() { return (String) items.get("user").get(); }
/**
* @return the password
* @since Envoy v0.3-alpha
*/
public char[] getPassword() { return (char[]) items.get("password").get(); }
/**
* @return {@code true} if user name and password are set
* @since Envoy 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 v0.3-alpha
*/
public LoginCredentials getLoginCredentials() {
try {
return new LoginCredentials(getUser(), getPassword(), false);
} catch (NoSuchAlgorithmException e) {
return null;
}
}
} }

View File

@ -1,4 +1,4 @@
package envoy.client; package envoy.client.data;
import java.util.function.Function; import java.util.function.Function;
@ -16,42 +16,63 @@ import java.util.function.Function;
*/ */
public class ConfigItem<T> { public class ConfigItem<T> {
private String commandLong, commandShort; private final String commandLong, commandShort;
private Function<String, T> parseFunction; private final Function<String, T> parseFunction;
private T value; private final boolean mandatory;
private T value;
/** /**
* Initializes a {@link ConfigItem} * Initializes a {@link ConfigItem}.
* *
* @param commandLong the long command line argument to set this value * @param commandLong the long command line argument to set this value
* @param commandShort the short command line argument to set this value * @param commandShort the short command line argument to set this value
* @param parseFunction the {@code Function<String, T>} that parses the value * @param parseFunction the {@code Function<String, T>} that parses the value
* from a string * from a string
* @param defaultValue the optional default value to set before parsing * @param defaultValue the optional default value to set before parsing
* @param mandatory indicated that this config item must be initialized with
* a non-null value
* @since Envoy v0.2-alpha * @since Envoy v0.2-alpha
*/ */
public ConfigItem(String commandLong, String commandShort, Function<String, T> parseFunction, T defaultValue) { public ConfigItem(String commandLong, String commandShort, Function<String, T> parseFunction, T defaultValue, boolean mandatory) {
this.commandLong = commandLong; this.commandLong = commandLong;
this.commandShort = commandShort; this.commandShort = commandShort;
this.parseFunction = parseFunction; this.parseFunction = parseFunction;
this.mandatory = mandatory;
value = defaultValue; value = defaultValue;
} }
/** /**
* Parses this {@ConfigItem}'s value from a string * Initializes an optional {@link ConfigItem} without a default value.
*
* @param commandLong the long command line argument to set this value
* @param commandShort the short command line argument to set this value
* @param parseFunction the {@code Function<String, T>} that parses the value
* from a string
* @since Envoy v0.3-alpha
*/
public ConfigItem(String commandLong, String commandShort, Function<String, T> parseFunction) {
this(commandLong, commandShort, parseFunction, null, false);
}
/**
* Parses this {@ConfigItem}'s value from a string.
*
* @param input the string to parse from * @param input the string to parse from
* @since Envoy v0.2-alpha * @since Envoy v0.2-alpha
*/ */
public void parse(String input) { value = parseFunction.apply(input); } public void parse(String input) { value = parseFunction.apply(input); }
/** /**
* @return The long command line argument to set the value of this {@link ConfigItem} * @return The long command line argument to set the value of this
* {@link ConfigItem}
* @since Envoy v0.2-alpha * @since Envoy v0.2-alpha
*/ */
public String getCommandLong() { return commandLong; } public String getCommandLong() { return commandLong; }
/** /**
* @return The short command line argument to set the value of this {@link ConfigItem} * @return The short command line argument to set the value of this
* {@link ConfigItem}
* @since Envoy v0.2-alpha * @since Envoy v0.2-alpha
*/ */
public String getCommandShort() { return commandShort; } public String getCommandShort() { return commandShort; }
@ -61,4 +82,11 @@ public class ConfigItem<T> {
* @since Envoy v0.2-alpha * @since Envoy v0.2-alpha
*/ */
public T get() { return value; } public T get() { return value; }
/**
* @return {@code true} if this {@link ConfigItem} is mandatory for successful
* application initialization
* @since Envoy v0.3-alpha
*/
public boolean isMandatory() { return mandatory; }
} }

View File

@ -3,7 +3,9 @@ package envoy.client.data;
import java.util.*; import java.util.*;
import envoy.data.IdGenerator; import envoy.data.IdGenerator;
import envoy.data.Message;
import envoy.data.User; import envoy.data.User;
import envoy.event.MessageStatusChangeEvent;
/** /**
* Stores information about the current {@link User} and their {@link Chat}s. * Stores information about the current {@link User} and their {@link Chat}s.
@ -18,10 +20,12 @@ import envoy.data.User;
*/ */
public abstract class LocalDb { public abstract class LocalDb {
protected User user; protected User user;
protected Map<String, User> users = new HashMap<>(); protected Map<String, User> users = new HashMap<>();
protected List<Chat> chats = new ArrayList<>(); protected List<Chat> chats = new ArrayList<>();
protected IdGenerator idGenerator; protected IdGenerator idGenerator;
protected Cache<Message> messageCache = new Cache<>();
protected Cache<MessageStatusChangeEvent> statusCache = new Cache<>();
/** /**
* Initializes a storage space for a user-specific list of chats. * Initializes a storage space for a user-specific list of chats.
@ -48,12 +52,12 @@ public abstract class LocalDb {
public void loadUsers() throws Exception {} public void loadUsers() throws Exception {}
/** /**
* Loads all chat data of the client user. * Loads all data of the client user.
* *
* @throws Exception if the loading process failed * @throws Exception if the loading process failed
* @since Envoy v0.3-alpha * @since Envoy v0.3-alpha
*/ */
public void loadChats() throws Exception {} public void loadUserData() throws Exception {}
/** /**
* Loads the ID generator. Any exception thrown during this process is ignored. * Loads the ID generator. Any exception thrown during this process is ignored.
@ -115,4 +119,28 @@ public abstract class LocalDb {
* @since Envoy v0.3-alpha * @since Envoy v0.3-alpha
*/ */
public boolean hasIdGenerator() { return idGenerator != null; } public boolean hasIdGenerator() { return idGenerator != null; }
/**
* @return the offline message cache
* @since Envoy v0.3-alpha
*/
public Cache<Message> getMessageCache() { return messageCache; }
/**
* @param messageCache the offline message cache to set
* @since Envoy v0.3-alpha
*/
public void setMessageCache(Cache<Message> messageCache) { this.messageCache = messageCache; }
/**
* @return the offline status cache
* @since Envoy v0.3-alpha
*/
public Cache<MessageStatusChangeEvent> getStatusCache() { return statusCache; }
/**
* @param statusCache the offline status cache to set
* @since Envoy v0.3-alpha
*/
public void setStatusCache(Cache<MessageStatusChangeEvent> statusCache) { this.statusCache = statusCache; }
} }

View File

@ -5,7 +5,6 @@ import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import envoy.client.ConfigItem;
import envoy.data.IdGenerator; import envoy.data.IdGenerator;
import envoy.util.SerializationUtils; import envoy.util.SerializationUtils;
@ -23,7 +22,7 @@ import envoy.util.SerializationUtils;
*/ */
public class PersistentLocalDb extends LocalDb { public class PersistentLocalDb extends LocalDb {
private File localDBDir, localDBFile, usersFile, idGeneratorFile; private File localDBDir, localDBFile, usersFile, idGeneratorFile, messageCacheFile, statusCacheFile;
/** /**
* Initializes an empty local database without a directory. All changes made to * Initializes an empty local database without a directory. All changes made to
@ -40,18 +39,18 @@ public class PersistentLocalDb extends LocalDb {
* Constructs an empty local database. To serialize any chats to the file * Constructs an empty local database. To serialize any chats to the file
* system, call {@link PersistentLocalDb#initializeUserStorage()}. * system, call {@link PersistentLocalDb#initializeUserStorage()}.
* *
* @param localDBDir the directory in which to store users and chats * @param localDbDir the directory in which to store users and chats
* @throws IOException if the PersistentLocalDb could not be initialized * @throws IOException if the PersistentLocalDb could not be initialized
* @since Envoy v0.1-alpha * @since Envoy v0.1-alpha
*/ */
public PersistentLocalDb(File localDBDir) throws IOException { public PersistentLocalDb(File localDbDir) throws IOException {
this.localDBDir = localDBDir; localDBDir = localDbDir;
// Initialize local database directory // Initialize local database directory
if (localDBDir.exists() && !localDBDir.isDirectory()) if (localDbDir.exists() && !localDbDir.isDirectory())
throw new IOException(String.format("LocalDBDir '%s' is not a directory!", localDBDir.getAbsolutePath())); throw new IOException(String.format("LocalDbDir '%s' is not a directory!", localDbDir.getAbsolutePath()));
usersFile = new File(localDBDir, "users.db"); usersFile = new File(localDbDir, "users.db");
idGeneratorFile = new File(localDBDir, "id_generator.db"); idGeneratorFile = new File(localDbDir, "id_generator.db");
} }
/** /**
@ -63,7 +62,9 @@ public class PersistentLocalDb extends LocalDb {
@Override @Override
public void initializeUserStorage() { public void initializeUserStorage() {
if (user == null) throw new NullPointerException("Client user is null"); if (user == null) throw new NullPointerException("Client user is null");
localDBFile = new File(localDBDir, user.getId() + ".db"); localDBFile = new File(localDBDir, user.getId() + ".db");
messageCacheFile = new File(localDBDir, user.getId() + "_message_cache.db");
statusCacheFile = new File(localDBDir, user.getId() + "_status_cache.db");
} }
@Override @Override
@ -71,8 +72,12 @@ public class PersistentLocalDb extends LocalDb {
// Save users // Save users
SerializationUtils.write(usersFile, users); SerializationUtils.write(usersFile, users);
// Save chats // Save user data
if (user != null) SerializationUtils.write(localDBFile, chats); if (user != null) {
SerializationUtils.write(localDBFile, chats);
SerializationUtils.write(messageCacheFile, messageCache);
SerializationUtils.write(statusCacheFile, statusCache);
}
// Save id generator // Save id generator
if (hasIdGenerator()) SerializationUtils.write(idGeneratorFile, idGenerator); if (hasIdGenerator()) SerializationUtils.write(idGeneratorFile, idGenerator);
@ -82,7 +87,11 @@ public class PersistentLocalDb extends LocalDb {
public void loadUsers() throws ClassNotFoundException, IOException { users = SerializationUtils.read(usersFile, HashMap.class); } public void loadUsers() throws ClassNotFoundException, IOException { users = SerializationUtils.read(usersFile, HashMap.class); }
@Override @Override
public void loadChats() throws ClassNotFoundException, IOException { chats = SerializationUtils.read(localDBFile, ArrayList.class); } public void loadUserData() throws ClassNotFoundException, IOException {
chats = SerializationUtils.read(localDBFile, ArrayList.class);
messageCache = SerializationUtils.read(messageCacheFile, Cache.class);
statusCache = SerializationUtils.read(statusCacheFile, Cache.class);
}
@Override @Override
public void loadIdGenerator() { public void loadIdGenerator() {

View File

@ -9,7 +9,8 @@ import java.util.logging.Logger;
import javax.naming.TimeLimitExceededException; import javax.naming.TimeLimitExceededException;
import envoy.client.Config; import envoy.client.data.Cache;
import envoy.client.data.Config;
import envoy.client.data.LocalDb; import envoy.client.data.LocalDb;
import envoy.client.util.EnvoyLog; import envoy.client.util.EnvoyLog;
import envoy.data.*; import envoy.data.*;
@ -61,7 +62,7 @@ public class Client implements Closeable {
* failed for some other reason * failed for some other reason
* @since Envoy v0.2-alpha * @since Envoy v0.2-alpha
*/ */
public MessageCache onlineInit(LoginCredentials credentials, LocalDb localDb) throws Exception { public Cache<Message> onlineInit(LoginCredentials credentials, LocalDb localDb) throws Exception {
// Establish TCP connection // Establish TCP connection
logger.info(String.format("Attempting connection to server %s:%d...", config.getServer(), config.getPort())); logger.info(String.format("Attempting connection to server %s:%d...", config.getServer(), config.getPort()));
socket = new Socket(config.getServer(), config.getPort()); socket = new Socket(config.getServer(), config.getPort());
@ -71,7 +72,7 @@ public class Client implements Closeable {
receiver = new Receiver(socket.getInputStream()); receiver = new Receiver(socket.getInputStream());
// Create cache for unread messages // Create cache for unread messages
final MessageCache cache = new MessageCache(); final Cache<Message> cache = new Cache<>();
// Register user creation processor, contact list processor and message cache // Register user creation processor, contact list processor and message cache
receiver.registerProcessor(User.class, sender -> { logger.info("Acquired user object " + sender); this.sender = sender; }); receiver.registerProcessor(User.class, sender -> { logger.info("Acquired user object " + sender); this.sender = sender; });
@ -117,6 +118,17 @@ public class Client implements Closeable {
return cache; return cache;
} }
/**
* Creates a new write proxy that uses this client to communicate with the
* server.
*
* @param localDb the local database that the write proxy will use to access
* caches
* @return a new write proxy
* @since Envoy Client v0.3-alpha
*/
public WriteProxy createWriteProxy(LocalDb localDb) { return new WriteProxy(this, localDb); }
/** /**
* Sends a message to the server. The message's status will be incremented once * Sends a message to the server. The message's status will be incremented once
* it was delivered successfully. * it was delivered successfully.

View File

@ -1,54 +0,0 @@
package envoy.client.net;
import java.util.LinkedList;
import java.util.Queue;
import java.util.function.Consumer;
import java.util.logging.Logger;
import envoy.client.util.EnvoyLog;
import envoy.data.Message;
/**
* Stores messages in a queue until the application initialization is complete.
* The messages can then be relayed to a processor.<br>
* <br>
* Project: <strong>envoy-client</strong><br>
* File: <strong>MessageCache.java</strong><br>
* Created: <strong>4 Feb 2020</strong><br>
*
* @author Kai S. K. Engelbart
* @since Envoy v0.3-alpha
*/
public class MessageCache implements Consumer<Message> {
private final Queue<Message> messages = new LinkedList<>();
private Consumer<Message> processor;
private static final Logger logger = EnvoyLog.getLogger(MessageCache.class.getSimpleName());
/**
* Adds a message to the cache.
*
* @since Envoy v0.3-alpha
*/
@Override
public void accept(Message message) {
logger.info(String.format("Adding message %s to cache", message));
messages.add(message);
}
/**
* Sets the processor to which messages are relayed.
*
* @param processor the processor to set
* @since Envoy v0.3-alpha
*/
public void setProcessor(Consumer<Message> processor) { this.processor = processor; }
/**
* Relays all cached messages to the processor.
*
* @since Envoy v0.3-alpha
*/
public void relayMessages() { messages.forEach(processor::accept); }
}

View File

@ -0,0 +1,103 @@
package envoy.client.net;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
import envoy.client.data.LocalDb;
import envoy.client.util.EnvoyLog;
import envoy.data.Message;
import envoy.event.MessageStatusChangeEvent;
/**
* Implements methods to send {@link Message}s and
* {@link MessageStatusChangeEvent}s to the server or cache them inside a
* {@link LocalDb} depending on the online status.<br>
* <br>
* Project: <strong>envoy-client</strong><br>
* File: <strong>WriteProxy.java</strong><br>
* Created: <strong>6 Feb 2020</strong><br>
*
* @author Kai S. K. Engelbart
* @since Envoy v0.3-alpha
*/
public class WriteProxy {
private final Client client;
private final LocalDb localDb;
private static final Logger logger = EnvoyLog.getLogger(WriteProxy.class.getSimpleName());
/**
* Initializes a write proxy using a client and a local database. The
* corresponding cache processors are injected into the caches.
*
* @param client the client used to send messages and message status change
* events
* @param localDb the local database used to cache messages and message status
* change events
* @since Envoy v0.3-alpha
*/
public WriteProxy(Client client, LocalDb localDb) {
this.client = client;
this.localDb = localDb;
// Initialize cache processors for messages and message status change events
localDb.getMessageCache().setProcessor(msg -> {
try {
client.sendMessage(msg);
} catch (IOException e) {
logger.log(Level.SEVERE, "Could not send cached message", e);
}
});
localDb.getStatusCache().setProcessor(evt -> {
try {
client.sendEvent(evt);
} catch (IOException e) {
logger.log(Level.SEVERE, "Could not send cached message status change event", e);
}
});
}
/**
* Sends cached {@link Message}s and {@link MessageStatusChangeEvent}s to the
* server.
*
* @since Envoy v0.3-alpha
*/
public void flushCache() {
logger.info("Sending cached messages and message status change events...");
// Send messages
localDb.getMessageCache().relay();
// Send message status change events
localDb.getStatusCache().relay();
}
/**
* Delivers a message to the server if online. Otherwise the message is cached
* inside the local database.
*
* @param message the message to send
* @throws IOException if the message could not be sent
* @since Envoy v0.3-alpha
*/
public void writeMessage(Message message) throws IOException {
if (client.isOnline()) client.sendMessage(message);
else localDb.getMessageCache().accept(message);
}
/**
* Delivers a message status change event to the server if online. Otherwise the
* event is cached inside the local database.
*
* @param evt the event to send
* @throws IOException if the event could not be sent
* @since Envoy v0.3-alpha
*/
public void writeMessageStatusChangeEvent(MessageStatusChangeEvent evt) throws IOException {
if (client.isOnline()) client.sendEvent(evt);
else localDb.getStatusCache().accept(evt);
}
}

View File

@ -16,6 +16,7 @@ import envoy.client.data.LocalDb;
import envoy.client.event.MessageCreationEvent; import envoy.client.event.MessageCreationEvent;
import envoy.client.event.ThemeChangeEvent; import envoy.client.event.ThemeChangeEvent;
import envoy.client.net.Client; import envoy.client.net.Client;
import envoy.client.net.WriteProxy;
import envoy.client.ui.list.ComponentList; import envoy.client.ui.list.ComponentList;
import envoy.client.ui.settings.SettingsScreen; import envoy.client.ui.settings.SettingsScreen;
import envoy.client.util.EnvoyLog; import envoy.client.util.EnvoyLog;
@ -39,8 +40,9 @@ import envoy.event.MessageStatusChangeEvent;
public class ChatWindow extends JFrame { public class ChatWindow extends JFrame {
// User specific objects // User specific objects
private Client client; private Client client;
private LocalDb localDb; private WriteProxy writeProxy;
private LocalDb localDb;
// GUI components // GUI components
private JPanel contentPane = new JPanel(); private JPanel contentPane = new JPanel();
@ -211,7 +213,7 @@ public class ChatWindow extends JFrame {
// Listen to received messages // Listen to received messages
EventBus.getInstance().register(MessageCreationEvent.class, (evt) -> { EventBus.getInstance().register(MessageCreationEvent.class, (evt) -> {
Message message = ((MessageCreationEvent) evt).get(); Message message = ((MessageCreationEvent) evt).get();
Chat chat = localDb.getChats().stream().filter(c -> c.getRecipient().getId() == message.getSenderId()).findFirst().get(); Chat chat = localDb.getChats().stream().filter(c -> c.getRecipient().getId() == message.getSenderId()).findFirst().get();
chat.appendMessage(message); chat.appendMessage(message);
@ -301,8 +303,7 @@ public class ChatWindow extends JFrame {
.build(); .build();
// Send message // Send message
// TODO: Store offline messages writeProxy.writeMessage(message);
client.sendMessage(message);
// Add message to PersistentLocalDb and update UI // Add message to PersistentLocalDb and update UI
currentChat.appendMessage(message); currentChat.appendMessage(message);
@ -345,7 +346,7 @@ public class ChatWindow extends JFrame {
private void readCurrentChat() { private void readCurrentChat() {
try { try {
currentChat.read(client); currentChat.read(writeProxy);
messageList.synchronizeModel(); messageList.synchronizeModel();
} catch (IOException e) { } catch (IOException e) {
e.printStackTrace(); e.printStackTrace();
@ -354,24 +355,23 @@ public class ChatWindow extends JFrame {
} }
/** /**
* Sets the {@link Client} used by this {@link ChatWindow}. * Initializes the components responsible server communication and
* persistence.<br>
* <br>
* This will trigger the display of the contact list.
* *
* @param client the {@link Client} used to send and receive messages * @param client the client used to send and receive messages
* @since Envoy v0.2-alpha * @param localDb the local database used to manage stored messages
* and users
* @param writeProxy the write proxy used to send messages and status change
* events to the server or cache them inside the local
* database
* @since Envoy v0.3-alpha
*/ */
public void setClient(Client client) { this.client = client; } public void initContent(Client client, LocalDb localDb, WriteProxy writeProxy) {
this.client = client;
/** this.localDb = localDb;
* Sets the {@link LocalDb} used by this {@link ChatWindow}. After this.writeProxy = writeProxy;
* invoking this
* method, users and chats will be loaded from the database into the GUI.
*
* @param localDb the {@link LocalDb} used to manage stored messages
* and users
* @since Envoy v0.2-alpha
*/
public void setLocalDB(LocalDb localDb) {
this.localDb = localDb;
loadUsersAndChats(); loadUsersAndChats();
} }
} }

View File

@ -11,15 +11,13 @@ import javax.swing.JFrame;
import javax.swing.JOptionPane; import javax.swing.JOptionPane;
import javax.swing.SwingUtilities; import javax.swing.SwingUtilities;
import envoy.client.Config;
import envoy.client.Settings; import envoy.client.Settings;
import envoy.client.data.LocalDb; import envoy.client.data.*;
import envoy.client.data.PersistentLocalDb;
import envoy.client.data.TransientLocalDb;
import envoy.client.net.Client; import envoy.client.net.Client;
import envoy.client.net.MessageCache; import envoy.client.net.WriteProxy;
import envoy.client.util.EnvoyLog; import envoy.client.util.EnvoyLog;
import envoy.data.LoginCredentials; import envoy.data.LoginCredentials;
import envoy.data.Message;
import envoy.data.User; import envoy.data.User;
import envoy.exception.EnvoyException; import envoy.exception.EnvoyException;
@ -64,7 +62,7 @@ public class Startup {
// Override configuration values with command line arguments // Override configuration values with command line arguments
if (args.length > 0) config.load(args); if (args.length > 0) config.load(args);
// Check if all configuration values have been initialized // Check if all mandatory configuration values have been initialized
if (!config.isInitialized()) throw new EnvoyException("Server or port are not defined"); if (!config.isInitialized()) throw new EnvoyException("Server or port are not defined");
} catch (Exception e) { } catch (Exception e) {
JOptionPane JOptionPane
@ -77,8 +75,8 @@ public class Startup {
EnvoyLog.setFileLevelBarrier(config.getFileLevelBarrier()); EnvoyLog.setFileLevelBarrier(config.getFileLevelBarrier());
EnvoyLog.setConsoleLevelBarrier(config.getConsoleLevelBarrier()); EnvoyLog.setConsoleLevelBarrier(config.getConsoleLevelBarrier());
// Ask the user for their user name and password // Acquire login credentials
LoginCredentials credentials = new LoginDialog().getCredentials(); LoginCredentials credentials = config.hasLoginCredentials() ? config.getLoginCredentials() : new LoginDialog().getCredentials();
if (credentials == null) { if (credentials == null) {
logger.info("The login process has been aborted by the user. Exiting..."); logger.info("The login process has been aborted by the user. Exiting...");
@ -108,7 +106,7 @@ public class Startup {
// Acquire the client user (with ID) either from the server or from the local // Acquire the client user (with ID) either from the server or from the local
// database, which triggers offline mode // database, which triggers offline mode
Client client = new Client(); Client client = new Client();
MessageCache cache = null; Cache<Message> cache = null;
try { try {
// Try entering online mode first // Try entering online mode first
localDb.loadIdGenerator(); localDb.loadIdGenerator();
@ -139,7 +137,7 @@ public class Startup {
// Initialize chats in local database // Initialize chats in local database
try { try {
localDb.initializeUserStorage(); localDb.initializeUserStorage();
localDb.loadChats(); localDb.loadUserData();
} catch (FileNotFoundException e) { } catch (FileNotFoundException e) {
// The local database file has not yet been created, probably first login // The local database file has not yet been created, probably first login
} catch (Exception e) { } catch (Exception e) {
@ -150,14 +148,19 @@ public class Startup {
JOptionPane.WARNING_MESSAGE); JOptionPane.WARNING_MESSAGE);
} }
// Save all users to the local database // Initialize write proxy
if (client.isOnline()) localDb.setUsers(client.getUsers()); final WriteProxy writeProxy = client.createWriteProxy(localDb);
// Save all users to the local database and flush cache
if (client.isOnline()) {
localDb.setUsers(client.getUsers());
writeProxy.flushCache();
}
// Display ChatWindow and StatusTrayIcon // Display ChatWindow and StatusTrayIcon
EventQueue.invokeLater(() -> { EventQueue.invokeLater(() -> {
try { try {
chatWindow.setClient(client); chatWindow.initContent(client, localDb, writeProxy);
chatWindow.setLocalDB(localDb);
try { try {
new StatusTrayIcon(chatWindow).show(); new StatusTrayIcon(chatWindow).show();
@ -178,7 +181,7 @@ public class Startup {
}); });
// Relay unread messages from cache // Relay unread messages from cache
if (cache != null) cache.relayMessages(); if (cache != null) cache.relay();
// Save Settings and PersistentLocalDb on shutdown // Save Settings and PersistentLocalDb on shutdown
Runtime.getRuntime().addShutdownHook(new Thread(() -> { Runtime.getRuntime().addShutdownHook(new Thread(() -> {

View File

@ -4,7 +4,7 @@ import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.logging.*; import java.util.logging.*;
import envoy.client.Config; import envoy.client.data.Config;
/** /**
* Project: <strong>envoy-client</strong><br> * Project: <strong>envoy-client</strong><br>