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();
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);
Set<String> likedReplyIds =
configurationSoneParser.parseLikedPostReplyIds();
- /* load friends. */
- Set<String> friends = configurationSoneParser.parseFriends();
-
/* load albums. */
List<Album> topLevelAlbums;
try {
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);
}
soneInserters.get(sone).setLastInsertFingerprint(lastInsertFingerprint);
}
}
- synchronized (knownSones) {
- for (String friend : friends) {
- knownSones.add(friend);
- }
- }
for (Post post : posts) {
post.setKnown(true);
}
}
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();
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
}
@Override
- public Sone addFriend(String friendSone) {
- return this;
- }
-
- @Override
- public Sone removeFriend(String friendSoneId) {
- return this;
- }
-
- @Override
public List<Post> getPosts() {
return emptyList();
}
* @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.
--- /dev/null
+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 {
+
+}
--- /dev/null
+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);
+
+}
--- /dev/null
+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);
+
+}
import static java.util.logging.Level.WARNING;
+import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import java.util.logging.Logger;
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");
}
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);
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;
private final Multimap<String, Image> soneImages = HashMultimap.create();
private final MemoryBookmarkDatabase memoryBookmarkDatabase;
+ private final MemoryFriendDatabase memoryFriendDatabase;
/**
* Creates a new memory database.
this.configurationLoader = new ConfigurationLoader(configuration);
memoryBookmarkDatabase =
new MemoryBookmarkDatabase(this, configurationLoader);
+ memoryFriendDatabase = new MemoryFriendDatabase(configurationLoader);
}
//
}
}
+ @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
//
--- /dev/null
+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();
+ }
+ }
+
+}
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}.
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
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());
+ }
+
}