Move friend-related functionality into the database.
authorDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Tue, 25 Nov 2014 20:26:22 +0000 (21:26 +0100)
committerDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Tue, 25 Nov 2014 20:39:46 +0000 (21:39 +0100)
src/main/java/net/pterodactylus/sone/core/Core.java
src/main/java/net/pterodactylus/sone/data/Sone.java
src/main/java/net/pterodactylus/sone/data/impl/IdOnlySone.java
src/main/java/net/pterodactylus/sone/database/Database.java
src/main/java/net/pterodactylus/sone/database/FriendDatabase.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/database/FriendProvider.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/database/FriendStore.java [new file with mode: 0644]
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/MemoryFriendDatabase.java [new file with mode: 0644]
src/test/java/net/pterodactylus/sone/database/memory/MemoryDatabaseTest.java

index dfe8074..17905f3 100644 (file)
@@ -726,7 +726,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
        public void followSone(Sone sone, String soneId) {
                checkNotNull(sone, "sone must not be null");
                checkNotNull(soneId, "soneId must not be null");
-               sone.addFriend(soneId);
+               database.addFriend(sone, soneId);
                synchronized (soneFollowingTimes) {
                        if (!soneFollowingTimes.containsKey(soneId)) {
                                long now = System.currentTimeMillis();
@@ -761,7 +761,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
        public void unfollowSone(Sone sone, String soneId) {
                checkNotNull(sone, "sone must not be null");
                checkNotNull(soneId, "soneId must not be null");
-               sone.removeFriend(soneId);
+               database.removeFriend(sone, soneId);
                boolean unfollowedSoneStillFollowed = false;
                for (Sone localSone : getLocalSones()) {
                        unfollowedSoneStillFollowed |= localSone.hasFriend(soneId);
@@ -1027,9 +1027,6 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                Set<String> likedReplyIds =
                                configurationSoneParser.parseLikedPostReplyIds();
 
-               /* load friends. */
-               Set<String> friends = configurationSoneParser.parseFriends();
-
                /* load albums. */
                List<Album> topLevelAlbums;
                try {
@@ -1081,9 +1078,6 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                        sone.setReplies(replies);
                        sone.setLikePostIds(likedPostIds);
                        sone.setLikeReplyIds(likedReplyIds);
-                       for (String friendId : friends) {
-                               followSone(sone, friendId);
-                       }
                        for (Album album : sone.getRootAlbum().getAlbums()) {
                                sone.getRootAlbum().removeAlbum(album);
                        }
@@ -1095,11 +1089,6 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                                soneInserters.get(sone).setLastInsertFingerprint(lastInsertFingerprint);
                        }
                }
-               synchronized (knownSones) {
-                       for (String friend : friends) {
-                               knownSones.add(friend);
-                       }
-               }
                for (Post post : posts) {
                        post.setKnown(true);
                }
@@ -1513,13 +1502,6 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                        }
                        configuration.getStringValue(sonePrefix + "/Likes/Reply/" + replyLikeCounter + "/ID").setValue(null);
 
-                       /* save friends. */
-                       int friendCounter = 0;
-                       for (String friendId : sone.getFriends()) {
-                               configuration.getStringValue(sonePrefix + "/Friends/" + friendCounter++ + "/ID").setValue(friendId);
-                       }
-                       configuration.getStringValue(sonePrefix + "/Friends/" + friendCounter + "/ID").setValue(null);
-
                        /* save albums. first, collect in a flat structure, top-level first. */
                        List<Album> albums = FluentIterable.from(sone.getRootAlbum().getAlbums()).transformAndConcat(Album.FLATTENER).toList();
 
index b23e48e..27f2cb1 100644 (file)
@@ -354,24 +354,6 @@ public interface Sone extends Identified, Fingerprintable, Comparable<Sone> {
        boolean hasFriend(String friendSoneId);
 
        /**
-        * Adds the given Sone as a friend Sone.
-        *
-        * @param friendSone
-        *              The friend Sone to add
-        * @return This Sone (for method chaining)
-        */
-       Sone addFriend(String friendSone);
-
-       /**
-        * Removes the given Sone as a friend Sone.
-        *
-        * @param friendSoneId
-        *              The ID of the friend Sone to remove
-        * @return This Sone (for method chaining)
-        */
-       Sone removeFriend(String friendSoneId);
-
-       /**
         * Returns the list of posts of this Sone, sorted by time, newest first.
         *
         * @return All posts of this Sone
index fa7aace..0ef220b 100644 (file)
@@ -126,16 +126,6 @@ public class IdOnlySone implements Sone {
        }
 
        @Override
-       public Sone addFriend(String friendSone) {
-               return this;
-       }
-
-       @Override
-       public Sone removeFriend(String friendSoneId) {
-               return this;
-       }
-
-       @Override
        public List<Post> getPosts() {
                return emptyList();
        }
index 2d80f7f..971a427 100644 (file)
@@ -29,7 +29,7 @@ import com.google.inject.ImplementedBy;
  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
  */
 @ImplementedBy(MemoryDatabase.class)
-public interface Database extends Service, SoneDatabase, PostDatabase, PostReplyDatabase, AlbumDatabase, ImageDatabase, BookmarkDatabase {
+public interface Database extends Service, SoneDatabase, FriendDatabase, PostDatabase, PostReplyDatabase, AlbumDatabase, ImageDatabase, BookmarkDatabase {
 
        /**
         * Saves the database.
diff --git a/src/main/java/net/pterodactylus/sone/database/FriendDatabase.java b/src/main/java/net/pterodactylus/sone/database/FriendDatabase.java
new file mode 100644 (file)
index 0000000..761d356
--- /dev/null
@@ -0,0 +1,10 @@
+package net.pterodactylus.sone.database;
+
+/**
+ * Combines a {@link FriendProvider} and a {@link FriendStore} into a friend database.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public interface FriendDatabase extends FriendProvider, FriendStore {
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/database/FriendProvider.java b/src/main/java/net/pterodactylus/sone/database/FriendProvider.java
new file mode 100644 (file)
index 0000000..3665d1b
--- /dev/null
@@ -0,0 +1,17 @@
+package net.pterodactylus.sone.database;
+
+import java.util.Collection;
+
+import net.pterodactylus.sone.data.Sone;
+
+/**
+ * Provides information about {@link Sone#getFriends() friends} of a {@link Sone}.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public interface FriendProvider {
+
+       Collection<String> getFriends(Sone localSone);
+       boolean isFriend(Sone localSone, String friendSoneId);
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/database/FriendStore.java b/src/main/java/net/pterodactylus/sone/database/FriendStore.java
new file mode 100644 (file)
index 0000000..38c1c80
--- /dev/null
@@ -0,0 +1,15 @@
+package net.pterodactylus.sone.database;
+
+import net.pterodactylus.sone.data.Sone;
+
+/**
+ * Stores information about the {@link Sone#getFriends() friends} of a {@link Sone}.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public interface FriendStore {
+
+       void addFriend(Sone localSone, String friendSoneId);
+       void removeFriend(Sone localSone, String friendSoneId);
+
+}
index 84d8198..1691ddb 100644 (file)
@@ -2,6 +2,7 @@ package net.pterodactylus.sone.database.memory;
 
 import static java.util.logging.Level.WARNING;
 
+import java.util.Collection;
 import java.util.HashSet;
 import java.util.Set;
 import java.util.logging.Logger;
@@ -24,6 +25,14 @@ public class ConfigurationLoader {
                this.configuration = configuration;
        }
 
+       public synchronized Set<String> loadFriends(String localSoneId) {
+               return loadIds("Sone/" + localSoneId + "/Friends");
+       }
+
+       public void saveFriends(String soneId, Collection<String> friends) {
+               saveIds("Sone/" + soneId + "/Friends", friends);
+       }
+
        public synchronized Set<String> loadKnownPosts() {
                return loadIds("KnownPosts");
        }
@@ -56,10 +65,10 @@ public class ConfigurationLoader {
                saveIds("Bookmarks/Post", bookmarkedPosts);
        }
 
-       private void saveIds(String prefix, Set<String> bookmarkedPosts) {
+       private void saveIds(String prefix, Collection<String> ids) {
                try {
                        int idCounter = 0;
-                       for (String id : bookmarkedPosts) {
+                       for (String id : ids) {
                                configuration
                                                .getStringValue(prefix + "/" + idCounter++ + "/ID")
                                                .setValue(id);
index 4ee375e..127b90c 100644 (file)
@@ -21,13 +21,13 @@ import static com.google.common.base.Optional.fromNullable;
 import static com.google.common.base.Preconditions.checkNotNull;
 import static com.google.common.base.Predicates.not;
 import static com.google.common.collect.FluentIterable.from;
-import static java.util.Collections.unmodifiableCollection;
 import static net.pterodactylus.sone.data.Reply.TIME_COMPARATOR;
 import static net.pterodactylus.sone.data.Sone.LOCAL_SONE_FILTER;
 import static net.pterodactylus.sone.data.Sone.toAllAlbums;
 import static net.pterodactylus.sone.data.Sone.toAllImages;
 
 import java.util.Collection;
+import java.util.Collections;
 import java.util.Comparator;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -118,6 +118,7 @@ public class MemoryDatabase extends AbstractService implements Database {
        private final Multimap<String, Image> soneImages = HashMultimap.create();
 
        private final MemoryBookmarkDatabase memoryBookmarkDatabase;
+       private final MemoryFriendDatabase memoryFriendDatabase;
 
        /**
         * Creates a new memory database.
@@ -134,6 +135,7 @@ public class MemoryDatabase extends AbstractService implements Database {
                this.configurationLoader = new ConfigurationLoader(configuration);
                memoryBookmarkDatabase =
                                new MemoryBookmarkDatabase(this, configurationLoader);
+               memoryFriendDatabase = new MemoryFriendDatabase(configurationLoader);
        }
 
        //
@@ -290,6 +292,38 @@ public class MemoryDatabase extends AbstractService implements Database {
                }
        }
 
+       @Override
+       public Collection<String> getFriends(Sone localSone) {
+               if (!localSone.isLocal()) {
+                       return Collections.emptySet();
+               }
+               return memoryFriendDatabase.getFriends(localSone.getId());
+       }
+
+       @Override
+       public boolean isFriend(Sone localSone, String friendSoneId) {
+               if (!localSone.isLocal()) {
+                       return false;
+               }
+               return memoryFriendDatabase.isFriend(localSone.getId(), friendSoneId);
+       }
+
+       @Override
+       public void addFriend(Sone localSone, String friendSoneId) {
+               if (!localSone.isLocal()) {
+                       return;
+               }
+               memoryFriendDatabase.addFriend(localSone.getId(), friendSoneId);
+       }
+
+       @Override
+       public void removeFriend(Sone localSone, String friendSoneId) {
+               if (!localSone.isLocal()) {
+                       return;
+               }
+               memoryFriendDatabase.removeFriend(localSone.getId(), friendSoneId);
+       }
+
        //
        // POSTPROVIDER METHODS
        //
diff --git a/src/main/java/net/pterodactylus/sone/database/memory/MemoryFriendDatabase.java b/src/main/java/net/pterodactylus/sone/database/memory/MemoryFriendDatabase.java
new file mode 100644 (file)
index 0000000..1802843
--- /dev/null
@@ -0,0 +1,79 @@
+package net.pterodactylus.sone.database.memory;
+
+import java.util.Collection;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.Multimap;
+
+/**
+ * In-memory implementation of friend-related functionality.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+class MemoryFriendDatabase {
+
+       private final ReadWriteLock lock = new ReentrantReadWriteLock();
+       private final ConfigurationLoader configurationLoader;
+       private final Multimap<String, String> soneFriends = HashMultimap.create();
+
+       MemoryFriendDatabase(ConfigurationLoader configurationLoader) {
+               this.configurationLoader = configurationLoader;
+       }
+
+       Collection<String> getFriends(String localSoneId) {
+               loadFriends(localSoneId);
+               lock.readLock().lock();
+               try {
+                       return soneFriends.get(localSoneId);
+               } finally {
+                       lock.readLock().unlock();
+               }
+       }
+
+       boolean isFriend(String localSoneId, String friendSoneId) {
+               loadFriends(localSoneId);
+               lock.readLock().lock();
+               try {
+                       return soneFriends.containsEntry(localSoneId, friendSoneId);
+               } finally {
+                       lock.readLock().unlock();
+               }
+       }
+
+       void addFriend(String localSoneId, String friendSoneId) {
+               lock.writeLock().lock();
+               try {
+                       if (soneFriends.put(localSoneId, friendSoneId)) {
+                               configurationLoader.saveFriends(localSoneId, soneFriends.get(localSoneId));
+                       }
+               } finally {
+                       lock.writeLock().unlock();
+               }
+       }
+
+       void removeFriend(String localSoneId, String friendSoneId) {
+               lock.writeLock().lock();
+               try {
+                       if (soneFriends.remove(localSoneId, friendSoneId)) {
+                               configurationLoader.saveFriends(localSoneId, soneFriends.get(localSoneId));
+                       }
+               } finally {
+                       lock.writeLock().unlock();
+               }
+       }
+
+       private void loadFriends(String localSoneId) {
+               lock.writeLock().lock();
+               try {
+                       if (soneFriends.containsKey(localSoneId)) {
+                               return;
+                       }
+                       soneFriends.putAll(localSoneId, configurationLoader.loadFriends(localSoneId));
+               } finally {
+                       lock.writeLock().unlock();
+               }
+       }
+
+}
index c5cd9f3..f7b07a2 100644 (file)
@@ -27,28 +27,42 @@ import static net.pterodactylus.sone.Matchers.isPostReply;
 import static org.hamcrest.CoreMatchers.is;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.contains;
+import static org.hamcrest.Matchers.containsInAnyOrder;
 import static org.hamcrest.Matchers.empty;
+import static org.hamcrest.Matchers.emptyIterable;
+import static org.mockito.Matchers.anyString;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import java.util.Collection;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 
 import net.pterodactylus.sone.TestAlbumBuilder;
 import net.pterodactylus.sone.TestImageBuilder;
 import net.pterodactylus.sone.TestPostBuilder;
 import net.pterodactylus.sone.TestPostReplyBuilder;
+import net.pterodactylus.sone.TestValue;
 import net.pterodactylus.sone.data.Album;
 import net.pterodactylus.sone.data.impl.AlbumImpl;
 import net.pterodactylus.sone.data.Image;
 import net.pterodactylus.sone.data.Post;
 import net.pterodactylus.sone.data.PostReply;
 import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.util.config.Configuration;
+import net.pterodactylus.util.config.Value;
 
 import com.google.common.base.Optional;
 import org.junit.Before;
 import org.junit.Test;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
 
 /**
  * Tests for {@link MemoryDatabase}.
@@ -59,7 +73,8 @@ public class MemoryDatabaseTest {
 
        private static final String SONE_ID = "sone";
        private static final String RECIPIENT_ID = "recipient";
-       private final MemoryDatabase memoryDatabase = new MemoryDatabase(null, null);
+       private final Configuration configuration = mock(Configuration.class);
+       private final MemoryDatabase memoryDatabase = new MemoryDatabase(null, configuration);
        private final Sone sone = mock(Sone.class);
 
        @Before
@@ -261,4 +276,113 @@ public class MemoryDatabaseTest {
                assertThat(memoryDatabase.getAlbum(newAlbum.getId()), is(Optional.<Album>absent()));
        }
 
+       private void initializeFriends() {
+               when(configuration.getStringValue("Sone/" + SONE_ID + "/Friends/0/ID")).thenReturn(
+                               new TestValue<String>("Friend1"));
+               when(configuration.getStringValue("Sone/" + SONE_ID + "/Friends/1/ID")).thenReturn(
+                               new TestValue<String>("Friend2"));
+               when(configuration.getStringValue("Sone/" + SONE_ID + "/Friends/2/ID")).thenReturn(
+                               new TestValue<String>(null));
+       }
+
+       @Test
+       public void friendsAreReturnedCorrectly() {
+               initializeFriends();
+               when(sone.isLocal()).thenReturn(true);
+               Collection<String> friends = memoryDatabase.getFriends(sone);
+               assertThat(friends, containsInAnyOrder("Friend1", "Friend2"));
+       }
+
+       @Test
+       public void friendsAreOnlyLoadedOnceFromConfiguration() {
+               friendsAreReturnedCorrectly();
+               memoryDatabase.getFriends(sone);
+               verify(configuration).getStringValue("Sone/" + SONE_ID + "/Friends/0/ID");
+       }
+
+       @Test
+       public void friendsAreOnlyReturnedForLocalSones() {
+               Collection<String> friends = memoryDatabase.getFriends(sone);
+               assertThat(friends, emptyIterable());
+               verify(configuration, never()).getStringValue("Sone/" + SONE_ID + "/Friends/0/ID");
+       }
+
+       @Test
+       public void checkingForAFriendReturnsTrue() {
+               initializeFriends();
+               when(sone.isLocal()).thenReturn(true);
+               assertThat(memoryDatabase.isFriend(sone, "Friend1"), is(true));
+       }
+
+       @Test
+       public void checkingForAFriendThatIsNotAFriendReturnsFalse() {
+               initializeFriends();
+               when(sone.isLocal()).thenReturn(true);
+               assertThat(memoryDatabase.isFriend(sone, "FriendX"), is(false));
+       }
+
+       @Test
+       public void checkingForAFriendOfRemoteSoneReturnsFalse() {
+               initializeFriends();
+               assertThat(memoryDatabase.isFriend(sone, "Friend1"), is(false));
+       }
+
+       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 = new TestValue(null);
+                               configurationValues.put((String) invocation.getArguments()[0], stringValue);
+                               return stringValue;
+                       }
+               });
+               return configurationValues;
+       }
+
+       @Test
+       public void friendIsAddedCorrectlyToLocalSone() {
+               Map<String, Value<String>> configurationValues = prepareConfigurationValues();
+               when(sone.isLocal()).thenReturn(true);
+               memoryDatabase.addFriend(sone, "Friend1");
+               assertThat(configurationValues.get("Sone/" + SONE_ID + "/Friends/0/ID"),
+                               is(TestValue.from("Friend1")));
+               assertThat(configurationValues.get("Sone/" + SONE_ID + "/Friends/1/ID"),
+                               is(TestValue.<String>from(null)));
+       }
+
+       @Test
+       public void friendIsNotAddedToRemoteSone() {
+               memoryDatabase.addFriend(sone, "Friend1");
+               verify(configuration, never()).getStringValue(anyString());
+       }
+
+       @Test
+       public void configurationIsWrittenOnceIfFriendIsAddedTwice() {
+               prepareConfigurationValues();
+               when(sone.isLocal()).thenReturn(true);
+               memoryDatabase.addFriend(sone, "Friend1");
+               memoryDatabase.addFriend(sone, "Friend1");
+               verify(configuration, times(2)).getStringValue(anyString());
+       }
+
+       @Test
+       public void friendIsRemovedCorrectlyFromLocalSone() {
+               Map<String, Value<String>> configurationValues = prepareConfigurationValues();
+               when(sone.isLocal()).thenReturn(true);
+               memoryDatabase.addFriend(sone, "Friend1");
+               memoryDatabase.removeFriend(sone, "Friend1");
+               assertThat(configurationValues.get("Sone/" + SONE_ID + "/Friends/0/ID"),
+                               is(TestValue.<String>from(null)));
+               assertThat(configurationValues.get("Sone/" + SONE_ID + "/Friends/1/ID"),
+                               is(TestValue.<String>from(null)));
+       }
+
+       @Test
+       public void configurationIsNotWrittenWhenANonFriendIsRemoved() {
+               when(sone.isLocal()).thenReturn(true);
+               memoryDatabase.removeFriend(sone, "Friend1");
+               verify(configuration, never()).getStringValue(anyString());
+       }
+
 }