package envoy.server.data; import java.time.Instant; import java.util.List; import java.util.logging.Level; import javax.persistence.*; import envoy.data.User.UserStatus; import envoy.server.net.ConnectionManager; import envoy.util.EnvoyLog; /** * Contains operations used for persistence. * * @author Leon Hofmeister * @author Maximilian Käfer * @since Envoy Server Standalone v0.1-alpha */ public final class PersistenceManager { private final EntityManager entityManager = Persistence.createEntityManagerFactory("envoy").createEntityManager(); private final EntityTransaction transaction = entityManager.getTransaction(); private static final PersistenceManager persistenceManager = new PersistenceManager(); /** * Creates the singleton instance of the @link{PersistenceManager}. * * @since Envoy Server Standalone v0.1-alpha */ private PersistenceManager() { Runtime.getRuntime().addShutdownHook(new Thread(() -> transaction(() -> { ConnectionManager.getInstance() .getOnlineUsers() .stream() .map(this::getUserByID) .forEach(user -> { user.setStatus(UserStatus.OFFLINE); user.setLastSeen(Instant.now()); entityManager.merge(user); }); }))); } /** * @return the {@link PersistenceManager} singleton * @since Envoy Server Standalone v0.1-alpha */ public static PersistenceManager getInstance() { return persistenceManager; } /** * Adds a {@link Contact} to the database. * * @param contact the {@link Contact} to add to the database * @since Envoy Server Standalone v0.1-alpha */ public void addContact(Contact contact) { persist(contact); } /** * Adds a {@link Message} to the database. * * @param message the {@link Message} to add to the database * @since Envoy Server Standalone v0.1-alpha */ public void addMessage(Message message) { persist(message); } /** * Adds a {@link ConfigItem} to the database. * * @param configItem the {@link ConfigItem} to add to the database * @since Envoy Server Standalone v0.1-alpha */ public void addConfigItem(ConfigItem configItem) { persist(configItem); } /** * Updates a {@link Contact} in the database * * @param contact the {@link Contact} to add to the database * @since Envoy Server Standalone v0.1-alpha */ public void updateContact(Contact contact) { merge(contact); } /** * Updates a {@link Message} in the database. * * @param message the message to update * @since Envoy Server Standalone v0.1-alpha */ public void updateMessage(Message message) { merge(message); } /** * Updates a {@link ConfigItem} in the database. * * @param configItem the configItem to update * @since Envoy Server Standalone v0.1-alpha */ public void updateConfigItem(ConfigItem configItem) { merge(configItem); } /** * Deletes a {@link Contact} in the database. * * @param contact the {@link Contact} to delete * @since Envoy Server Standalone v0.1-alpha */ public void deleteContact(Contact contact) { transaction(() -> { // Remove this contact from the contact list of his contacts for (final var remainingContact : contact.getContacts()) remainingContact.getContacts().remove(contact); }); remove(contact); } /** * Deletes a {@link Message} in the database. * * @param message the {@link Message} to delete * @since Envoy Server Standalone v0.1-alpha */ public void deleteMessage(Message message) { remove(message); } /** * Searches for a {@link User} with a specific ID. * * @param id the id to search for * @return the user with the specified ID or {@code null} if none was found * @since Envoy Server Standalone v0.1-alpha */ public User getUserByID(long id) { return entityManager.find(User.class, id); } /** * Searches for a {@link Group} with a specific ID. * * @param id the id to search for * @return the group with the specified ID or {@code null} if none was found * @since Envoy Server Standalone v0.1-beta */ public Group getGroupByID(long id) { return entityManager.find(Group.class, id); } /** * Searches for a {@link Contact} with a specific ID. * * @param id the id to search for * @return the contact with the specified ID or {@code null} if none was found * @since Envoy Server Standalone v0.1-beta */ public Contact getContactByID(long id) { return entityManager.find(Contact.class, id); } /** * Searched for a {@link User} with a specific name. * * @param name the name of the user * @return the user with the specified name * @since Envoy Server Standalone v0.1-alpha */ public User getUserByName(String name) { return (User) entityManager.createNamedQuery(User.findByName).setParameter("name", name).getSingleResult(); } /** * Searched for a {@link Group} with a specific name. * * @param name the name of the group * @return the group with the specified name * @since Envoy Server Standalone v0.1-alpha */ public Group getGroupByName(String name) { return (Group) entityManager.createNamedQuery(Group.findByName).setParameter("name", name).getSingleResult(); } /** * Searches for a {@link Message} with a specific id. * * @param id the id to search for * @return the message with the specified ID or {@code null} if none is found * @since Envoy Server Standalone v0.1-alpha */ public Message getMessageByID(long id) { return entityManager.find(Message.class, id); } /** * @param key the name of this {@link ConfigItem} * @return the {@link ConfigItem} with the given name * @since Envoy Server Standalone v0.1-alpha */ public ConfigItem getConfigItemByID(String key) { return entityManager.find(ConfigItem.class, key); } /** * Returns all messages received while being offline or the ones that have * changed. * * @param user the user who wants to receive his unread messages * @param lastSync the time stamp of the last synchronization * @return all messages that the client does not yet have (unread messages) * @since Envoy Server Standalone v0.2-beta */ public List getPendingMessages(User user, Instant lastSync) { return entityManager.createNamedQuery(Message.getPending).setParameter("user", user).setParameter("lastSeen", lastSync).getResultList(); } /** * Returns all groupMessages received while being offline or the ones that have * changed. * * @param user the user who wants to receive his unread groupMessages * @param lastSync the time stamp of the last synchronization * @return all groupMessages that the client does not yet have (unread * groupMessages) * @since Envoy Server Standalone v0.2-beta */ public List getPendingGroupMessages(User user, Instant lastSync) { return entityManager.createNamedQuery(GroupMessage.getPendingGroupMsg) .setParameter("userId", user.getID()) .setParameter("lastSeen", lastSync) .getResultList(); } /** * Searches for users matching a search phrase. Contacts of the attached user * and the attached user is ignored. * * @param searchPhrase the search phrase * @param userId the ID of the user in whose context the search is * performed * @return a list of all users who matched the criteria * @since Envoy Server Standalone v0.1-alpha */ public List searchUsers(String searchPhrase, long userId) { return entityManager.createNamedQuery(User.searchByName) .setParameter("searchPhrase", searchPhrase + "%") .setParameter("context", getUserByID(userId)) .getResultList(); } /** * Adds a contact to the contact list of another contact and vice versa. * * @param contactID1 the ID of the first contact * @param contactID2 the ID of the second contact * @since Envoy Server Standalone v0.1-alpha */ public void addContactBidirectional(long contactID1, long contactID2) { addContactBidirectional(getContactByID(contactID1), getContactByID(contactID2)); } /** * Adds a contact to the contact list of another contact and vice versa. * * @param contact1 the first contact * @param contact2 the second contact * @since Envoy Server v0.3-beta */ public void addContactBidirectional(Contact contact1, Contact contact2) { // Add users to each others contact list contact1.getContacts().add(contact2); contact2.getContacts().add(contact1); // Synchronize changes with the database transaction(() -> { entityManager.merge(contact1); entityManager.merge(contact2); }); } /** * Removes a contact from the contact list of another contact and vice versa. * * @param contactID1 the ID of the first contact * @param contactID2 the ID of the second contact * @since Envoy Server v0.3-beta */ public void removeContactBidirectional(long contactID1, long contactID2) { removeContactBidirectional(getContactByID(contactID1), getContactByID(contactID2)); } /** * Removes a contact from the contact list of another contact and vice versa. * * @param contact1 the first contact * @param contact2 the second contact * @since Envoy Server v0.3-beta */ public void removeContactBidirectional(Contact contact1, Contact contact2) { // Remove users from each others contact list contact1.getContacts().remove(contact2); contact2.getContacts().remove(contact1); // Synchronize changes with the database transaction(() -> { entityManager.merge(contact1); entityManager.merge(contact2); }); } /** * @param user the User whose contacts should be retrieved * @return the contacts of this User * @since Envoy Server Standalone v0.1-alpha */ public List getContacts(User user) { return entityManager.createNamedQuery(User.findContacts).setParameter("user", user).getResultList(); } private void persist(Object obj) { transaction(() -> entityManager.persist(obj)); } private void merge(Object obj) { transaction(() -> entityManager.merge(obj)); } private void remove(Object obj) { transaction(() -> entityManager.remove(obj)); } /** * Performs a transaction with the given Runnable, that should somewhere call * {@link EntityManager}. * * @param entityManagerRelatedAction the action that changes something in the * database * @since Envoy Server v0.3-beta */ private void transaction(Runnable entityManagerRelatedAction) { try { transaction.begin(); entityManagerRelatedAction.run(); transaction.commit(); // Last transaction threw an error resulting in the transaction not being closed } catch (final IllegalStateException e) { if (transaction.isActive()) { transaction.rollback(); transaction.begin(); entityManagerRelatedAction.run(); transaction.commit(); } } catch (final RollbackException e2) { // Apparently a major problem exists. Discard faulty transaction and then go on. if (transaction.isActive()) { transaction.rollback(); EnvoyLog.getLogger(PersistenceManager.class) .log(Level.SEVERE, "Could not perform transaction, hence discarding it. It's likely that a serious issue exists."); } else throw e2; } } }