From 2524f4d47c56874a263f9e53ec5c4035f2baa7e0 Mon Sep 17 00:00:00 2001 From: =?utf8?q?David=20=E2=80=98Bombe=E2=80=99=20Roden?= Date: Tue, 25 Nov 2014 21:26:22 +0100 Subject: [PATCH] Move friend-related functionality into the database. --- .../java/net/pterodactylus/sone/core/Core.java | 22 +--- .../java/net/pterodactylus/sone/data/Sone.java | 18 --- .../pterodactylus/sone/data/impl/IdOnlySone.java | 10 -- .../net/pterodactylus/sone/database/Database.java | 2 +- .../sone/database/FriendDatabase.java | 10 ++ .../sone/database/FriendProvider.java | 17 +++ .../pterodactylus/sone/database/FriendStore.java | 15 +++ .../sone/database/memory/ConfigurationLoader.java | 13 ++- .../sone/database/memory/MemoryDatabase.java | 36 +++++- .../sone/database/memory/MemoryFriendDatabase.java | 79 +++++++++++++ .../sone/database/memory/MemoryDatabaseTest.java | 126 ++++++++++++++++++++- 11 files changed, 295 insertions(+), 53 deletions(-) create mode 100644 src/main/java/net/pterodactylus/sone/database/FriendDatabase.java create mode 100644 src/main/java/net/pterodactylus/sone/database/FriendProvider.java create mode 100644 src/main/java/net/pterodactylus/sone/database/FriendStore.java create mode 100644 src/main/java/net/pterodactylus/sone/database/memory/MemoryFriendDatabase.java diff --git a/src/main/java/net/pterodactylus/sone/core/Core.java b/src/main/java/net/pterodactylus/sone/core/Core.java index dfe8074..17905f3 100644 --- a/src/main/java/net/pterodactylus/sone/core/Core.java +++ b/src/main/java/net/pterodactylus/sone/core/Core.java @@ -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 likedReplyIds = configurationSoneParser.parseLikedPostReplyIds(); - /* load friends. */ - Set friends = configurationSoneParser.parseFriends(); - /* load albums. */ List 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 albums = FluentIterable.from(sone.getRootAlbum().getAlbums()).transformAndConcat(Album.FLATTENER).toList(); diff --git a/src/main/java/net/pterodactylus/sone/data/Sone.java b/src/main/java/net/pterodactylus/sone/data/Sone.java index b23e48e..27f2cb1 100644 --- a/src/main/java/net/pterodactylus/sone/data/Sone.java +++ b/src/main/java/net/pterodactylus/sone/data/Sone.java @@ -354,24 +354,6 @@ public interface Sone extends Identified, Fingerprintable, Comparable { 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 diff --git a/src/main/java/net/pterodactylus/sone/data/impl/IdOnlySone.java b/src/main/java/net/pterodactylus/sone/data/impl/IdOnlySone.java index fa7aace..0ef220b 100644 --- a/src/main/java/net/pterodactylus/sone/data/impl/IdOnlySone.java +++ b/src/main/java/net/pterodactylus/sone/data/impl/IdOnlySone.java @@ -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 getPosts() { return emptyList(); } diff --git a/src/main/java/net/pterodactylus/sone/database/Database.java b/src/main/java/net/pterodactylus/sone/database/Database.java index 2d80f7f..971a427 100644 --- a/src/main/java/net/pterodactylus/sone/database/Database.java +++ b/src/main/java/net/pterodactylus/sone/database/Database.java @@ -29,7 +29,7 @@ import com.google.inject.ImplementedBy; * @author David ‘Bombe’ Roden */ @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 index 0000000..761d356 --- /dev/null +++ b/src/main/java/net/pterodactylus/sone/database/FriendDatabase.java @@ -0,0 +1,10 @@ +package net.pterodactylus.sone.database; + +/** + * Combines a {@link FriendProvider} and a {@link FriendStore} into a friend database. + * + * @author David ‘Bombe’ Roden + */ +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 index 0000000..3665d1b --- /dev/null +++ b/src/main/java/net/pterodactylus/sone/database/FriendProvider.java @@ -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 David ‘Bombe’ Roden + */ +public interface FriendProvider { + + Collection 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 index 0000000..38c1c80 --- /dev/null +++ b/src/main/java/net/pterodactylus/sone/database/FriendStore.java @@ -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 David ‘Bombe’ Roden + */ +public interface FriendStore { + + void addFriend(Sone localSone, String friendSoneId); + void removeFriend(Sone localSone, String friendSoneId); + +} diff --git a/src/main/java/net/pterodactylus/sone/database/memory/ConfigurationLoader.java b/src/main/java/net/pterodactylus/sone/database/memory/ConfigurationLoader.java index 84d8198..1691ddb 100644 --- a/src/main/java/net/pterodactylus/sone/database/memory/ConfigurationLoader.java +++ b/src/main/java/net/pterodactylus/sone/database/memory/ConfigurationLoader.java @@ -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 loadFriends(String localSoneId) { + return loadIds("Sone/" + localSoneId + "/Friends"); + } + + public void saveFriends(String soneId, Collection friends) { + saveIds("Sone/" + soneId + "/Friends", friends); + } + public synchronized Set loadKnownPosts() { return loadIds("KnownPosts"); } @@ -56,10 +65,10 @@ public class ConfigurationLoader { saveIds("Bookmarks/Post", bookmarkedPosts); } - private void saveIds(String prefix, Set bookmarkedPosts) { + private void saveIds(String prefix, Collection ids) { try { int idCounter = 0; - for (String id : bookmarkedPosts) { + for (String id : ids) { configuration .getStringValue(prefix + "/" + idCounter++ + "/ID") .setValue(id); diff --git a/src/main/java/net/pterodactylus/sone/database/memory/MemoryDatabase.java b/src/main/java/net/pterodactylus/sone/database/memory/MemoryDatabase.java index 4ee375e..127b90c 100644 --- a/src/main/java/net/pterodactylus/sone/database/memory/MemoryDatabase.java +++ b/src/main/java/net/pterodactylus/sone/database/memory/MemoryDatabase.java @@ -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 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 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 index 0000000..1802843 --- /dev/null +++ b/src/main/java/net/pterodactylus/sone/database/memory/MemoryFriendDatabase.java @@ -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 David ‘Bombe’ Roden + */ +class MemoryFriendDatabase { + + private final ReadWriteLock lock = new ReentrantReadWriteLock(); + private final ConfigurationLoader configurationLoader; + private final Multimap soneFriends = HashMultimap.create(); + + MemoryFriendDatabase(ConfigurationLoader configurationLoader) { + this.configurationLoader = configurationLoader; + } + + Collection 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(); + } + } + +} diff --git a/src/test/java/net/pterodactylus/sone/database/memory/MemoryDatabaseTest.java b/src/test/java/net/pterodactylus/sone/database/memory/MemoryDatabaseTest.java index c5cd9f3..f7b07a2 100644 --- a/src/test/java/net/pterodactylus/sone/database/memory/MemoryDatabaseTest.java +++ b/src/test/java/net/pterodactylus/sone/database/memory/MemoryDatabaseTest.java @@ -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.absent())); } + private void initializeFriends() { + when(configuration.getStringValue("Sone/" + SONE_ID + "/Friends/0/ID")).thenReturn( + new TestValue("Friend1")); + when(configuration.getStringValue("Sone/" + SONE_ID + "/Friends/1/ID")).thenReturn( + new TestValue("Friend2")); + when(configuration.getStringValue("Sone/" + SONE_ID + "/Friends/2/ID")).thenReturn( + new TestValue(null)); + } + + @Test + public void friendsAreReturnedCorrectly() { + initializeFriends(); + when(sone.isLocal()).thenReturn(true); + Collection 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 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> prepareConfigurationValues() { + final Map> configurationValues = new HashMap>(); + when(configuration.getStringValue(anyString())).thenAnswer(new Answer>() { + @Override + public Value answer(InvocationOnMock invocation) throws Throwable { + Value stringValue = new TestValue(null); + configurationValues.put((String) invocation.getArguments()[0], stringValue); + return stringValue; + } + }); + return configurationValues; + } + + @Test + public void friendIsAddedCorrectlyToLocalSone() { + Map> 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.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> 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.from(null))); + assertThat(configurationValues.get("Sone/" + SONE_ID + "/Friends/1/ID"), + is(TestValue.from(null))); + } + + @Test + public void configurationIsNotWrittenWhenANonFriendIsRemoved() { + when(sone.isLocal()).thenReturn(true); + memoryDatabase.removeFriend(sone, "Friend1"); + verify(configuration, never()).getStringValue(anyString()); + } + } -- 2.7.4