/* * 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 * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ 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.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.List; 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.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.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 */ @Singleton public class MemoryDatabase extends AbstractService implements Database { /** The lock. */ private final ReadWriteLock lock = new ReentrantReadWriteLock(); /** 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 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 * @param configuration * The configuration for loading and saving elements */ @Inject public MemoryDatabase(SoneProvider soneProvider, Configuration configuration) { this.soneProvider = soneProvider; this.configuration = configuration; this.configurationLoader = new ConfigurationLoader(configuration); memoryBookmarkDatabase = new MemoryBookmarkDatabase(this, configurationLoader); } // // DATABASE METHODS // /** * 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 fromNullable(allPosts.get(postId)); } finally { lock.readLock().unlock(); } } /** {@inheritDocs} */ @Override public Collection getPosts(String soneId) { return new HashSet(getPostsFrom(soneId)); } /** {@inheritDocs} */ @Override public Collection getDirectedPosts(final String recipientId) { lock.readLock().lock(); try { return from(sonePosts.values()).filter(new Predicate() { @Override public boolean apply(Post post) { return post.getRecipientId().asSet().contains(recipientId); } }).toSet(); } finally { lock.readLock().unlock(); } } // // POSTBUILDERFACTORY METHODS // /** {@inheritDocs} */ @Override public PostBuilder newPostBuilder() { return new MemoryPostBuilder(this, soneProvider); } // // POSTSTORE METHODS // /** {@inheritDocs} */ @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(); } } /** {@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(); } } // // POSTREPLYPROVIDER METHODS // /** {@inheritDocs} */ @Override public Optional getPostReply(String id) { lock.readLock().lock(); try { return fromNullable(allPostReplies.get(id)); } finally { lock.readLock().unlock(); } } /** {@inheritDocs} */ @Override public List getReplies(final String postId) { lock.readLock().lock(); try { 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(); } } // // 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} */ @Override public void removePostReply(PostReply postReply) { lock.writeLock().lock(); try { allPostReplies.remove(postReply.getId()); } finally { lock.writeLock().unlock(); } } // // 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 // @Override public void storeAlbum(Album album) { lock.writeLock().lock(); try { allAlbums.put(album.getId(), album); soneAlbums.put(album.getSone().getId(), album); } finally { lock.writeLock().unlock(); } } @Override 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 { 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 // /** * Returns whether the given post is known. * * @param post * The post * @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(); } } /** * Sets whether the given post is known. * * @param post * The post * @param known * {@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(); } } /** * 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 // /** * Gets all posts for the given Sone, creating a new collection if there is * none yet. * * @param soneId * The ID of the Sone to get the posts for * @return All posts */ private Collection getPostsFrom(String soneId) { lock.readLock().lock(); try { return sonePosts.get(soneId); } finally { lock.readLock().unlock(); } } /** Loads the known posts. */ private void loadKnownPosts() { Set 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(); } } /** Loads the known post replies. */ private void loadKnownPostReplies() { Set knownPostReplies = configurationLoader.loadKnownPostReplies(); lock.writeLock().lock(); try { this.knownPostReplies.clear(); this.knownPostReplies.addAll(knownPostReplies); } finally { lock.writeLock().unlock(); } } /** * Saves the known post replies to the configuration. * * @throws DatabaseException * if a configuration error occurs */ 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(); } } }