package envoy.client; import java.io.*; import java.time.Instant; import java.util.*; import java.util.logging.Logger; import javax.xml.datatype.DatatypeConfigurationException; import javax.xml.datatype.DatatypeFactory; import envoy.client.event.EventBus; import envoy.client.event.MessageCreationEvent; import envoy.client.util.EnvoyLog; import envoy.exception.EnvoyException; import envoy.schema.*; import envoy.schema.Message.Metadata.MessageState; /** * 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; private User user; private Map users = new HashMap<>(); private List chats = new ArrayList<>(); private ObjectFactory objectFactory = new ObjectFactory(); private DatatypeFactory datatypeFactory; private Sync unreadMessagesSync = objectFactory.createSync(); private Sync sync = objectFactory.createSync(); private Sync readMessages = objectFactory.createSync(); private static final Logger logger = EnvoyLog.getLogger(LocalDB.class.getSimpleName()); /** * 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; try { datatypeFactory = DatatypeFactory.newInstance(); } catch (DatatypeConfigurationException e) { e.printStackTrace(); } // 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"); } /** * 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. * * @throws IOException if something went wrong during saving * @since Envoy v0.1-alpha */ public void save() throws IOException { // Save users write(usersFile, users); // Save chats write(localDBFile, chats); } /** * Loads all users that are stored in the local database. * * @throws EnvoyException if the loading process failed * @since Envoy v0.2-alpha */ @SuppressWarnings("unchecked") public void loadUsers() throws EnvoyException { users = read(usersFile, HashMap.class); } /** * Loads all chats saved by Envoy for the client user. * * @throws EnvoyException if the loading process failed * @since Envoy v0.1-alpha */ @SuppressWarnings("unchecked") public void loadChats() throws EnvoyException { chats = read(localDBFile, ArrayList.class); } private T read(File file, Class serializedClass) throws EnvoyException { if (file == null) throw new NullPointerException("File is null"); try (ObjectInputStream in = new ObjectInputStream(new FileInputStream(file))) { return serializedClass.cast(in.readObject()); } catch (ClassNotFoundException | IOException e) { throw new EnvoyException("Could not load serialized object", e); } } private void write(File file, T obj) throws IOException { if (file == null) throw new NullPointerException("File is null"); if (obj == null) throw new NullPointerException("Object to serialize is null"); if (!file.exists()) { file.getParentFile().mkdirs(); file.createNewFile(); } try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(file))) { out.writeObject(obj); } catch (IOException e) { throw e; } } /** * Creates a {@link Message} object serializable to XML. * * @param textContent The content (text) of the message * @param recipientID The recipient of the message * @return prepared {@link Message} object * @since Envoy v0.1-alpha */ public Message createMessage(String textContent, long recipientID) { Message.Metadata metaData = objectFactory.createMessageMetadata(); metaData.setSender(user.getID()); metaData.setRecipient(recipientID); metaData.setState(MessageState.WAITING); metaData.setDate(datatypeFactory.newXMLGregorianCalendar(Instant.now().toString())); Message.Content content = objectFactory.createMessageContent(); content.setType("text"); content.setText(textContent); Message message = objectFactory.createMessage(); message.setMetadata(metaData); message.getContent().add(content); return message; } /** * Creates a {@link Sync} object filled with the changes that occurred to the * local database since the last synchronization. * * @param userId the ID of the user that is synchronized by this client * @return {@link Sync} object filled with the current changes * @since Envoy v0.1-alpha */ public Sync fillSync(long userId) { addWaitingMessagesToSync(); sync.getMessages().addAll(readMessages.getMessages()); readMessages.getMessages().clear(); logger.finest(String.format("Filled sync with %d messages.", sync.getMessages().size())); return sync; } /** * Applies the changes carried by a {@link Sync} object to the local database * * @param returnSync the {@link Sync} object to apply * @since Envoy v0.1-alpha */ public void applySync(Sync returnSync) { for (int i = 0; i < returnSync.getMessages().size(); i++) { // The message has an ID if (returnSync.getMessages().get(i).getMetadata().getMessageId() != 0) { // Messages are processes differently corresponding to their state switch (returnSync.getMessages().get(i).getMetadata().getState()) { case SENT: // Update previously waiting and now sent messages that were assigned an ID by // the server sync.getMessages().get(i).getMetadata().setMessageId(returnSync.getMessages().get(i).getMetadata().getMessageId()); sync.getMessages().get(i).getMetadata().setState(returnSync.getMessages().get(i).getMetadata().getState()); break; case RECEIVED: if (returnSync.getMessages().get(i).getMetadata().getSender() != 0) { // these are the unread Messages from the server unreadMessagesSync.getMessages().add(returnSync.getMessages().get(i)); // Create and dispatch message creation event EventBus.getInstance().dispatch(new MessageCreationEvent(returnSync.getMessages().get(i))); } else { // Update Messages in localDB to state RECEIVED for (Chat chat : getChats()) if (chat.getRecipient().getID() == returnSync.getMessages().get(i).getMetadata().getRecipient()) for (int j = 0; j < chat.getModel().getSize(); j++) if (chat.getModel().get(j).getMetadata().getMessageId() == returnSync.getMessages() .get(i) .getMetadata() .getMessageId()) chat.getModel().get(j).getMetadata().setState(returnSync.getMessages().get(i).getMetadata().getState()); } break; case READ: // Update local Messages to state READ logger.info("Message with ID: " + returnSync.getMessages().get(i).getMetadata().getMessageId() + "was initialized to be set to READ in localDB."); for (Chat chat : getChats()) if (chat.getRecipient().getID() == returnSync.getMessages().get(i).getMetadata().getRecipient()) { logger.info("Chat with: " + chat.getRecipient().getID() + "was selected."); for (int k = 0; k < chat.getModel().getSize(); k++) if (chat.getModel().get(k).getMetadata().getMessageId() == returnSync.getMessages() .get(i) .getMetadata() .getMessageId()) { logger.info("Message with ID: " + chat.getModel().get(k).getMetadata().getMessageId() + "was selected."); chat.getModel().get(k).getMetadata().setState(returnSync.getMessages().get(i).getMetadata().getState()); logger.info("Message State is now: " + chat.getModel().get(k).getMetadata().getState()); } } break; } } } // Updating UserStatus of all users in LocalDB for (User user : returnSync.getUsers()) for (Chat chat : getChats()) if (user.getID() == chat.getRecipient().getID()) chat.getRecipient().setStatus(user.getStatus()); sync.getMessages().clear(); sync.getUsers().clear(); } /** * Adds the unread messages returned from the server in the latest sync to the * right chats in the LocalDB. * * @since Envoy v0.1-alpha */ public void addUnreadMessagesToLocalDB() { for (Message message : unreadMessagesSync.getMessages()) for (Chat chat : getChats()) if (message.getMetadata().getSender() == chat.getRecipient().getID()) { chat.appendMessage(message); break; } } /** * Changes all messages with state {@code RECEIVED} of a specific chat to state * {@code READ}. *
* Adds these messages to the {@code readMessages} {@link Sync} object. * * @param currentChat the {@link Chat} that was just opened * @since Envoy v0.1-alpha */ public void setMessagesToRead(Chat currentChat) { for (int i = currentChat.getModel().size() - 1; i >= 0; --i) if (currentChat.getModel().get(i).getMetadata().getRecipient() != currentChat.getRecipient().getID()) if (currentChat.getModel().get(i).getMetadata().getState() == MessageState.RECEIVED) { currentChat.getModel().get(i).getMetadata().setState(MessageState.READ); readMessages.getMessages().add(currentChat.getModel().get(i)); } else break; } /** * Adds all messages with state {@code WAITING} from the {@link LocalDB} to the * {@link Sync} object. * * @since Envoy v0.1-alpha */ private void addWaitingMessagesToSync() { for (Chat chat : getChats()) for (int i = 0; i < chat.getModel().size(); i++) if (chat.getModel().get(i).getMetadata().getState() == MessageState.WAITING) { logger.info("Got Waiting Message"); sync.getMessages().add(chat.getModel().get(i)); } } /** * Clears the {@code unreadMessagesSync} {@link Sync} object. * * @since Envoy v0.1-alpha */ public void clearUnreadMessagesSync() { unreadMessagesSync.getMessages().clear(); } /** * @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; } }