X-Git-Url: https://git.pterodactylus.net/?p=Sone.git;a=blobdiff_plain;f=src%2Fmain%2Fjava%2Fnet%2Fpterodactylus%2Fsone%2Fdatabase%2Fmemory%2FMemoryDatabase.java;h=c611eea01d4b7c24650ed65983ab5d532456a8e6;hp=eed00eab1e3607a08b69a2fdb047f7230b9d72cc;hb=d0170947a5968260c73b9b985466ede0f7c12336;hpb=60fda3f6fd8cd72151338c831f509dd8d9d0f9ff 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 eed00ea..c611eea 100644 --- a/src/main/java/net/pterodactylus/sone/database/memory/MemoryDatabase.java +++ b/src/main/java/net/pterodactylus/sone/database/memory/MemoryDatabase.java @@ -1,5 +1,5 @@ /* - * Sone - MemoryPostDatabase.java - Copyright © 2013 David Roden + * Sone - MemoryDatabase.java - Copyright © 2013 David Roden * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -17,36 +17,62 @@ package net.pterodactylus.sone.database.memory; +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; +import java.util.List; import java.util.Map; import java.util.Set; -import java.util.UUID; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; +import net.pterodactylus.sone.data.Album; +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.sone.data.impl.AbstractPostBuilder; +import net.pterodactylus.sone.data.impl.AlbumBuilderImpl; +import net.pterodactylus.sone.data.impl.ImageBuilderImpl; +import net.pterodactylus.sone.database.AlbumBuilder; +import net.pterodactylus.sone.database.Database; +import net.pterodactylus.sone.database.DatabaseException; +import net.pterodactylus.sone.database.ImageBuilder; import net.pterodactylus.sone.database.PostBuilder; import net.pterodactylus.sone.database.PostDatabase; +import net.pterodactylus.sone.database.PostReplyBuilder; +import net.pterodactylus.sone.database.SoneBuilder; import net.pterodactylus.sone.database.SoneProvider; import net.pterodactylus.util.config.Configuration; import net.pterodactylus.util.config.ConfigurationException; import com.google.common.base.Optional; +import com.google.common.base.Predicate; +import com.google.common.collect.HashMultimap; +import com.google.common.collect.Multimap; +import com.google.common.collect.SortedSetMultimap; +import com.google.common.collect.TreeMultimap; +import com.google.common.util.concurrent.AbstractService; import com.google.inject.Inject; +import com.google.inject.Singleton; /** * Memory-based {@link PostDatabase} implementation. * * @author David ‘Bombe’ Roden */ -public class MemoryPostDatabase implements PostDatabase { +@Singleton +public class MemoryDatabase extends AbstractService implements Database { /** The lock. */ private final ReadWriteLock lock = new ReentrantReadWriteLock(); @@ -54,63 +80,237 @@ public class MemoryPostDatabase implements PostDatabase { /** The Sone provider. */ private final SoneProvider soneProvider; + /** The configuration. */ + private final Configuration configuration; + private final ConfigurationLoader configurationLoader; + + private final Map allSones = new HashMap(); + /** All posts by their ID. */ private final Map allPosts = new HashMap(); /** All posts by their Sones. */ - private final Map> sonePosts = new HashMap>(); - - /** All posts by their recipient. */ - private final Map> recipientPosts = new HashMap>(); + private final Multimap sonePosts = HashMultimap.create(); /** Whether posts are known. */ private final Set knownPosts = new HashSet(); + /** All post replies by their ID. */ + private final Map allPostReplies = new HashMap(); + + /** Replies sorted by Sone. */ + private final SortedSetMultimap sonePostReplies = TreeMultimap.create(new Comparator() { + + @Override + public int compare(String leftString, String rightString) { + return leftString.compareTo(rightString); + } + }, TIME_COMPARATOR); + + /** Whether post replies are known. */ + private final Set knownPostReplies = new HashSet(); + + private final Map allAlbums = new HashMap(); + private final Multimap soneAlbums = HashMultimap.create(); + + private final Map allImages = new HashMap(); + private final Multimap soneImages = HashMultimap.create(); + + private final MemoryBookmarkDatabase memoryBookmarkDatabase; + /** * Creates a new memory database. * * @param soneProvider - * The Sone provider + * The Sone provider + * @param configuration + * The configuration for loading and saving elements */ @Inject - public MemoryPostDatabase(SoneProvider soneProvider) { + public MemoryDatabase(SoneProvider soneProvider, Configuration configuration) { this.soneProvider = soneProvider; + this.configuration = configuration; + this.configurationLoader = new ConfigurationLoader(configuration); + memoryBookmarkDatabase = + new MemoryBookmarkDatabase(this, configurationLoader); } // - // POSTPROVIDER METHODS + // DATABASE METHODS // /** - * {@inheritDocs} + * Saves the database. + * + * @throws DatabaseException + * if an error occurs while saving */ @Override + public void save() throws DatabaseException { + saveKnownPosts(); + saveKnownPostReplies(); + } + + // + // SERVICE METHODS + // + + /** {@inheritDocs} */ + @Override + protected void doStart() { + memoryBookmarkDatabase.start(); + loadKnownPosts(); + loadKnownPostReplies(); + notifyStarted(); + } + + /** {@inheritDocs} */ + @Override + protected void doStop() { + try { + memoryBookmarkDatabase.stop(); + save(); + notifyStopped(); + } catch (DatabaseException de1) { + notifyFailed(de1); + } + } + + @Override + public SoneBuilder newSoneBuilder() { + return new MemorySoneBuilder(); + } + + @Override + public void storeSone(Sone sone) { + lock.writeLock().lock(); + try { + removeSone(sone); + + allSones.put(sone.getId(), sone); + sonePosts.putAll(sone.getId(), sone.getPosts()); + for (Post post : sone.getPosts()) { + allPosts.put(post.getId(), post); + } + sonePostReplies.putAll(sone.getId(), sone.getReplies()); + for (PostReply postReply : sone.getReplies()) { + allPostReplies.put(postReply.getId(), postReply); + } + soneAlbums.putAll(sone.getId(), toAllAlbums.apply(sone)); + for (Album album : toAllAlbums.apply(sone)) { + allAlbums.put(album.getId(), album); + } + soneImages.putAll(sone.getId(), toAllImages.apply(sone)); + for (Image image : toAllImages.apply(sone)) { + allImages.put(image.getId(), image); + } + } finally { + lock.writeLock().unlock(); + } + } + + @Override + public void removeSone(Sone sone) { + lock.writeLock().lock(); + try { + allSones.remove(sone.getId()); + Collection removedPosts = sonePosts.removeAll(sone.getId()); + for (Post removedPost : removedPosts) { + allPosts.remove(removedPost.getId()); + } + Collection removedPostReplies = + sonePostReplies.removeAll(sone.getId()); + for (PostReply removedPostReply : removedPostReplies) { + allPostReplies.remove(removedPostReply.getId()); + } + Collection removedAlbums = + soneAlbums.removeAll(sone.getId()); + for (Album removedAlbum : removedAlbums) { + allAlbums.remove(removedAlbum.getId()); + } + Collection removedImages = + soneImages.removeAll(sone.getId()); + for (Image removedImage : removedImages) { + allImages.remove(removedImage.getId()); + } + } finally { + lock.writeLock().unlock(); + } + } + + @Override + public Optional getSone(String soneId) { + lock.readLock().lock(); + try { + return fromNullable(allSones.get(soneId)); + } finally { + lock.readLock().unlock(); + } + } + + @Override + public Collection getSones() { + lock.readLock().lock(); + try { + return new HashSet(allSones.values()); + } finally { + lock.readLock().unlock(); + } + } + + @Override + public Collection getLocalSones() { + lock.readLock().lock(); + try { + return from(allSones.values()).filter(LOCAL_SONE_FILTER).toSet(); + } finally { + lock.readLock().unlock(); + } + } + + @Override + public Collection getRemoteSones() { + lock.readLock().lock(); + try { + return from(allSones.values()) + .filter(not(LOCAL_SONE_FILTER)) .toSet(); + } finally { + lock.readLock().unlock(); + } + } + + // + // POSTPROVIDER METHODS + // + + /** {@inheritDocs} */ + @Override public Optional getPost(String postId) { lock.readLock().lock(); try { - return Optional.fromNullable(allPosts.get(postId)); + return fromNullable(allPosts.get(postId)); } finally { lock.readLock().unlock(); } } - /** - * {@inheritDocs} - */ + /** {@inheritDocs} */ @Override public Collection getPosts(String soneId) { return new HashSet(getPostsFrom(soneId)); } - /** - * {@inheritDocs} - */ + /** {@inheritDocs} */ @Override - public Collection getDirectedPosts(String recipientId) { + public Collection getDirectedPosts(final String recipientId) { lock.readLock().lock(); try { - Collection posts = recipientPosts.get(recipientId); - return (posts == null) ? Collections. emptySet() : new HashSet(posts); + return from(sonePosts.values()).filter(new Predicate() { + @Override + public boolean apply(Post post) { + return post.getRecipientId().asSet().contains(recipientId); + } + }).toSet(); } finally { lock.readLock().unlock(); } @@ -120,21 +320,17 @@ public class MemoryPostDatabase implements PostDatabase { // POSTBUILDERFACTORY METHODS // - /** - * {@inheritDocs} - */ + /** {@inheritDocs} */ @Override public PostBuilder newPostBuilder() { - return new MemoryPostBuilder(soneProvider); + return new MemoryPostBuilder(this, soneProvider); } // // POSTSTORE METHODS // - /** - * {@inheritDocs} - */ + /** {@inheritDocs} */ @Override public void storePost(Post post) { checkNotNull(post, "post must not be null"); @@ -142,17 +338,12 @@ public class MemoryPostDatabase implements PostDatabase { try { allPosts.put(post.getId(), post); getPostsFrom(post.getSone().getId()).add(post); - if (post.getRecipientId().isPresent()) { - getPostsTo(post.getRecipientId().get()).add(post); - } } finally { lock.writeLock().unlock(); } } - /** - * {@inheritDocs} - */ + /** {@inheritDocs} */ @Override public void removePost(Post post) { checkNotNull(post, "post must not be null"); @@ -160,115 +351,199 @@ public class MemoryPostDatabase implements PostDatabase { try { allPosts.remove(post.getId()); getPostsFrom(post.getSone().getId()).remove(post); - if (post.getRecipientId().isPresent()) { - getPostsTo(post.getRecipientId().get()).remove(post); - } post.getSone().removePost(post); } finally { lock.writeLock().unlock(); } } - /** - * {@inheritDocs} - */ + // + // POSTREPLYPROVIDER METHODS + // + + /** {@inheritDocs} */ @Override - public void storePosts(Sone sone, Collection posts) throws IllegalArgumentException { - checkNotNull(sone, "sone must not be null"); - /* verify that all posts are from the same Sone. */ - for (Post post : posts) { - if (!sone.equals(post.getSone())) { - throw new IllegalArgumentException(String.format("Post from different Sone found: %s", post)); - } + public Optional getPostReply(String id) { + lock.readLock().lock(); + try { + return fromNullable(allPostReplies.get(id)); + } finally { + lock.readLock().unlock(); } + } - lock.writeLock().lock(); + /** {@inheritDocs} */ + @Override + public List getReplies(final String postId) { + lock.readLock().lock(); try { - /* remove all posts by the Sone. */ - getPostsFrom(sone.getId()).clear(); - for (Post post : posts) { - allPosts.remove(post.getId()); - if (post.getRecipientId().isPresent()) { - getPostsTo(post.getRecipientId().get()).remove(post); - } - } + return from(allPostReplies.values()) + .filter(new Predicate() { + @Override + public boolean apply(PostReply postReply) { + return postReply.getPostId().equals(postId); + } + }).toSortedList(TIME_COMPARATOR); + } finally { + lock.readLock().unlock(); + } + } - /* add new posts. */ - getPostsFrom(sone.getId()).addAll(posts); - for (Post post : posts) { - allPosts.put(post.getId(), post); - if (post.getRecipientId().isPresent()) { - getPostsTo(post.getRecipientId().get()).add(post); - } - } + // + // POSTREPLYBUILDERFACTORY METHODS + // + + /** {@inheritDocs} */ + @Override + public PostReplyBuilder newPostReplyBuilder() { + return new MemoryPostReplyBuilder(this, soneProvider); + } + + // + // POSTREPLYSTORE METHODS + // + + /** {@inheritDocs} */ + @Override + public void storePostReply(PostReply postReply) { + lock.writeLock().lock(); + try { + allPostReplies.put(postReply.getId(), postReply); } finally { lock.writeLock().unlock(); } } - /** - * {@inheritDocs} - */ + /** {@inheritDocs} */ @Override - public void removePosts(Sone sone) { - checkNotNull(sone, "sone must not be null"); + public void removePostReply(PostReply postReply) { lock.writeLock().lock(); try { - /* remove all posts by the Sone. */ - getPostsFrom(sone.getId()).clear(); - for (Post post : sone.getPosts()) { - allPosts.remove(post.getId()); - if (post.getRecipientId().isPresent()) { - getPostsTo(post.getRecipientId().get()).remove(post); - } - } + allPostReplies.remove(postReply.getId()); } finally { lock.writeLock().unlock(); } } // - // POSTDATABASE METHODS + // ALBUMPROVDER METHODS + // + + @Override + public Optional getAlbum(String albumId) { + lock.readLock().lock(); + try { + return fromNullable(allAlbums.get(albumId)); + } finally { + lock.readLock().unlock(); + } + } + + // + // ALBUMBUILDERFACTORY METHODS + // + + @Override + public AlbumBuilder newAlbumBuilder() { + return new AlbumBuilderImpl(); + } + + // + // ALBUMSTORE METHODS // - /** - * {@inheritDocs} - */ @Override - public void loadKnownPosts(Configuration configuration, String prefix) { + public void storeAlbum(Album album) { lock.writeLock().lock(); try { - int postCounter = 0; - while (true) { - String knownPostId = configuration.getStringValue(prefix + postCounter++ + "/ID").getValue(null); - if (knownPostId == null) { - break; - } - knownPosts.add(knownPostId); - } + allAlbums.put(album.getId(), album); + soneAlbums.put(album.getSone().getId(), album); } finally { lock.writeLock().unlock(); } } - /** - * {@inheritDocs} - */ @Override - public void saveKnownPosts(Configuration configuration, String prefix) throws ConfigurationException { + public void removeAlbum(Album album) { + lock.writeLock().lock(); + try { + allAlbums.remove(album.getId()); + soneAlbums.remove(album.getSone().getId(), album); + } finally { + lock.writeLock().unlock(); + } + } + + // + // IMAGEPROVIDER METHODS + // + + @Override + public Optional getImage(String imageId) { lock.readLock().lock(); try { - int postCounter = 0; - for (String knownPostId : knownPosts) { - configuration.getStringValue(prefix + postCounter++ + "/ID").setValue(knownPostId); - } - configuration.getStringValue(prefix + postCounter + "/ID").setValue(null); + return fromNullable(allImages.get(imageId)); } finally { lock.readLock().unlock(); } } // + // IMAGEBUILDERFACTORY METHODS + // + + @Override + public ImageBuilder newImageBuilder() { + return new ImageBuilderImpl(); + } + + // + // IMAGESTORE METHODS + // + + @Override + public void storeImage(Image image) { + lock.writeLock().lock(); + try { + allImages.put(image.getId(), image); + soneImages.put(image.getSone().getId(), image); + } finally { + lock.writeLock().unlock(); + } + } + + @Override + public void removeImage(Image image) { + lock.writeLock().lock(); + try { + allImages.remove(image.getId()); + soneImages.remove(image.getSone().getId(), image); + } finally { + lock.writeLock().unlock(); + } + } + + @Override + public void bookmarkPost(Post post) { + memoryBookmarkDatabase.bookmarkPost(post); + } + + @Override + public void unbookmarkPost(Post post) { + memoryBookmarkDatabase.unbookmarkPost(post); + } + + @Override + public boolean isPostBookmarked(Post post) { + return memoryBookmarkDatabase.isPostBookmarked(post); + } + + @Override + public Set getBookmarkedPosts() { + return memoryBookmarkDatabase.getBookmarkedPosts(); + } + + // // PACKAGE-PRIVATE METHODS // @@ -276,7 +551,7 @@ public class MemoryPostDatabase implements PostDatabase { * Returns whether the given post is known. * * @param post - * The post + * The post * @return {@code true} if the post is known, {@code false} otherwise */ boolean isPostKnown(Post post) { @@ -292,9 +567,9 @@ public class MemoryPostDatabase implements PostDatabase { * Sets whether the given post is known. * * @param post - * The post + * The post * @param known - * {@code true} if the post is known, {@code false} otherwise + * {@code true} if the post is known, {@code false} otherwise */ void setPostKnown(Post post, boolean known) { lock.writeLock().lock(); @@ -309,6 +584,44 @@ public class MemoryPostDatabase implements PostDatabase { } } + /** + * Returns whether the given post reply is known. + * + * @param postReply + * The post reply + * @return {@code true} if the given post reply is known, {@code false} + * otherwise + */ + boolean isPostReplyKnown(PostReply postReply) { + lock.readLock().lock(); + try { + return knownPostReplies.contains(postReply.getId()); + } finally { + lock.readLock().unlock(); + } + } + + /** + * Sets whether the given post reply is known. + * + * @param postReply + * The post reply + * @param known + * {@code true} if the post reply is known, {@code false} otherwise + */ + void setPostReplyKnown(PostReply postReply, boolean known) { + lock.writeLock().lock(); + try { + if (known) { + knownPostReplies.add(postReply.getId()); + } else { + knownPostReplies.remove(postReply.getId()); + } + } finally { + lock.writeLock().unlock(); + } + } + // // PRIVATE METHODS // @@ -318,91 +631,82 @@ public class MemoryPostDatabase implements PostDatabase { * none yet. * * @param soneId - * The ID of the Sone to get the posts for + * The ID of the Sone to get the posts for * @return All posts */ private Collection getPostsFrom(String soneId) { - Collection posts = null; lock.readLock().lock(); try { - posts = sonePosts.get(soneId); + return sonePosts.get(soneId); } finally { lock.readLock().unlock(); } - if (posts != null) { - return posts; - } + } - posts = new HashSet(); + /** Loads the known posts. */ + private void loadKnownPosts() { + Set knownPosts = configurationLoader.loadKnownPosts(); lock.writeLock().lock(); try { - sonePosts.put(soneId, posts); + this.knownPosts.clear(); + this.knownPosts.addAll(knownPosts); } finally { lock.writeLock().unlock(); } - - return posts; } /** - * Gets all posts that are directed the given Sone, creating a new - * collection if there is none yet. + * Saves the known posts to the configuration. * - * @param recipientId - * The ID of the Sone to get the posts for - * @return All posts + * @throws DatabaseException + * if a configuration error occurs */ - private Collection getPostsTo(String recipientId) { - Collection posts = null; + private void saveKnownPosts() throws DatabaseException { lock.readLock().lock(); try { - posts = recipientPosts.get(recipientId); + 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(); } - if (posts != null) { - return posts; - } + } - posts = new HashSet(); + /** Loads the known post replies. */ + private void loadKnownPostReplies() { + Set knownPostReplies = configurationLoader.loadKnownPostReplies(); lock.writeLock().lock(); try { - recipientPosts.put(recipientId, posts); + this.knownPostReplies.clear(); + this.knownPostReplies.addAll(knownPostReplies); } finally { lock.writeLock().unlock(); } - - return posts; } /** - * {@link PostBuilder} implementation that creates a {@link MemoryPost}. + * Saves the known post replies to the configuration. * - * @author David ‘Bombe’ Roden + * @throws DatabaseException + * if a configuration error occurs */ - private class MemoryPostBuilder extends AbstractPostBuilder { - - /** - * Creates a new memory post builder. - * - * @param soneProvider - * The Sone provider - */ - public MemoryPostBuilder(SoneProvider soneProvider) { - super(soneProvider); - } - - /** - * {@inheritDocs} - */ - @Override - public Post build() throws IllegalStateException { - validate(); - Post post = new MemoryPost(MemoryPostDatabase.this, soneProvider, randomId ? UUID.randomUUID().toString() : id, senderId, recipientId, currentTime ? System.currentTimeMillis() : time, text); - post.setKnown(isPostKnown(post)); - return post; + private void saveKnownPostReplies() throws DatabaseException { + lock.readLock().lock(); + try { + int replyCounter = 0; + for (String knownReplyId : knownPostReplies) { + configuration.getStringValue("KnownReplies/" + replyCounter++ + "/ID").setValue(knownReplyId); + } + configuration.getStringValue("KnownReplies/" + replyCounter + "/ID").setValue(null); + } catch (ConfigurationException ce1) { + throw new DatabaseException("Could not save database.", ce1); + } finally { + lock.readLock().unlock(); } - } }