diff --git a/src/main/java/envoy/client/Config.java b/src/main/java/envoy/client/Config.java
index fc93a08..24e7780 100644
--- a/src/main/java/envoy/client/Config.java
+++ b/src/main/java/envoy/client/Config.java
@@ -1,9 +1,7 @@
package envoy.client;
import java.io.File;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Properties;
+import java.util.*;
import java.util.logging.Level;
import envoy.exception.EnvoyException;
@@ -31,6 +29,7 @@ public class Config {
items.put("server", new ConfigItem<>("server", "s", (input) -> input, null));
items.put("port", new ConfigItem<>("port", "p", (input) -> Integer.parseInt(input), null));
items.put("localDB", new ConfigItem<>("localDB", "db", (input) -> new File(input), new File("localDB")));
+ items.put("ignoreLocalDB", new ConfigItem<>("ignoreLocalDB", "nodb", (input) -> Boolean.parseBoolean(input), false));
items.put("homeDirectory",
new ConfigItem<>("homeDirectory", "h", (input) -> new File(input), new File(System.getProperty("user.home"), ".envoy")));
items.put("fileLevelBarrier", new ConfigItem<>("fileLevelBarrier", "fb", (input) -> Level.parse(input), Level.CONFIG));
@@ -99,7 +98,7 @@ public class Config {
* @return {@code true} if server, port and localDB directory are known.
* @since Envoy v0.1-alpha
*/
- public boolean isInitialized() { return items.values().stream().noneMatch(item -> item.get() == null); }
+ public boolean isInitialized() { return items.values().stream().map(ConfigItem::get).noneMatch(Objects::isNull); }
/**
* @return the host name of the Envoy server
@@ -119,6 +118,12 @@ public class Config {
*/
public File getLocalDB() { return (File) items.get("localDB").get(); }
+ /**
+ * @return {@code true} if the local database is to be ignored
+ * @since Envoy v0.3-alpha
+ */
+ public Boolean isIgnoreLocalDB() { return (Boolean) items.get("ignoreLocalDB").get(); }
+
/**
* @return the directory in which all local files are saves
* @since Envoy v0.2-alpha
diff --git a/src/main/java/envoy/client/LocalDB.java b/src/main/java/envoy/client/LocalDB.java
deleted file mode 100644
index 7511d41..0000000
--- a/src/main/java/envoy/client/LocalDB.java
+++ /dev/null
@@ -1,163 +0,0 @@
-package envoy.client;
-
-import java.io.File;
-import java.io.IOException;
-import java.util.*;
-
-import envoy.data.IdGenerator;
-import envoy.data.User;
-import envoy.util.SerializationUtils;
-
-/**
- * Stored information about the current {@link User} and their {@link Chat}s.
- * For message ID generation a {@link IdGenerator} is stored as well.
- * These object are persisted inside a folder of the local file system.
- *
- * Project: envoy-client
- * File: LocalDB.java
- * Created: 27.10.2019
- *
- * @author Kai S. K. Engelbart
- * @author Maximilian Käfer
- * @since Envoy v0.1-alpha
- */
-public class LocalDB {
-
- private File localDBDir, localDBFile, usersFile, idGeneratorFile;
- private User user;
- private Map users = new HashMap<>();
- private List chats = new ArrayList<>();
- private IdGenerator idGenerator;
-
- /**
- * Constructs an empty local database. To serialize any chats to the file
- * system, call {@link LocalDB#initializeDBFile()}.
- *
- * @param localDBDir the directory in which to store users and chats
- * @throws IOException if the LocalDB could not be initialized
- * @since Envoy v0.1-alpha
- */
- public LocalDB(File localDBDir) throws IOException {
- this.localDBDir = localDBDir;
-
- // Initialize local database directory
- if (localDBDir.exists() && !localDBDir.isDirectory())
- throw new IOException(String.format("LocalDBDir '%s' is not a directory!", localDBDir.getAbsolutePath()));
- usersFile = new File(localDBDir, "users.db");
- idGeneratorFile = new File(localDBDir, "id_generator.db");
- }
-
- /**
- * Creates a database file for a user-specific list of chats.
- *
- * @throws NullPointerException if the client user is not yet specified
- * @since Envoy v0.1-alpha
- */
- public void initializeDBFile() {
- if (user == null) throw new NullPointerException("Client user is null");
- localDBFile = new File(localDBDir, user.getId() + ".db");
- }
-
- /**
- * Stores all users to the local database. If the client user is specified, the
- * chats related to this user are stored as well. The message id generator will
- * also be saved if present.
- *
- * @throws IOException if something went wrong during saving
- * @since Envoy v0.1-alpha
- */
- public void save() throws IOException {
- // Save users
- SerializationUtils.write(usersFile, users);
-
- // Save chats
- if (user != null) SerializationUtils.write(localDBFile, chats);
-
- // Save id generator
- if (hasIdGenerator()) SerializationUtils.write(idGeneratorFile, idGenerator);
- }
-
- /**
- * Loads all users that are stored in the local database.
- *
- * @throws IOException if the loading process failed
- * @throws ClassNotFoundException if the loading process failed
- * @since Envoy v0.2-alpha
- */
- public void loadUsers() throws ClassNotFoundException, IOException { users = SerializationUtils.read(usersFile, HashMap.class); }
-
- /**
- * Loads all chats saved by Envoy for the client user.
- *
- * @throws IOException if the loading process failed
- * @throws ClassNotFoundException if the loading process failed
- * @since Envoy v0.1-alpha
- */
- public void loadChats() throws ClassNotFoundException, IOException { chats = SerializationUtils.read(localDBFile, ArrayList.class); }
-
- /**
- * Loads the message ID generator that is stored in the local database. If the
- * file is not found, the exception is ignored.
- *
- * @since Envoy v0.3-alpha
- */
- public void loadIdGenerator() {
- try {
- idGenerator = SerializationUtils.read(idGeneratorFile, IdGenerator.class);
- } catch (ClassNotFoundException | IOException e) {}
- }
-
- /**
- * @return a {@code Map} of all users stored locally with their
- * user names as keys
- * @since Envoy v0.2-alpha
- */
- public Map getUsers() { return users; }
-
- /**
- * @param users the users to set
- */
- public void setUsers(Map users) { this.users = users; }
-
- /**
- * @return all saved {@link Chat} objects that list the client user as the
- * sender
- * @since Envoy v0.1-alpha
- **/
- public List getChats() { return chats; }
-
- /**
- * @param chats the chats to set
- */
- public void setChats(List chats) { this.chats = chats; }
-
- /**
- * @return the {@link User} who initialized the local database
- * @since Envoy v0.2-alpha
- */
- public User getUser() { return user; }
-
- /**
- * @param user the user to set
- * @since Envoy v0.2-alpha
- */
- public void setUser(User user) { this.user = user; }
-
- /**
- * @return the message ID generator
- * @since Envoy v0.3-alpha
- */
- public IdGenerator getIdGenerator() { return idGenerator; }
-
- /**
- * @param idGenerator the message ID generator to set
- * @since Envoy v0.3-alpha
- */
- public void setIdGenerator(IdGenerator idGenerator) { this.idGenerator = idGenerator; }
-
- /**
- * @return {@code true} if an {@link IdGenerator} is present
- * @since Envoy v0.3-alpha
- */
- public boolean hasIdGenerator() { return idGenerator != null; }
-}
\ No newline at end of file
diff --git a/src/main/java/envoy/client/Chat.java b/src/main/java/envoy/client/data/Chat.java
similarity index 52%
rename from src/main/java/envoy/client/Chat.java
rename to src/main/java/envoy/client/data/Chat.java
index a00e46d..f10e816 100644
--- a/src/main/java/envoy/client/Chat.java
+++ b/src/main/java/envoy/client/data/Chat.java
@@ -1,11 +1,14 @@
-package envoy.client;
+package envoy.client.data;
+import java.io.IOException;
import java.io.Serializable;
+import envoy.client.net.Client;
import envoy.client.ui.list.ComponentListModel;
import envoy.data.Message;
import envoy.data.Message.MessageStatus;
import envoy.data.User;
+import envoy.event.MessageStatusChangeEvent;
/**
* Represents a chat between two {@link User}s
@@ -24,8 +27,8 @@ public class Chat implements Serializable {
private static final long serialVersionUID = -7751248474547242056L;
- private User recipient;
- private ComponentListModel model = new ComponentListModel<>();
+ private final User recipient;
+ private final ComponentListModel model = new ComponentListModel<>();
/**
* Provides the list of messages that the recipient receives.
@@ -45,17 +48,38 @@ public class Chat implements Serializable {
public void appendMessage(Message message) { model.add(message); }
/**
- * Sets the status of all chat messages to {@code READ} starting from the bottom
- * and stopping once a read message is found.
- *
+ * Sets the status of all chat messages received from the recipient to
+ * {@code READ} starting from the bottom and stopping once a read message is
+ * found.
+ *
+ * @param client the client instance used to notify the server about the message
+ * status changes
+ * @throws IOException if a {@link MessageStatusChangeEvent} could not be
+ * delivered to the server
* @since Envoy v0.3-alpha
*/
- public void read() {
- for (int i = model.size() - 1; i >= 0; --i)
- if (model.get(i).getStatus() == MessageStatus.READ) break;
- else model.get(i).setStatus(MessageStatus.READ);
+ public void read(Client client) throws IOException {
+ for (int i = model.size() - 1; i >= 0; --i) {
+ final Message m = model.get(i);
+ if (m.getSenderId() == recipient.getId()) {
+ if (m.getStatus() == MessageStatus.READ) break;
+ else {
+ m.setStatus(MessageStatus.READ);
+
+ // TODO: Cache events in offline mode
+ client.sendEvent(new MessageStatusChangeEvent(m));
+ }
+ }
+ }
}
+ /**
+ * @return {@code true} if the newest message received in the chat doesn't have
+ * the status {@code READ}
+ * @since Envoy v0.3-alpha
+ */
+ public boolean isUnread() { return !model.isEmpty() && model.get(model.size() - 1).getStatus() != MessageStatus.READ; }
+
/**
* @return all messages in the current chat
* @since Envoy v0.1-alpha
diff --git a/src/main/java/envoy/client/data/LocalDb.java b/src/main/java/envoy/client/data/LocalDb.java
new file mode 100644
index 0000000..79368a8
--- /dev/null
+++ b/src/main/java/envoy/client/data/LocalDb.java
@@ -0,0 +1,118 @@
+package envoy.client.data;
+
+import java.util.*;
+
+import envoy.data.IdGenerator;
+import envoy.data.User;
+
+/**
+ * Stores information about the current {@link User} and their {@link Chat}s.
+ * For message ID generation a {@link IdGenerator} is stored as well.
+ *
+ * Project: envoy-client
+ * File: LocalDb.java
+ * Created: 3 Feb 2020
+ *
+ * @author Kai S. K. Engelbart
+ * @since Envoy v0.3-alpha
+ */
+public abstract class LocalDb {
+
+ protected User user;
+ protected Map users = new HashMap<>();
+ protected List chats = new ArrayList<>();
+ protected IdGenerator idGenerator;
+
+ /**
+ * Initializes a storage space for a user-specific list of chats.
+ *
+ * @since Envoy v0.3-alpha
+ */
+ public void initializeUserStorage() {}
+
+ /**
+ * 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 Exception if the saving process failed
+ * @since Envoy v0.3-alpha
+ */
+ public void save() throws Exception {}
+
+ /**
+ * Loads all user data.
+ *
+ * @throws Exception if the loading process failed
+ * @since Envoy v0.3-alpha
+ */
+ public void loadUsers() throws Exception {}
+
+ /**
+ * Loads all chat data of the client user.
+ *
+ * @throws Exception if the loading process failed
+ * @since Envoy v0.3-alpha
+ */
+ public void loadChats() throws Exception {}
+
+ /**
+ * Loads the ID generator. Any exception thrown during this process is ignored.
+ *
+ * @since Envoy v0.3-alpha
+ */
+ public void loadIdGenerator() {}
+
+ /**
+ * @return a {@code Map} of all users stored locally with their
+ * user names as keys
+ * @since Envoy v0.2-alpha
+ */
+ public Map getUsers() { return users; }
+
+ /**
+ * @param users the users to set
+ */
+ public void setUsers(Map users) { this.users = users; }
+
+ /**
+ * @return all saved {@link Chat} objects that list the client user as the
+ * sender
+ * @since Envoy v0.1-alpha
+ **/
+ public List getChats() { return chats; }
+
+ /**
+ * @param chats the chats to set
+ */
+ public void setChats(List chats) { this.chats = chats; }
+
+ /**
+ * @return the {@link User} who initialized the local database
+ * @since Envoy v0.2-alpha
+ */
+ public User getUser() { return user; }
+
+ /**
+ * @param user the user to set
+ * @since Envoy v0.2-alpha
+ */
+ public void setUser(User user) { this.user = user; }
+
+ /**
+ * @return the message ID generator
+ * @since Envoy v0.3-alpha
+ */
+ public IdGenerator getIdGenerator() { return idGenerator; }
+
+ /**
+ * @param idGenerator the message ID generator to set
+ * @since Envoy v0.3-alpha
+ */
+ public void setIdGenerator(IdGenerator idGenerator) { this.idGenerator = idGenerator; }
+
+ /**
+ * @return {@code true} if an {@link IdGenerator} is present
+ * @since Envoy v0.3-alpha
+ */
+ public boolean hasIdGenerator() { return idGenerator != null; }
+}
diff --git a/src/main/java/envoy/client/data/PersistentLocalDb.java b/src/main/java/envoy/client/data/PersistentLocalDb.java
new file mode 100644
index 0000000..7856517
--- /dev/null
+++ b/src/main/java/envoy/client/data/PersistentLocalDb.java
@@ -0,0 +1,93 @@
+package envoy.client.data;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+
+import envoy.client.ConfigItem;
+import envoy.data.IdGenerator;
+import envoy.util.SerializationUtils;
+
+/**
+ * Implements a {@link LocalDb} in a way that stores all information inside a
+ * folder on the local file system.
+ *
+ * Project: envoy-client
+ * File: PersistentLocalDb.java
+ * Created: 27.10.2019
+ *
+ * @author Kai S. K. Engelbart
+ * @author Maximilian Käfer
+ * @since Envoy v0.1-alpha
+ */
+public class PersistentLocalDb extends LocalDb {
+
+ private File localDBDir, localDBFile, usersFile, idGeneratorFile;
+
+ /**
+ * Initializes an empty local database without a directory. All changes made to
+ * this instance cannot be saved to the file system.
+ *
+ * This constructor shall be used in conjunction with the {@code ignoreLocalDB}
+ * {@link ConfigItem}.
+ *
+ * @since Envoy v0.3-alpha
+ */
+ public PersistentLocalDb() {}
+
+ /**
+ * Constructs an empty local database. To serialize any chats to the file
+ * system, call {@link PersistentLocalDb#initializeUserStorage()}.
+ *
+ * @param localDBDir the directory in which to store users and chats
+ * @throws IOException if the PersistentLocalDb could not be initialized
+ * @since Envoy v0.1-alpha
+ */
+ public PersistentLocalDb(File localDBDir) throws IOException {
+ this.localDBDir = localDBDir;
+
+ // Initialize local database directory
+ if (localDBDir.exists() && !localDBDir.isDirectory())
+ throw new IOException(String.format("LocalDBDir '%s' is not a directory!", localDBDir.getAbsolutePath()));
+ usersFile = new File(localDBDir, "users.db");
+ idGeneratorFile = new File(localDBDir, "id_generator.db");
+ }
+
+ /**
+ * Creates a database file for a user-specific list of chats.
+ *
+ * @throws NullPointerException if the client user is not yet specified
+ * @since Envoy v0.1-alpha
+ */
+ @Override
+ public void initializeUserStorage() {
+ if (user == null) throw new NullPointerException("Client user is null");
+ localDBFile = new File(localDBDir, user.getId() + ".db");
+ }
+
+ @Override
+ public void save() throws IOException {
+ // Save users
+ SerializationUtils.write(usersFile, users);
+
+ // Save chats
+ if (user != null) SerializationUtils.write(localDBFile, chats);
+
+ // Save id generator
+ if (hasIdGenerator()) SerializationUtils.write(idGeneratorFile, idGenerator);
+ }
+
+ @Override
+ public void loadUsers() throws ClassNotFoundException, IOException { users = SerializationUtils.read(usersFile, HashMap.class); }
+
+ @Override
+ public void loadChats() throws ClassNotFoundException, IOException { chats = SerializationUtils.read(localDBFile, ArrayList.class); }
+
+ @Override
+ public void loadIdGenerator() {
+ try {
+ idGenerator = SerializationUtils.read(idGeneratorFile, IdGenerator.class);
+ } catch (ClassNotFoundException | IOException e) {}
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/envoy/client/data/TransientLocalDb.java b/src/main/java/envoy/client/data/TransientLocalDb.java
new file mode 100644
index 0000000..433488a
--- /dev/null
+++ b/src/main/java/envoy/client/data/TransientLocalDb.java
@@ -0,0 +1,15 @@
+package envoy.client.data;
+
+/**
+ * Implements a {@link LocalDb} in a way that does not persist any information
+ * after application shutdown.
+ *
+ * Project: envoy-client
+ * File: TransientLocalDb.java
+ * Created: 3 Feb 2020
+ *
+ * @author Kai S. K. Engelbart
+ * @since Envoy v0.3-alpha
+ */
+public class TransientLocalDb extends LocalDb {
+}
diff --git a/src/main/java/envoy/client/event/MessageCreationEvent.java b/src/main/java/envoy/client/event/MessageCreationEvent.java
index 36397d9..72e47f5 100644
--- a/src/main/java/envoy/client/event/MessageCreationEvent.java
+++ b/src/main/java/envoy/client/event/MessageCreationEvent.java
@@ -8,9 +8,12 @@ import envoy.data.Message;
* Created: 4 Dec 2019
*
* @author Kai S. K. Engelbart
+ * @since Envoy v0.2-alpha
*/
public class MessageCreationEvent extends MessageEvent {
+ private static final long serialVersionUID = -6451021678064566774L;
+
/**
* @param message the {@link Message} that has been created
*/
diff --git a/src/main/java/envoy/client/event/MessageEvent.java b/src/main/java/envoy/client/event/MessageEvent.java
index a1b5c68..fb50109 100644
--- a/src/main/java/envoy/client/event/MessageEvent.java
+++ b/src/main/java/envoy/client/event/MessageEvent.java
@@ -9,9 +9,12 @@ import envoy.event.Event;
* Created: 4 Dec 2019
*
* @author Kai S. K. Engelbart
+ * @since Envoy v0.2-alpha
*/
public class MessageEvent implements Event {
+ private static final long serialVersionUID = 7658989461923112804L;
+
/**
* the {@link Message} attached to this {@link MessageEvent}.
*/
diff --git a/src/main/java/envoy/client/event/MessageModificationEvent.java b/src/main/java/envoy/client/event/MessageModificationEvent.java
index 248c6f1..4077383 100644
--- a/src/main/java/envoy/client/event/MessageModificationEvent.java
+++ b/src/main/java/envoy/client/event/MessageModificationEvent.java
@@ -8,9 +8,12 @@ import envoy.data.Message;
* Created: 4 Dec 2019
*
* @author Kai S. K. Engelbart
+ * @since Envoy v0.2-alpha
*/
public class MessageModificationEvent extends MessageEvent {
+ private static final long serialVersionUID = 4650039506439563116L;
+
/**
* @param message the {@link Message} that has been modified
*/
diff --git a/src/main/java/envoy/client/event/ThemeChangeEvent.java b/src/main/java/envoy/client/event/ThemeChangeEvent.java
index adb9707..02a15df 100644
--- a/src/main/java/envoy/client/event/ThemeChangeEvent.java
+++ b/src/main/java/envoy/client/event/ThemeChangeEvent.java
@@ -13,7 +13,8 @@ import envoy.event.Event;
*/
public class ThemeChangeEvent implements Event {
- private final Theme theme;
+ private static final long serialVersionUID = 6756772448803774547L;
+ private final Theme theme;
/**
* Initializes a {@link ThemeChangeEvent} conveying information about the change
diff --git a/src/main/java/envoy/client/Client.java b/src/main/java/envoy/client/net/Client.java
similarity index 69%
rename from src/main/java/envoy/client/Client.java
rename to src/main/java/envoy/client/net/Client.java
index 935669f..91e9040 100644
--- a/src/main/java/envoy/client/Client.java
+++ b/src/main/java/envoy/client/net/Client.java
@@ -1,4 +1,4 @@
-package envoy.client;
+package envoy.client.net;
import java.io.Closeable;
import java.io.IOException;
@@ -9,12 +9,19 @@ import java.util.logging.Logger;
import javax.naming.TimeLimitExceededException;
+import envoy.client.Config;
+import envoy.client.data.LocalDb;
import envoy.client.util.EnvoyLog;
import envoy.data.*;
+import envoy.event.Event;
import envoy.event.IdGeneratorRequest;
+import envoy.event.MessageStatusChangeEvent;
import envoy.util.SerializationUtils;
/**
+ * Establishes a connection to the server, performs a handshake and delivers
+ * certain objects to the server.
+ *
* Project: envoy-client
* File: Client.java
* Created: 28 Sep 2019
@@ -26,18 +33,18 @@ import envoy.util.SerializationUtils;
*/
public class Client implements Closeable {
+ // Connection handling
private Socket socket;
private Receiver receiver;
private boolean online;
- private volatile User sender;
- private User recipient;
+ // Asynchronously initialized during handshake
+ private volatile User sender;
+ private volatile Contacts contacts;
- private volatile Contacts contacts;
-
- private Config config = Config.getInstance();
-
- private static final Logger logger = EnvoyLog.getLogger(Client.class.getSimpleName());
+ // Configuration and logging
+ private static final Config config = Config.getInstance();
+ private static final Logger logger = EnvoyLog.getLogger(Client.class.getSimpleName());
/**
* Enters the online mode by acquiring a user ID from the server. As a
@@ -46,13 +53,15 @@ public class Client implements Closeable {
* an exception is thrown.
*
* @param credentials the login credentials of the user
- * @param localDB the local database used to persist the current
+ * @param localDb the local database used to persist the current
* {@link IdGenerator}
+ * @return a message cache containing all unread messages from the server that
+ * can be relayed after initialization
* @throws Exception if the online mode could not be entered or the request
* failed for some other reason
* @since Envoy v0.2-alpha
*/
- public void onlineInit(LoginCredentials credentials, LocalDB localDB) throws Exception {
+ public MessageCache onlineInit(LoginCredentials credentials, LocalDb localDb) throws Exception {
// Establish TCP connection
logger.info(String.format("Attempting connection to server %s:%d...", config.getServer(), config.getPort()));
socket = new Socket(config.getServer(), config.getPort());
@@ -61,9 +70,13 @@ public class Client implements Closeable {
// Create message receiver
receiver = new Receiver(socket.getInputStream());
- // Register user creation processor
+ // Create cache for unread messages
+ final MessageCache cache = new MessageCache();
+
+ // 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(Contacts.class, contacts -> { logger.info("Acquired contacts object " + contacts); this.contacts = contacts; });
+ receiver.registerProcessor(Message.class, cache);
// Start receiver
new Thread(receiver).start();
@@ -86,18 +99,27 @@ public class Client implements Closeable {
receiver.removeAllProcessors();
// Register processors for message and status handling
- receiver.registerProcessor(Message.class, new ReceivedMessageProcessor());
- // TODO: Status handling
+ final ReceivedMessageProcessor receivedMessageProcessor = new ReceivedMessageProcessor();
+ receiver.registerProcessor(Message.class, receivedMessageProcessor);
+
+ // Relay cached unread messages
+ cache.setProcessor(receivedMessageProcessor);
+
+ // Process message status changes
+ receiver.registerProcessor(MessageStatusChangeEvent.class, new MessageStatusChangeEventProcessor());
// Process message ID generation
- receiver.registerProcessor(IdGenerator.class, localDB::setIdGenerator);
+ receiver.registerProcessor(IdGenerator.class, localDb::setIdGenerator);
- // Request a generator if none is present
- if (!localDB.hasIdGenerator() || !localDB.getIdGenerator().hasNext()) requestIdGenerator();
+ // Request a generator if none is present or the existing one is consumed
+ if (!localDb.hasIdGenerator() || !localDb.getIdGenerator().hasNext()) requestIdGenerator();
+
+ return cache;
}
/**
- * Sends a message to the server.
+ * Sends a message to the server. The message's status will be incremented once
+ * it was delivered successfully.
*
* @param message the message to send
* @throws IOException if the message does not reach the server
@@ -108,6 +130,14 @@ public class Client implements Closeable {
message.nextStatus();
}
+ /**
+ * Sends an event to the server.
+ *
+ * @param evt the event to send
+ * @throws IOException if the event did not reach the server
+ */
+ public void sendEvent(Event> evt) throws IOException { writeObject(evt); }
+
/**
* Requests a new {@link IdGenerator} from the server.
*
@@ -155,26 +185,6 @@ public class Client implements Closeable {
*/
public void setSender(User sender) { this.sender = sender; }
- /**
- * @return the current recipient of the current chat.
- * @since Envoy v0.1-alpha
- */
- public User getRecipient() { return recipient; }
-
- /**
- * Sets the recipient.
- *
- * @param recipient the recipient to set
- * @since Envoy v0.1-alpha
- */
- public void setRecipient(User recipient) { this.recipient = recipient; }
-
- /**
- * @return true, if a recipient is selected
- * @since Envoy v0.1-alpha
- */
- public boolean hasRecipient() { return recipient != null; }
-
/**
* @return the {@link Receiver} used by this {@link Client}
*/
diff --git a/src/main/java/envoy/client/net/MessageCache.java b/src/main/java/envoy/client/net/MessageCache.java
new file mode 100644
index 0000000..1fc49c3
--- /dev/null
+++ b/src/main/java/envoy/client/net/MessageCache.java
@@ -0,0 +1,54 @@
+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.
+ *
+ * Project: envoy-client
+ * File: MessageCache.java
+ * Created: 4 Feb 2020
+ *
+ * @author Kai S. K. Engelbart
+ * @since Envoy v0.3-alpha
+ */
+public class MessageCache implements Consumer {
+
+ private final Queue messages = new LinkedList<>();
+ private Consumer 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 processor) { this.processor = processor; }
+
+ /**
+ * Relays all cached messages to the processor.
+ *
+ * @since Envoy v0.3-alpha
+ */
+ public void relayMessages() { messages.forEach(processor::accept); }
+}
diff --git a/src/main/java/envoy/client/net/MessageStatusChangeEventProcessor.java b/src/main/java/envoy/client/net/MessageStatusChangeEventProcessor.java
new file mode 100644
index 0000000..1cf457f
--- /dev/null
+++ b/src/main/java/envoy/client/net/MessageStatusChangeEventProcessor.java
@@ -0,0 +1,38 @@
+package envoy.client.net;
+
+import java.util.function.Consumer;
+import java.util.logging.Logger;
+
+import envoy.client.util.EnvoyLog;
+import envoy.data.Message.MessageStatus;
+import envoy.event.EventBus;
+import envoy.event.MessageStatusChangeEvent;
+
+/**
+ * Project: envoy-client
+ * File: MessageStatusChangeEventProcessor.java
+ * Created: 4 Feb 2020
+ *
+ * @author Kai S. K. Engelbart
+ * @since Envoy v0.3-alpha
+ */
+public class MessageStatusChangeEventProcessor implements Consumer {
+
+ private static final Logger logger = EnvoyLog.getLogger(MessageStatusChangeEventProcessor.class.getSimpleName());
+
+ /**
+ * Dispatches a {@link MessageStatusChangeEvent} if the status is
+ * {@code RECEIVED} or {@code READ}.
+ *
+ * @param evt the status change event
+ * @since Envoy v0.3-alpha
+ */
+ @Override
+ public void accept(MessageStatusChangeEvent evt) {
+ if (evt.get().ordinal() < MessageStatus.RECEIVED.ordinal()) logger.info("Received invalid message status change " + evt);
+ else {
+ logger.info("Received " + evt.toString());
+ EventBus.getInstance().dispatch(evt);
+ }
+ }
+}
diff --git a/src/main/java/envoy/client/ReceivedMessageProcessor.java b/src/main/java/envoy/client/net/ReceivedMessageProcessor.java
similarity index 90%
rename from src/main/java/envoy/client/ReceivedMessageProcessor.java
rename to src/main/java/envoy/client/net/ReceivedMessageProcessor.java
index d408993..ba0a386 100644
--- a/src/main/java/envoy/client/ReceivedMessageProcessor.java
+++ b/src/main/java/envoy/client/net/ReceivedMessageProcessor.java
@@ -1,4 +1,4 @@
-package envoy.client;
+package envoy.client.net;
import java.util.function.Consumer;
import java.util.logging.Logger;
@@ -25,8 +25,12 @@ public class ReceivedMessageProcessor implements Consumer {
public void accept(Message message) {
logger.info("Received message object " + message);
if (message.getStatus() != MessageStatus.SENT) logger.warning("The message has the unexpected status " + message.getStatus());
- else
+ else {
+ // Update status to RECEIVED
+ message.nextStatus();
+
// Dispatch event
EventBus.getInstance().dispatch(new MessageCreationEvent(message));
+ }
}
}
diff --git a/src/main/java/envoy/client/Receiver.java b/src/main/java/envoy/client/net/Receiver.java
similarity index 99%
rename from src/main/java/envoy/client/Receiver.java
rename to src/main/java/envoy/client/net/Receiver.java
index e6cd3c5..5030241 100644
--- a/src/main/java/envoy/client/Receiver.java
+++ b/src/main/java/envoy/client/net/Receiver.java
@@ -1,4 +1,4 @@
-package envoy.client;
+package envoy.client.net;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
diff --git a/src/main/java/envoy/client/ui/ChatWindow.java b/src/main/java/envoy/client/ui/ChatWindow.java
index ab9e968..3abd54a 100644
--- a/src/main/java/envoy/client/ui/ChatWindow.java
+++ b/src/main/java/envoy/client/ui/ChatWindow.java
@@ -3,22 +3,28 @@ package envoy.client.ui;
import java.awt.*;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
+import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
-import envoy.client.*;
+import envoy.client.Settings;
+import envoy.client.data.Chat;
+import envoy.client.data.LocalDb;
import envoy.client.event.MessageCreationEvent;
import envoy.client.event.ThemeChangeEvent;
+import envoy.client.net.Client;
import envoy.client.ui.list.ComponentList;
import envoy.client.ui.settings.SettingsScreen;
import envoy.client.util.EnvoyLog;
import envoy.data.Message;
+import envoy.data.Message.MessageStatus;
import envoy.data.MessageBuilder;
import envoy.data.User;
import envoy.event.EventBus;
+import envoy.event.MessageStatusChangeEvent;
/**
* Project: envoy-client
@@ -34,7 +40,7 @@ public class ChatWindow extends JFrame {
// User specific objects
private Client client;
- private LocalDB localDB;
+ private LocalDb localDb;
// GUI components
private JPanel contentPane = new JPanel();
@@ -161,25 +167,25 @@ public class ChatWindow extends JFrame {
userList.setCellRenderer(new UserListRenderer());
userList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
userList.addListSelectionListener((listSelectionEvent) -> {
- if (client != null && localDB != null && !listSelectionEvent.getValueIsAdjusting()) {
+ if (client != null && localDb != null && !listSelectionEvent.getValueIsAdjusting()) {
@SuppressWarnings("unchecked")
final JList selectedUserList = (JList) listSelectionEvent.getSource();
final User user = selectedUserList.getSelectedValue();
// Select current chat
- currentChat = localDB.getChats().stream().filter(chat -> chat.getRecipient().getId() == user.getId()).findFirst().get();
+ currentChat = localDb.getChats().stream().filter(chat -> chat.getRecipient().getId() == user.getId()).findFirst().get();
- // Read current Chat
- currentChat.read();
+ // Read current chat
+ readCurrentChat();
- // Set recipient in client and chat title
- client.setRecipient(user);
+ // Set chat title
textPane.setText(currentChat.getRecipient().getName());
// Update model and scroll down
messageList.setModel(currentChat.getModel());
scrollPane.setChatOpened(true);
+ messageList.synchronizeModel();
revalidate();
repaint();
}
@@ -206,7 +212,35 @@ public class ChatWindow extends JFrame {
// Listen to received messages
EventBus.getInstance().register(MessageCreationEvent.class, (evt) -> {
Message message = ((MessageCreationEvent) evt).get();
- localDB.getChats().stream().filter(c -> c.getRecipient().getId() == message.getSenderId()).findFirst().get().appendMessage(message);
+ Chat chat = localDb.getChats().stream().filter(c -> c.getRecipient().getId() == message.getSenderId()).findFirst().get();
+ chat.appendMessage(message);
+
+ // Read message and update UI if in current chat
+ if (chat == currentChat) readCurrentChat();
+
+ revalidate();
+ repaint();
+ });
+
+ // Listen to message status changes
+ EventBus.getInstance().register(MessageStatusChangeEvent.class, (evt) -> {
+ final long id = ((MessageStatusChangeEvent) evt).getId();
+ final MessageStatus status = (MessageStatus) evt.get();
+
+ for (Chat c : localDb.getChats())
+ for (Message m : c.getModel())
+ if (m.getId() == id) {
+
+ // Update message status
+ m.setStatus(status);
+
+ // Update model and scroll down if current chat
+ if (c == currentChat) {
+ messageList.setModel(currentChat.getModel());
+ scrollPane.setChatOpened(true);
+ } else messageList.synchronizeModel();
+ }
+
revalidate();
repaint();
});
@@ -254,7 +288,7 @@ public class ChatWindow extends JFrame {
}
private void postMessage() {
- if (!client.hasRecipient()) {
+ if (userList.isSelectionEmpty()) {
JOptionPane.showMessageDialog(this, "Please select a recipient!", "Cannot send message", JOptionPane.INFORMATION_MESSAGE);
return;
}
@@ -262,7 +296,7 @@ public class ChatWindow extends JFrame {
if (!messageEnterTextArea.getText().isEmpty()) try {
// Create message
- final Message message = new MessageBuilder(localDB.getUser().getId(), currentChat.getRecipient().getId(), localDB.getIdGenerator())
+ final Message message = new MessageBuilder(localDb.getUser().getId(), currentChat.getRecipient().getId(), localDb.getIdGenerator())
.setText(messageEnterTextArea.getText())
.build();
@@ -270,9 +304,8 @@ public class ChatWindow extends JFrame {
// TODO: Store offline messages
client.sendMessage(message);
- // Add message to LocalDB and update UI
+ // Add message to PersistentLocalDb and update UI
currentChat.appendMessage(message);
- // messageList.setModel(currentChat.getModel());
// Clear text field
messageEnterTextArea.setText("");
@@ -281,14 +314,11 @@ public class ChatWindow extends JFrame {
revalidate();
repaint();
- // Request a new id generator if all ids were used
- if (!localDB.getIdGenerator().hasNext()) client.requestIdGenerator();
+ // Request a new id generator if all IDs were used
+ if (!localDb.getIdGenerator().hasNext()) client.requestIdGenerator();
} catch (Exception e) {
- JOptionPane.showMessageDialog(this,
- "Error sending message:\n" + e.toString(),
- "Message sending error",
- JOptionPane.ERROR_MESSAGE);
+ JOptionPane.showMessageDialog(this, "Error sending message:\n" + e.toString(), "Message sending error", JOptionPane.ERROR_MESSAGE);
e.printStackTrace();
}
}
@@ -302,36 +332,46 @@ public class ChatWindow extends JFrame {
private void loadUsersAndChats() {
new Thread(() -> {
DefaultListModel userListModel = new DefaultListModel<>();
- localDB.getUsers().values().forEach(user -> {
+ localDb.getUsers().values().forEach(user -> {
userListModel.addElement(user);
// Check if user exists in local DB
- if (localDB.getChats().stream().filter(c -> c.getRecipient().getId() == user.getId()).count() == 0)
- localDB.getChats().add(new Chat(user));
+ if (localDb.getChats().stream().filter(c -> c.getRecipient().getId() == user.getId()).count() == 0)
+ localDb.getChats().add(new Chat(user));
});
SwingUtilities.invokeLater(() -> userList.setModel(userListModel));
}).start();
}
+ private void readCurrentChat() {
+ try {
+ currentChat.read(client);
+ messageList.synchronizeModel();
+ } catch (IOException e) {
+ e.printStackTrace();
+ logger.log(Level.WARNING, "Couldn't notify server about message status change", e);
+ }
+ }
+
/**
* Sets the {@link Client} used by this {@link ChatWindow}.
*
* @param client the {@link Client} used to send and receive messages
* @since Envoy v0.2-alpha
*/
- public void setClient(Client client) {
- this.client = client;
- }
+ public void setClient(Client client) { this.client = client; }
/**
- * Sets the {@link LocalDB} used by this {@link ChatWindow}. After invoking this
+ * Sets the {@link LocalDb} used by this {@link ChatWindow}. After
+ * 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
+ * @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;
+ public void setLocalDB(LocalDb localDb) {
+ this.localDb = localDb;
loadUsersAndChats();
}
}
diff --git a/src/main/java/envoy/client/ui/Startup.java b/src/main/java/envoy/client/ui/Startup.java
index 949b464..9f308dc 100644
--- a/src/main/java/envoy/client/ui/Startup.java
+++ b/src/main/java/envoy/client/ui/Startup.java
@@ -11,7 +11,13 @@ import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.SwingUtilities;
-import envoy.client.*;
+import envoy.client.Config;
+import envoy.client.Settings;
+import envoy.client.data.LocalDb;
+import envoy.client.data.PersistentLocalDb;
+import envoy.client.data.TransientLocalDb;
+import envoy.client.net.Client;
+import envoy.client.net.MessageCache;
import envoy.client.util.EnvoyLog;
import envoy.data.LoginCredentials;
import envoy.data.User;
@@ -63,8 +69,8 @@ public class Startup {
} catch (Exception e) {
JOptionPane
.showMessageDialog(null, "Error loading configuration values:\n" + e.toString(), "Configuration error", JOptionPane.ERROR_MESSAGE);
- System.exit(1);
e.printStackTrace();
+ System.exit(1);
}
// Set new logger levels loaded from config
@@ -80,12 +86,19 @@ public class Startup {
}
// Initialize the local database
- LocalDB localDB;
- try {
- localDB = new LocalDB(new File(config.getHomeDirectory(), config.getLocalDB().getPath()));
+ LocalDb localDb;
+ if (config.isIgnoreLocalDB()) {
+ localDb = new TransientLocalDb();
+ JOptionPane.showMessageDialog(null,
+ "Ignoring local database.\nMessages will not be saved!",
+ "Local database warning",
+ JOptionPane.WARNING_MESSAGE);
+ } else try {
+ localDb = new PersistentLocalDb(new File(config.getHomeDirectory(), config.getLocalDB().getPath()));
} catch (IOException e3) {
logger.log(Level.SEVERE, "Could not initialize local database", e3);
- JOptionPane.showMessageDialog(null, "Could not initialize local database!\n" + e3.toString());
+ JOptionPane
+ .showMessageDialog(null, "Could not initialize local database!\n" + e3.toString(), "Local database error", JOptionPane.ERROR_MESSAGE);
System.exit(1);
return;
}
@@ -94,18 +107,19 @@ public class Startup {
// Acquire the client user (with ID) either from the server or from the local
// database, which triggers offline mode
- Client client = new Client();
+ Client client = new Client();
+ MessageCache cache = null;
try {
// Try entering online mode first
- localDB.loadIdGenerator();
- client.onlineInit(credentials, localDB);
+ localDb.loadIdGenerator();
+ cache = client.onlineInit(credentials, localDb);
} catch (Exception e1) {
logger.warning("Could not connect to server. Trying offline mode...");
e1.printStackTrace();
try {
// Try entering offline mode
- localDB.loadUsers();
- User clientUser = localDB.getUsers().get(credentials.getName());
+ localDb.loadUsers();
+ User clientUser = localDb.getUsers().get(credentials.getName());
if (clientUser == null) throw new EnvoyException("Could not enter offline mode: user name unknown");
client.setSender(clientUser);
JOptionPane.showMessageDialog(null,
@@ -120,12 +134,12 @@ public class Startup {
}
// Set client user in local database
- localDB.setUser(client.getSender());
+ localDb.setUser(client.getSender());
// Initialize chats in local database
try {
- localDB.initializeDBFile();
- localDB.loadChats();
+ localDb.initializeUserStorage();
+ localDb.loadChats();
} catch (FileNotFoundException e) {
// The local database file has not yet been created, probably first login
} catch (Exception e) {
@@ -137,12 +151,13 @@ public class Startup {
}
// Save all users to the local database
- if (client.isOnline()) localDB.setUsers(client.getUsers());
+ if (client.isOnline()) localDb.setUsers(client.getUsers());
+ // Display ChatWindow and StatusTrayIcon
EventQueue.invokeLater(() -> {
try {
chatWindow.setClient(client);
- chatWindow.setLocalDB(localDB);
+ chatWindow.setLocalDB(localDb);
try {
new StatusTrayIcon(chatWindow).show();
@@ -162,16 +177,19 @@ public class Startup {
}
});
- // Save Settings and LocalDB on shutdown
+ // Relay unread messages from cache
+ if (cache != null) cache.relayMessages();
+
+ // Save Settings and PersistentLocalDb on shutdown
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
try {
logger.info("Closing connection...");
client.close();
logger.info("Saving local database and settings...");
- localDB.save();
+ localDb.save();
Settings.getInstance().save();
- } catch (IOException e) {
+ } catch (Exception e) {
logger.log(Level.SEVERE, "Unable to save local files", e);
}
}));
diff --git a/src/main/java/envoy/client/ui/list/ComponentList.java b/src/main/java/envoy/client/ui/list/ComponentList.java
index 8cbf2db..f62dc7b 100644
--- a/src/main/java/envoy/client/ui/list/ComponentList.java
+++ b/src/main/java/envoy/client/ui/list/ComponentList.java
@@ -66,6 +66,18 @@ public class ComponentList extends JPanel {
synchronizeModel();
}
+ /**
+ * Removes all child components and then adds all components representing the
+ * elements of the {@link ComponentListModel}.
+ *
+ * @since Envoy v0.3-alpha
+ */
+ public void synchronizeModel() {
+ removeAll();
+ if (model != null) for (E elem : model)
+ add(elem);
+ }
+
/**
* Adds an object to the list by rendering it with the current
* {@link ComponentListCellRenderer}.
@@ -76,16 +88,4 @@ public class ComponentList extends JPanel {
void add(E elem) {
add(renderer.getListCellComponent(this, elem, false));
}
-
- /**
- * Removes all child components and then adds all components representing the
- * elements of the {@link ComponentListModel}.
- *
- * @since Envoy v0.3-alpha
- */
- void synchronizeModel() {
- removeAll();
- if (model != null) for (E elem : model)
- add(elem);
- }
}
diff --git a/src/main/java/envoy/client/ui/list/ComponentListModel.java b/src/main/java/envoy/client/ui/list/ComponentListModel.java
index 8fb1d68..fff3e6c 100644
--- a/src/main/java/envoy/client/ui/list/ComponentListModel.java
+++ b/src/main/java/envoy/client/ui/list/ComponentListModel.java
@@ -78,6 +78,12 @@ public final class ComponentListModel implements Iterable, Serializable {
*/
public int size() { return elements.size(); }
+ /**
+ * @return {@code true} if this model contains no elements
+ * @see java.util.List#isEmpty()
+ */
+ public boolean isEmpty() { return elements.isEmpty(); }
+
/**
* @return an iterator over the elements of this list model
* @see java.util.List#iterator()