Move post-related database functionality into its own class.
authorDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Wed, 26 Nov 2014 21:09:38 +0000 (22:09 +0100)
committerDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Wed, 26 Nov 2014 21:09:38 +0000 (22:09 +0100)
src/main/java/net/pterodactylus/sone/database/memory/ConfigurationLoader.java
src/main/java/net/pterodactylus/sone/database/memory/MemoryDatabase.java
src/main/java/net/pterodactylus/sone/database/memory/MemoryPostDatabase.java [new file with mode: 0644]
src/test/java/net/pterodactylus/sone/database/memory/ConfigurationLoaderTest.java

index 8974d10..e65cf48 100644 (file)
@@ -39,6 +39,10 @@ public class ConfigurationLoader {
                return loadIds("KnownPosts");
        }
 
+       public synchronized void saveKnownPosts(Set<String> knownPosts) {
+               saveIds("KnownPosts", knownPosts);
+       }
+
        public synchronized Set<String> loadKnownPostReplies() {
                return loadIds("KnownReplies");
        }
index 83a886f..813dc42 100644 (file)
@@ -110,15 +110,6 @@ public class MemoryDatabase extends AbstractService implements Database {
        private final Map<String, Sone> allSones = new HashMap<String, Sone>();
        private final Map<String, String> lastInsertFingerprints = new HashMap<String, String>();
 
-       /** All posts by their ID. */
-       private final Map<String, Post> allPosts = new HashMap<String, Post>();
-
-       /** All posts by their Sones. */
-       private final Multimap<String, Post> sonePosts = HashMultimap.create();
-
-       /** Whether posts are known. */
-       private final Set<String> knownPosts = new HashSet<String>();
-
        /** All post replies by their ID. */
        private final Map<String, PostReply> allPostReplies = new HashMap<String, PostReply>();
 
@@ -140,6 +131,7 @@ public class MemoryDatabase extends AbstractService implements Database {
        private final Map<String, Image> allImages = new HashMap<String, Image>();
        private final Multimap<String, Image> soneImages = HashMultimap.create();
 
+       private final MemoryPostDatabase postDatabase;
        private final MemoryBookmarkDatabase memoryBookmarkDatabase;
        private final MemoryFriendDatabase memoryFriendDatabase;
 
@@ -156,6 +148,7 @@ public class MemoryDatabase extends AbstractService implements Database {
                this.soneProvider = soneProvider;
                this.configuration = configuration;
                this.configurationLoader = new ConfigurationLoader(configuration);
+               postDatabase = new MemoryPostDatabase(this, configurationLoader);
                memoryBookmarkDatabase =
                                new MemoryBookmarkDatabase(this, configurationLoader);
                memoryFriendDatabase = new MemoryFriendDatabase(configurationLoader);
@@ -326,7 +319,6 @@ public class MemoryDatabase extends AbstractService implements Database {
        public void save() throws DatabaseException {
                lock.writeLock().lock();
                try {
-                       saveKnownPosts();
                        saveKnownPostReplies();
                        for (Sone localSone : from(localSones).transform(soneLoader()).transform(Optionals.<Sone>get())) {
                                saveSone(localSone);
@@ -456,8 +448,8 @@ public class MemoryDatabase extends AbstractService implements Database {
        /** {@inheritDocs} */
        @Override
        protected void doStart() {
+               postDatabase.start();
                memoryBookmarkDatabase.start();
-               loadKnownPosts();
                loadKnownPostReplies();
                notifyStarted();
        }
@@ -466,6 +458,7 @@ public class MemoryDatabase extends AbstractService implements Database {
        @Override
        protected void doStop() {
                try {
+                       postDatabase.stop();
                        memoryBookmarkDatabase.stop();
                        save();
                        notifyStopped();
@@ -496,10 +489,7 @@ public class MemoryDatabase extends AbstractService implements Database {
        }
 
        private void storePosts(String soneId, Collection<Post> posts) {
-               sonePosts.putAll(soneId, posts);
-               for (Post post : posts) {
-                       allPosts.put(post.getId(), post);
-               }
+               postDatabase.storePosts(soneId, posts);
        }
 
        private void storePostReplies(String soneId, Collection<PostReply> postReplies) {
@@ -528,10 +518,7 @@ public class MemoryDatabase extends AbstractService implements Database {
                lock.writeLock().lock();
                try {
                        allSones.remove(sone.getId());
-                       Collection<Post> removedPosts = sonePosts.removeAll(sone.getId());
-                       for (Post removedPost : removedPosts) {
-                               allPosts.remove(removedPost.getId());
-                       }
+                       postDatabase.removePostsFor(sone.getId());
                        Collection<PostReply> removedPostReplies =
                                        sonePostReplies.removeAll(sone.getId());
                        for (PostReply removedPostReply : removedPostReplies) {
@@ -642,12 +629,7 @@ public class MemoryDatabase extends AbstractService implements Database {
        /** {@inheritDocs} */
        @Override
        public Optional<Post> getPost(String postId) {
-               lock.readLock().lock();
-               try {
-                       return fromNullable(allPosts.get(postId));
-               } finally {
-                       lock.readLock().unlock();
-               }
+               return postDatabase.getPost(postId);
        }
 
        /** {@inheritDocs} */
@@ -659,17 +641,7 @@ public class MemoryDatabase extends AbstractService implements Database {
        /** {@inheritDocs} */
        @Override
        public Collection<Post> getDirectedPosts(final String recipientId) {
-               lock.readLock().lock();
-               try {
-                       return from(sonePosts.values()).filter(new Predicate<Post>() {
-                               @Override
-                               public boolean apply(Post post) {
-                                       return post.getRecipientId().asSet().contains(recipientId);
-                               }
-                       }).toSet();
-               } finally {
-                       lock.readLock().unlock();
-               }
+               return postDatabase.getDirectedPosts(recipientId);
        }
 
        //
@@ -690,27 +662,14 @@ public class MemoryDatabase extends AbstractService implements Database {
        @Override
        public void storePost(Post post) {
                checkNotNull(post, "post must not be null");
-               lock.writeLock().lock();
-               try {
-                       allPosts.put(post.getId(), post);
-                       getPostsFrom(post.getSone().getId()).add(post);
-               } finally {
-                       lock.writeLock().unlock();
-               }
+               postDatabase.storePost(post);
        }
 
        /** {@inheritDocs} */
        @Override
        public void removePost(Post post) {
                checkNotNull(post, "post must not be null");
-               lock.writeLock().lock();
-               try {
-                       allPosts.remove(post.getId());
-                       getPostsFrom(post.getSone().getId()).remove(post);
-                       post.getSone().removePost(post);
-               } finally {
-                       lock.writeLock().unlock();
-               }
+               postDatabase.removePost(post.getId());
        }
 
        //
@@ -911,12 +870,7 @@ public class MemoryDatabase extends AbstractService implements Database {
         * @return {@code true} if the post is known, {@code false} otherwise
         */
        boolean isPostKnown(Post post) {
-               lock.readLock().lock();
-               try {
-                       return knownPosts.contains(post.getId());
-               } finally {
-                       lock.readLock().unlock();
-               }
+               return postDatabase.isPostKnown(post.getId());
        }
 
        /**
@@ -928,16 +882,7 @@ public class MemoryDatabase extends AbstractService implements Database {
         *              {@code true} if the post is known, {@code false} otherwise
         */
        void setPostKnown(Post post, boolean known) {
-               lock.writeLock().lock();
-               try {
-                       if (known) {
-                               knownPosts.add(post.getId());
-                       } else {
-                               knownPosts.remove(post.getId());
-                       }
-               } finally {
-                       lock.writeLock().unlock();
-               }
+               postDatabase.setPostKnown(post.getId(), known);
        }
 
        /**
@@ -991,46 +936,7 @@ public class MemoryDatabase extends AbstractService implements Database {
         * @return All posts
         */
        private Collection<Post> getPostsFrom(String soneId) {
-               lock.readLock().lock();
-               try {
-                       return sonePosts.get(soneId);
-               } finally {
-                       lock.readLock().unlock();
-               }
-       }
-
-       /** Loads the known posts. */
-       private void loadKnownPosts() {
-               Set<String> knownPosts = configurationLoader.loadKnownPosts();
-               lock.writeLock().lock();
-               try {
-                       this.knownPosts.clear();
-                       this.knownPosts.addAll(knownPosts);
-               } finally {
-                       lock.writeLock().unlock();
-               }
-       }
-
-       /**
-        * Saves the known posts to the configuration.
-        *
-        * @throws DatabaseException
-        *              if a configuration error occurs
-        */
-       private void saveKnownPosts() throws DatabaseException {
-               lock.readLock().lock();
-               try {
-                       int postCounter = 0;
-                       for (String knownPostId : knownPosts) {
-                               configuration.getStringValue("KnownPosts/" + postCounter++ + "/ID").setValue(
-                                               knownPostId);
-                       }
-                       configuration.getStringValue("KnownPosts/" + postCounter + "/ID").setValue(null);
-               } catch (ConfigurationException ce1) {
-                       throw new DatabaseException("Could not save database.", ce1);
-               } finally {
-                       lock.readLock().unlock();
-               }
+               return postDatabase.getPostsFrom(soneId);
        }
 
        /** Loads the known post replies. */
diff --git a/src/main/java/net/pterodactylus/sone/database/memory/MemoryPostDatabase.java b/src/main/java/net/pterodactylus/sone/database/memory/MemoryPostDatabase.java
new file mode 100644 (file)
index 0000000..fb66162
--- /dev/null
@@ -0,0 +1,194 @@
+package net.pterodactylus.sone.database.memory;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+import net.pterodactylus.sone.data.Post;
+import net.pterodactylus.sone.database.DatabaseException;
+import net.pterodactylus.sone.utils.Optionals;
+
+import com.google.common.base.Function;
+import com.google.common.base.Optional;
+import com.google.common.collect.FluentIterable;
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.Multimap;
+
+/**
+ * Groups {@link Post}-related database functions.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+class MemoryPostDatabase {
+
+       private final ReadWriteLock lock = new ReentrantReadWriteLock();
+       private final MemoryDatabase database;
+       private final ConfigurationLoader configurationLoader;
+       private final Multimap<String, String> sonePosts = HashMultimap.create();
+       private final Map<String, String> postSones = new HashMap<String, String>();
+       private final Map<String, Long> postTimes = new HashMap<String, Long>();
+       private final Map<String, String> postRecipients = new HashMap<String, String>();
+       private final Multimap<String, String> recipientPosts = HashMultimap.create();
+       private final Map<String, String> postTexts = new HashMap<String, String>();
+       private final Set<String> knownPosts = new HashSet<String>();
+
+       MemoryPostDatabase(MemoryDatabase database, ConfigurationLoader configurationLoader) {
+               this.database = database;
+               this.configurationLoader = configurationLoader;
+       }
+
+       void start() {
+               loadKnownPosts();
+       }
+
+       void stop() {
+               saveKnownPosts();
+       }
+
+       Optional<Post> getPost(String postId) {
+               lock.readLock().lock();
+               try {
+                       if (!postSones.containsKey(postId)) {
+                               return Optional.absent();
+                       }
+                       String sender = postSones.get(postId);
+                       String recipientId = postRecipients.get(postId);
+                       long time = postTimes.get(postId);
+                       String text = postTexts.get(postId);
+                       return Optional.<Post>of(
+                                       new MemoryPost(database, database, postId, sender, recipientId, time, text));
+               } finally {
+                       lock.readLock().unlock();
+               }
+       }
+
+       Collection<Post> getPostsFrom(String soneId) {
+               lock.readLock().lock();
+               try {
+                       return FluentIterable.from(sonePosts.get(soneId))
+                                       .transform(postLoader)
+                                       .transform(Optionals.<Post>get())
+                                       .toList();
+               } finally {
+                       lock.readLock().unlock();
+               }
+       }
+
+       Collection<Post> getDirectedPosts(String recipientId) {
+               lock.readLock().lock();
+               try {
+                       return FluentIterable.from(recipientPosts.get(recipientId))
+                                       .transform(postLoader)
+                                       .transform(Optionals.<Post>get())
+                                       .toList();
+               } finally {
+                       lock.readLock().unlock();
+               }
+       }
+
+       void storePosts(String soneId, Collection<Post> posts) {
+               lock.writeLock().lock();
+               try {
+                       removePostsFor(soneId);
+                       for (Post post : posts) {
+                               storePost(post);
+                       }
+               } finally {
+                       lock.writeLock().unlock();
+               }
+       }
+
+       void storePost(Post post) {
+               String soneId = post.getSone().getId();
+               final String postId = post.getId();
+               sonePosts.put(soneId, postId);
+               postSones.put(postId, soneId);
+               postRecipients.put(postId, post.getRecipientId().orNull());
+               if (post.getRecipientId().isPresent()) {
+                       recipientPosts.put(post.getRecipientId().get(), postId);
+               }
+               postTimes.put(postId, post.getTime());
+               postTexts.put(postId, post.getText());
+       }
+
+       void removePostsFor(String soneId) {
+               lock.writeLock().lock();
+               try {
+                       for (String postId : sonePosts.removeAll(soneId)) {
+                               removePost(postId);
+                       }
+               } finally {
+                       lock.writeLock().unlock();
+               }
+       }
+
+       void removePost(String postId) {
+               lock.writeLock().lock();
+               try {
+                       postSones.remove(postId);
+                       String recipient = postRecipients.remove(postId);
+                       if (recipient != null) {
+                               recipientPosts.remove(recipient, postId);
+                       }
+                       postTimes.remove(postId);
+                       postTexts.remove(postId);
+               } finally {
+                       lock.writeLock().unlock();
+               }
+       }
+
+       boolean isPostKnown(String postId) {
+               lock.readLock().lock();
+               try {
+                       return knownPosts.contains(postId);
+               } finally {
+                       lock.readLock().unlock();
+               }
+       }
+
+       void setPostKnown(String postId, boolean known) {
+               lock.writeLock().lock();
+               try {
+                       if (known) {
+                               knownPosts.add(postId);
+                       } else {
+                               knownPosts.remove(postId);
+                       }
+                       saveKnownPosts();
+               } finally {
+                       lock.writeLock().unlock();
+               }
+       }
+
+       private void loadKnownPosts() {
+               Set<String> knownPosts = configurationLoader.loadKnownPosts();
+               lock.writeLock().lock();
+               try {
+                       this.knownPosts.clear();
+                       this.knownPosts.addAll(knownPosts);
+               } finally {
+                       lock.writeLock().unlock();
+               }
+       }
+
+       private void saveKnownPosts() {
+               lock.readLock().lock();
+               try {
+                       configurationLoader.saveKnownPosts(knownPosts);
+               } finally {
+                       lock.readLock().unlock();
+               }
+       }
+
+       private Function<String, Optional<Post>> postLoader = new Function<String, Optional<Post>>() {
+               @Override
+               public Optional<Post> apply(String input) {
+                       return getPost(input);
+               }
+       };
+
+}
index 47e26b7..41bc593 100644 (file)
@@ -3,11 +3,16 @@ package net.pterodactylus.sone.database.memory;
 import static java.util.Arrays.asList;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.containsInAnyOrder;
+import static org.hamcrest.Matchers.is;
 import static org.hamcrest.Matchers.nullValue;
+import static org.mockito.Matchers.anyString;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
+import java.util.HashMap;
 import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.Map;
 import java.util.Set;
 
 import net.pterodactylus.sone.TestValue;
@@ -16,6 +21,8 @@ import net.pterodactylus.util.config.ConfigurationException;
 import net.pterodactylus.util.config.Value;
 
 import org.junit.Test;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
 
 /**
  * Unit test for {@link ConfigurationLoader}.
@@ -41,6 +48,29 @@ public class ConfigurationLoaderTest {
        }
 
        @Test
+       public void loaderCanSaveKnownPosts() throws ConfigurationException {
+               Map<String, Value<String>> configurationValues = prepareConfigurationValues();
+               HashSet<String> originalPosts = new LinkedHashSet<String>(asList("Post1", "Post2"));
+               configurationLoader.saveKnownPosts(originalPosts);
+               assertThat(configurationValues.get("KnownPosts/0/ID").getValue(), is("Post1"));
+               assertThat(configurationValues.get("KnownPosts/1/ID").getValue(), is("Post2"));
+               assertThat(configurationValues.get("KnownPosts/2/ID").getValue(), nullValue());
+       }
+
+       private Map<String, Value<String>> prepareConfigurationValues() {
+               final Map<String, Value<String>> configurationValues = new HashMap<String, Value<String>>();
+               when(configuration.getStringValue(anyString())).thenAnswer(new Answer<Value<String>>() {
+                       @Override
+                       public Value<String> answer(InvocationOnMock invocation) throws Throwable {
+                               Value<String> stringValue = TestValue.from(null);
+                               configurationValues.put((String) invocation.getArguments()[0], stringValue);
+                               return stringValue;
+                       }
+               });
+               return configurationValues;
+       }
+
+       @Test
        public void loaderCanLoadKnownPostReplies() {
                when(configuration.getStringValue("KnownReplies/0/ID"))
                                .thenReturn(TestValue.from("PostReply2"));