Merge pull request #99 from informatik-ag-ngl/f/message_synchronization

Fully implemented online message synchronization
This commit is contained in:
Kai S. K. Engelbart 2020-02-05 21:00:02 +01:00 committed by GitHub
commit e68f15b184
19 changed files with 550 additions and 278 deletions

View File

@ -1,9 +1,7 @@
package envoy.client; package envoy.client;
import java.io.File; import java.io.File;
import java.util.HashMap; import java.util.*;
import java.util.Map;
import java.util.Properties;
import java.util.logging.Level; import java.util.logging.Level;
import envoy.exception.EnvoyException; import envoy.exception.EnvoyException;
@ -31,6 +29,7 @@ public class Config {
items.put("server", new ConfigItem<>("server", "s", (input) -> input, null)); items.put("server", new ConfigItem<>("server", "s", (input) -> input, null));
items.put("port", new ConfigItem<>("port", "p", (input) -> Integer.parseInt(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("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", items.put("homeDirectory",
new ConfigItem<>("homeDirectory", "h", (input) -> new File(input), new File(System.getProperty("user.home"), ".envoy"))); 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)); 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. * @return {@code true} if server, port and localDB directory are known.
* @since Envoy v0.1-alpha * @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 * @return the host name of the Envoy server
@ -119,6 +118,12 @@ public class Config {
*/ */
public File getLocalDB() { return (File) items.get("localDB").get(); } 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 * @return the directory in which all local files are saves
* @since Envoy v0.2-alpha * @since Envoy v0.2-alpha

View File

@ -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.<br>
* <br>
* Project: <strong>envoy-client</strong><br>
* File: <strong>LocalDB.java</strong><br>
* Created: <strong>27.10.2019</strong><br>
*
* @author Kai S. K. Engelbart
* @author Maximilian K&auml;fer
* @since Envoy v0.1-alpha
*/
public class LocalDB {
private File localDBDir, localDBFile, usersFile, idGeneratorFile;
private User user;
private Map<String, User> users = new HashMap<>();
private List<Chat> 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<String, User>} of all users stored locally with their
* user names as keys
* @since Envoy v0.2-alpha
*/
public Map<String, User> getUsers() { return users; }
/**
* @param users the users to set
*/
public void setUsers(Map<String, User> 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<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
* @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; }
}

View File

@ -1,11 +1,14 @@
package envoy.client; package envoy.client.data;
import java.io.IOException;
import java.io.Serializable; import java.io.Serializable;
import envoy.client.net.Client;
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;
import envoy.data.User; import envoy.data.User;
import envoy.event.MessageStatusChangeEvent;
/** /**
* Represents a chat between two {@link User}s <br> * Represents a chat between two {@link User}s <br>
@ -24,8 +27,8 @@ public class Chat implements Serializable {
private static final long serialVersionUID = -7751248474547242056L; private static final long serialVersionUID = -7751248474547242056L;
private User recipient; private final User recipient;
private ComponentListModel<Message> model = new ComponentListModel<>(); private final ComponentListModel<Message> model = new ComponentListModel<>();
/** /**
* Provides the list of messages that the recipient receives.<br> * Provides the list of messages that the recipient receives.<br>
@ -45,16 +48,37 @@ public class Chat implements Serializable {
public void appendMessage(Message message) { model.add(message); } public void appendMessage(Message message) { model.add(message); }
/** /**
* Sets the status of all chat messages to {@code READ} starting from the bottom * Sets the status of all chat messages received from the recipient to
* and stopping once a read message is found. * {@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 * @since Envoy v0.3-alpha
*/ */
public void read() { public void read(Client client) throws IOException {
for (int i = model.size() - 1; i >= 0; --i) for (int i = model.size() - 1; i >= 0; --i) {
if (model.get(i).getStatus() == MessageStatus.READ) break; final Message m = model.get(i);
else model.get(i).setStatus(MessageStatus.READ); 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 * @return all messages in the current chat

View File

@ -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.<br>
* <br>
* Project: <strong>envoy-client</strong><br>
* File: <strong>LocalDb.java</strong><br>
* Created: <strong>3 Feb 2020</strong><br>
*
* @author Kai S. K. Engelbart
* @since Envoy v0.3-alpha
*/
public abstract class LocalDb {
protected User user;
protected Map<String, User> users = new HashMap<>();
protected List<Chat> 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<String, User>} of all users stored locally with their
* user names as keys
* @since Envoy v0.2-alpha
*/
public Map<String, User> getUsers() { return users; }
/**
* @param users the users to set
*/
public void setUsers(Map<String, User> 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<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
* @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; }
}

View File

@ -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.<br>
* <br>
* Project: <strong>envoy-client</strong><br>
* File: <strong>PersistentLocalDb.java</strong><br>
* Created: <strong>27.10.2019</strong><br>
*
* @author Kai S. K. Engelbart
* @author Maximilian K&auml;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.<br>
* <br>
* 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) {}
}
}

View File

@ -0,0 +1,15 @@
package envoy.client.data;
/**
* Implements a {@link LocalDb} in a way that does not persist any information
* after application shutdown.<br>
* <br>
* Project: <strong>envoy-client</strong><br>
* File: <strong>TransientLocalDb.java</strong><br>
* Created: <strong>3 Feb 2020</strong><br>
*
* @author Kai S. K. Engelbart
* @since Envoy v0.3-alpha
*/
public class TransientLocalDb extends LocalDb {
}

View File

@ -8,9 +8,12 @@ import envoy.data.Message;
* Created: <strong>4 Dec 2019</strong><br> * Created: <strong>4 Dec 2019</strong><br>
* *
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
* @since Envoy v0.2-alpha
*/ */
public class MessageCreationEvent extends MessageEvent { public class MessageCreationEvent extends MessageEvent {
private static final long serialVersionUID = -6451021678064566774L;
/** /**
* @param message the {@link Message} that has been created * @param message the {@link Message} that has been created
*/ */

View File

@ -9,9 +9,12 @@ import envoy.event.Event;
* Created: <strong>4 Dec 2019</strong><br> * Created: <strong>4 Dec 2019</strong><br>
* *
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
* @since Envoy v0.2-alpha
*/ */
public class MessageEvent implements Event<Message> { public class MessageEvent implements Event<Message> {
private static final long serialVersionUID = 7658989461923112804L;
/** /**
* the {@link Message} attached to this {@link MessageEvent}. * the {@link Message} attached to this {@link MessageEvent}.
*/ */

View File

@ -8,9 +8,12 @@ import envoy.data.Message;
* Created: <strong>4 Dec 2019</strong><br> * Created: <strong>4 Dec 2019</strong><br>
* *
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
* @since Envoy v0.2-alpha
*/ */
public class MessageModificationEvent extends MessageEvent { public class MessageModificationEvent extends MessageEvent {
private static final long serialVersionUID = 4650039506439563116L;
/** /**
* @param message the {@link Message} that has been modified * @param message the {@link Message} that has been modified
*/ */

View File

@ -13,6 +13,7 @@ import envoy.event.Event;
*/ */
public class ThemeChangeEvent implements Event<Theme> { public class ThemeChangeEvent implements Event<Theme> {
private static final long serialVersionUID = 6756772448803774547L;
private final Theme theme; private final Theme theme;
/** /**

View File

@ -1,4 +1,4 @@
package envoy.client; package envoy.client.net;
import java.io.Closeable; import java.io.Closeable;
import java.io.IOException; import java.io.IOException;
@ -9,12 +9,19 @@ import java.util.logging.Logger;
import javax.naming.TimeLimitExceededException; import javax.naming.TimeLimitExceededException;
import envoy.client.Config;
import envoy.client.data.LocalDb;
import envoy.client.util.EnvoyLog; import envoy.client.util.EnvoyLog;
import envoy.data.*; import envoy.data.*;
import envoy.event.Event;
import envoy.event.IdGeneratorRequest; import envoy.event.IdGeneratorRequest;
import envoy.event.MessageStatusChangeEvent;
import envoy.util.SerializationUtils; import envoy.util.SerializationUtils;
/** /**
* Establishes a connection to the server, performs a handshake and delivers
* certain objects to the server.<br>
* <br>
* Project: <strong>envoy-client</strong><br> * Project: <strong>envoy-client</strong><br>
* File: <strong>Client.java</strong><br> * File: <strong>Client.java</strong><br>
* Created: <strong>28 Sep 2019</strong><br> * Created: <strong>28 Sep 2019</strong><br>
@ -26,17 +33,17 @@ import envoy.util.SerializationUtils;
*/ */
public class Client implements Closeable { public class Client implements Closeable {
// Connection handling
private Socket socket; private Socket socket;
private Receiver receiver; private Receiver receiver;
private boolean online; private boolean online;
// Asynchronously initialized during handshake
private volatile User sender; private volatile User sender;
private User recipient;
private volatile Contacts contacts; private volatile Contacts contacts;
private Config config = Config.getInstance(); // Configuration and logging
private static final Config config = Config.getInstance();
private static final Logger logger = EnvoyLog.getLogger(Client.class.getSimpleName()); private static final Logger logger = EnvoyLog.getLogger(Client.class.getSimpleName());
/** /**
@ -46,13 +53,15 @@ public class Client implements Closeable {
* an exception is thrown. * an exception is thrown.
* *
* @param credentials the login credentials of the user * @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} * {@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 * @throws Exception if the online mode could not be entered or the request
* failed for some other reason * failed for some other reason
* @since Envoy v0.2-alpha * @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 // 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());
@ -61,9 +70,13 @@ public class Client implements Closeable {
// Create message receiver // Create message receiver
receiver = new Receiver(socket.getInputStream()); 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(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(Contacts.class, contacts -> { logger.info("Acquired contacts object " + contacts); this.contacts = contacts; });
receiver.registerProcessor(Message.class, cache);
// Start receiver // Start receiver
new Thread(receiver).start(); new Thread(receiver).start();
@ -86,18 +99,27 @@ public class Client implements Closeable {
receiver.removeAllProcessors(); receiver.removeAllProcessors();
// Register processors for message and status handling // Register processors for message and status handling
receiver.registerProcessor(Message.class, new ReceivedMessageProcessor()); final ReceivedMessageProcessor receivedMessageProcessor = new ReceivedMessageProcessor();
// TODO: Status handling 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 // Process message ID generation
receiver.registerProcessor(IdGenerator.class, localDB::setIdGenerator); receiver.registerProcessor(IdGenerator.class, localDb::setIdGenerator);
// Request a generator if none is present // Request a generator if none is present or the existing one is consumed
if (!localDB.hasIdGenerator() || !localDB.getIdGenerator().hasNext()) requestIdGenerator(); 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 * @param message the message to send
* @throws IOException if the message does not reach the server * @throws IOException if the message does not reach the server
@ -108,6 +130,14 @@ public class Client implements Closeable {
message.nextStatus(); 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. * 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; } 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} * @return the {@link Receiver} used by this {@link Client}
*/ */

View File

@ -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.<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,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: <strong>envoy-client</strong><br>
* File: <strong>MessageStatusChangeEventProcessor.java</strong><br>
* Created: <strong>4 Feb 2020</strong><br>
*
* @author Kai S. K. Engelbart
* @since Envoy v0.3-alpha
*/
public class MessageStatusChangeEventProcessor implements Consumer<MessageStatusChangeEvent> {
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);
}
}
}

View File

@ -1,4 +1,4 @@
package envoy.client; package envoy.client.net;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.logging.Logger; import java.util.logging.Logger;
@ -25,8 +25,12 @@ public class ReceivedMessageProcessor implements Consumer<Message> {
public void accept(Message message) { public void accept(Message message) {
logger.info("Received message object " + message); logger.info("Received message object " + message);
if (message.getStatus() != MessageStatus.SENT) logger.warning("The message has the unexpected status " + message.getStatus()); 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 // Dispatch event
EventBus.getInstance().dispatch(new MessageCreationEvent(message)); EventBus.getInstance().dispatch(new MessageCreationEvent(message));
} }
} }
}

View File

@ -1,4 +1,4 @@
package envoy.client; package envoy.client.net;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.InputStream; import java.io.InputStream;

View File

@ -3,22 +3,28 @@ package envoy.client.ui;
import java.awt.*; import java.awt.*;
import java.awt.event.KeyAdapter; import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent; import java.awt.event.KeyEvent;
import java.io.IOException;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
import javax.swing.*; import javax.swing.*;
import javax.swing.border.EmptyBorder; 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.MessageCreationEvent;
import envoy.client.event.ThemeChangeEvent; import envoy.client.event.ThemeChangeEvent;
import envoy.client.net.Client;
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;
import envoy.data.Message; import envoy.data.Message;
import envoy.data.Message.MessageStatus;
import envoy.data.MessageBuilder; import envoy.data.MessageBuilder;
import envoy.data.User; import envoy.data.User;
import envoy.event.EventBus; import envoy.event.EventBus;
import envoy.event.MessageStatusChangeEvent;
/** /**
* Project: <strong>envoy-client</strong><br> * Project: <strong>envoy-client</strong><br>
@ -34,7 +40,7 @@ public class ChatWindow extends JFrame {
// User specific objects // User specific objects
private Client client; private Client client;
private LocalDB localDB; private LocalDb localDb;
// GUI components // GUI components
private JPanel contentPane = new JPanel(); private JPanel contentPane = new JPanel();
@ -161,25 +167,25 @@ public class ChatWindow extends JFrame {
userList.setCellRenderer(new UserListRenderer()); userList.setCellRenderer(new UserListRenderer());
userList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); userList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
userList.addListSelectionListener((listSelectionEvent) -> { userList.addListSelectionListener((listSelectionEvent) -> {
if (client != null && localDB != null && !listSelectionEvent.getValueIsAdjusting()) { if (client != null && localDb != null && !listSelectionEvent.getValueIsAdjusting()) {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
final JList<User> selectedUserList = (JList<User>) listSelectionEvent.getSource(); final JList<User> selectedUserList = (JList<User>) listSelectionEvent.getSource();
final User user = selectedUserList.getSelectedValue(); final User user = selectedUserList.getSelectedValue();
// Select current chat // 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 // Read current chat
currentChat.read(); readCurrentChat();
// Set recipient in client and chat title // Set chat title
client.setRecipient(user);
textPane.setText(currentChat.getRecipient().getName()); textPane.setText(currentChat.getRecipient().getName());
// Update model and scroll down // Update model and scroll down
messageList.setModel(currentChat.getModel()); messageList.setModel(currentChat.getModel());
scrollPane.setChatOpened(true); scrollPane.setChatOpened(true);
messageList.synchronizeModel();
revalidate(); revalidate();
repaint(); repaint();
} }
@ -206,7 +212,35 @@ 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();
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(); revalidate();
repaint(); repaint();
}); });
@ -254,7 +288,7 @@ public class ChatWindow extends JFrame {
} }
private void postMessage() { private void postMessage() {
if (!client.hasRecipient()) { if (userList.isSelectionEmpty()) {
JOptionPane.showMessageDialog(this, "Please select a recipient!", "Cannot send message", JOptionPane.INFORMATION_MESSAGE); JOptionPane.showMessageDialog(this, "Please select a recipient!", "Cannot send message", JOptionPane.INFORMATION_MESSAGE);
return; return;
} }
@ -262,7 +296,7 @@ public class ChatWindow extends JFrame {
if (!messageEnterTextArea.getText().isEmpty()) try { if (!messageEnterTextArea.getText().isEmpty()) try {
// Create message // 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()) .setText(messageEnterTextArea.getText())
.build(); .build();
@ -270,9 +304,8 @@ public class ChatWindow extends JFrame {
// TODO: Store offline messages // TODO: Store offline messages
client.sendMessage(message); client.sendMessage(message);
// Add message to LocalDB and update UI // Add message to PersistentLocalDb and update UI
currentChat.appendMessage(message); currentChat.appendMessage(message);
// messageList.setModel(currentChat.getModel());
// Clear text field // Clear text field
messageEnterTextArea.setText(""); messageEnterTextArea.setText("");
@ -281,14 +314,11 @@ public class ChatWindow extends JFrame {
revalidate(); revalidate();
repaint(); repaint();
// Request a new id generator if all ids were used // Request a new id generator if all IDs were used
if (!localDB.getIdGenerator().hasNext()) client.requestIdGenerator(); if (!localDb.getIdGenerator().hasNext()) client.requestIdGenerator();
} catch (Exception e) { } catch (Exception e) {
JOptionPane.showMessageDialog(this, JOptionPane.showMessageDialog(this, "Error sending message:\n" + e.toString(), "Message sending error", JOptionPane.ERROR_MESSAGE);
"Error sending message:\n" + e.toString(),
"Message sending error",
JOptionPane.ERROR_MESSAGE);
e.printStackTrace(); e.printStackTrace();
} }
} }
@ -302,36 +332,46 @@ public class ChatWindow extends JFrame {
private void loadUsersAndChats() { private void loadUsersAndChats() {
new Thread(() -> { new Thread(() -> {
DefaultListModel<User> userListModel = new DefaultListModel<>(); DefaultListModel<User> userListModel = new DefaultListModel<>();
localDB.getUsers().values().forEach(user -> { localDb.getUsers().values().forEach(user -> {
userListModel.addElement(user); userListModel.addElement(user);
// Check if user exists in local DB // Check if user exists in local DB
if (localDB.getChats().stream().filter(c -> c.getRecipient().getId() == user.getId()).count() == 0) if (localDb.getChats().stream().filter(c -> c.getRecipient().getId() == user.getId()).count() == 0)
localDB.getChats().add(new Chat(user)); localDb.getChats().add(new Chat(user));
}); });
SwingUtilities.invokeLater(() -> userList.setModel(userListModel)); SwingUtilities.invokeLater(() -> userList.setModel(userListModel));
}).start(); }).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}. * Sets the {@link Client} used by this {@link ChatWindow}.
* *
* @param client the {@link Client} used to send and receive messages * @param client the {@link Client} used to send and receive messages
* @since Envoy v0.2-alpha * @since Envoy v0.2-alpha
*/ */
public void setClient(Client client) { public void setClient(Client client) { this.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. * 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 * @since Envoy v0.2-alpha
*/ */
public void setLocalDB(LocalDB localDB) { public void setLocalDB(LocalDb localDb) {
this.localDB = localDB; this.localDb = localDb;
loadUsersAndChats(); loadUsersAndChats();
} }
} }

View File

@ -11,7 +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.*; 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.client.util.EnvoyLog;
import envoy.data.LoginCredentials; import envoy.data.LoginCredentials;
import envoy.data.User; import envoy.data.User;
@ -63,8 +69,8 @@ public class Startup {
} catch (Exception e) { } catch (Exception e) {
JOptionPane JOptionPane
.showMessageDialog(null, "Error loading configuration values:\n" + e.toString(), "Configuration error", JOptionPane.ERROR_MESSAGE); .showMessageDialog(null, "Error loading configuration values:\n" + e.toString(), "Configuration error", JOptionPane.ERROR_MESSAGE);
System.exit(1);
e.printStackTrace(); e.printStackTrace();
System.exit(1);
} }
// Set new logger levels loaded from config // Set new logger levels loaded from config
@ -80,12 +86,19 @@ public class Startup {
} }
// Initialize the local database // Initialize the local database
LocalDB localDB; LocalDb localDb;
try { if (config.isIgnoreLocalDB()) {
localDB = new LocalDB(new File(config.getHomeDirectory(), config.getLocalDB().getPath())); 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) { } catch (IOException e3) {
logger.log(Level.SEVERE, "Could not initialize local database", 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); System.exit(1);
return; return;
} }
@ -95,17 +108,18 @@ 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;
try { try {
// Try entering online mode first // Try entering online mode first
localDB.loadIdGenerator(); localDb.loadIdGenerator();
client.onlineInit(credentials, localDB); cache = client.onlineInit(credentials, localDb);
} catch (Exception e1) { } catch (Exception e1) {
logger.warning("Could not connect to server. Trying offline mode..."); logger.warning("Could not connect to server. Trying offline mode...");
e1.printStackTrace(); e1.printStackTrace();
try { try {
// Try entering offline mode // Try entering offline mode
localDB.loadUsers(); localDb.loadUsers();
User clientUser = localDB.getUsers().get(credentials.getName()); User clientUser = localDb.getUsers().get(credentials.getName());
if (clientUser == null) throw new EnvoyException("Could not enter offline mode: user name unknown"); if (clientUser == null) throw new EnvoyException("Could not enter offline mode: user name unknown");
client.setSender(clientUser); client.setSender(clientUser);
JOptionPane.showMessageDialog(null, JOptionPane.showMessageDialog(null,
@ -120,12 +134,12 @@ public class Startup {
} }
// Set client user in local database // Set client user in local database
localDB.setUser(client.getSender()); localDb.setUser(client.getSender());
// Initialize chats in local database // Initialize chats in local database
try { try {
localDB.initializeDBFile(); localDb.initializeUserStorage();
localDB.loadChats(); localDb.loadChats();
} 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) {
@ -137,12 +151,13 @@ public class Startup {
} }
// Save all users to the local database // 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(() -> { EventQueue.invokeLater(() -> {
try { try {
chatWindow.setClient(client); chatWindow.setClient(client);
chatWindow.setLocalDB(localDB); chatWindow.setLocalDB(localDb);
try { try {
new StatusTrayIcon(chatWindow).show(); 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(() -> { Runtime.getRuntime().addShutdownHook(new Thread(() -> {
try { try {
logger.info("Closing connection..."); logger.info("Closing connection...");
client.close(); client.close();
logger.info("Saving local database and settings..."); logger.info("Saving local database and settings...");
localDB.save(); localDb.save();
Settings.getInstance().save(); Settings.getInstance().save();
} catch (IOException e) { } catch (Exception e) {
logger.log(Level.SEVERE, "Unable to save local files", e); logger.log(Level.SEVERE, "Unable to save local files", e);
} }
})); }));

View File

@ -66,6 +66,18 @@ public class ComponentList<E> extends JPanel {
synchronizeModel(); 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 * Adds an object to the list by rendering it with the current
* {@link ComponentListCellRenderer}. * {@link ComponentListCellRenderer}.
@ -76,16 +88,4 @@ public class ComponentList<E> extends JPanel {
void add(E elem) { void add(E elem) {
add(renderer.getListCellComponent(this, elem, false)); 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);
}
} }

View File

@ -78,6 +78,12 @@ public final class ComponentListModel<E> implements Iterable<E>, Serializable {
*/ */
public int size() { return elements.size(); } 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 * @return an iterator over the elements of this list model
* @see java.util.List#iterator() * @see java.util.List#iterator()