2 * Sone - MemoryDatabase.java - Copyright © 2013 David Roden
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 3 of the License, or
7 * (at your option) any later version.
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
18 package net.pterodactylus.sone.database.memory;
20 import static com.google.common.base.Optional.fromNullable;
21 import static com.google.common.base.Preconditions.checkNotNull;
22 import static com.google.common.base.Predicates.not;
23 import static com.google.common.collect.FluentIterable.from;
24 import static java.util.Collections.unmodifiableCollection;
25 import static net.pterodactylus.sone.data.Reply.TIME_COMPARATOR;
26 import static net.pterodactylus.sone.data.Sone.LOCAL_SONE_FILTER;
27 import static net.pterodactylus.sone.data.Sone.toAllAlbums;
28 import static net.pterodactylus.sone.data.Sone.toAllImages;
30 import java.util.Collection;
31 import java.util.Comparator;
32 import java.util.HashMap;
33 import java.util.HashSet;
34 import java.util.List;
37 import java.util.concurrent.locks.ReadWriteLock;
38 import java.util.concurrent.locks.ReentrantReadWriteLock;
40 import net.pterodactylus.sone.data.Album;
41 import net.pterodactylus.sone.data.Image;
42 import net.pterodactylus.sone.data.Post;
43 import net.pterodactylus.sone.data.PostReply;
44 import net.pterodactylus.sone.data.Sone;
45 import net.pterodactylus.sone.data.impl.AlbumBuilderImpl;
46 import net.pterodactylus.sone.data.impl.ImageBuilderImpl;
47 import net.pterodactylus.sone.database.AlbumBuilder;
48 import net.pterodactylus.sone.database.Database;
49 import net.pterodactylus.sone.database.DatabaseException;
50 import net.pterodactylus.sone.database.ImageBuilder;
51 import net.pterodactylus.sone.database.PostBuilder;
52 import net.pterodactylus.sone.database.PostDatabase;
53 import net.pterodactylus.sone.database.PostReplyBuilder;
54 import net.pterodactylus.sone.database.SoneBuilder;
55 import net.pterodactylus.sone.database.SoneProvider;
56 import net.pterodactylus.util.config.Configuration;
57 import net.pterodactylus.util.config.ConfigurationException;
59 import com.google.common.base.Optional;
60 import com.google.common.base.Predicate;
61 import com.google.common.collect.HashMultimap;
62 import com.google.common.collect.Multimap;
63 import com.google.common.collect.SortedSetMultimap;
64 import com.google.common.collect.TreeMultimap;
65 import com.google.common.util.concurrent.AbstractService;
66 import com.google.inject.Inject;
67 import com.google.inject.Singleton;
70 * Memory-based {@link PostDatabase} implementation.
72 * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
75 public class MemoryDatabase extends AbstractService implements Database {
78 private final ReadWriteLock lock = new ReentrantReadWriteLock();
80 /** The Sone provider. */
81 private final SoneProvider soneProvider;
83 /** The configuration. */
84 private final Configuration configuration;
85 private final ConfigurationLoader configurationLoader;
87 private final Map<String, Sone> allSones = new HashMap<String, Sone>();
89 /** All posts by their ID. */
90 private final Map<String, Post> allPosts = new HashMap<String, Post>();
92 /** All posts by their Sones. */
93 private final Multimap<String, Post> sonePosts = HashMultimap.create();
95 /** Whether posts are known. */
96 private final Set<String> knownPosts = new HashSet<String>();
98 /** All post replies by their ID. */
99 private final Map<String, PostReply> allPostReplies = new HashMap<String, PostReply>();
101 /** Replies sorted by Sone. */
102 private final SortedSetMultimap<String, PostReply> sonePostReplies = TreeMultimap.create(new Comparator<String>() {
105 public int compare(String leftString, String rightString) {
106 return leftString.compareTo(rightString);
110 /** Whether post replies are known. */
111 private final Set<String> knownPostReplies = new HashSet<String>();
113 private final Map<String, Album> allAlbums = new HashMap<String, Album>();
114 private final Multimap<String, Album> soneAlbums = HashMultimap.create();
116 private final Map<String, Image> allImages = new HashMap<String, Image>();
117 private final Multimap<String, Image> soneImages = HashMultimap.create();
119 private final MemoryBookmarkDatabase memoryBookmarkDatabase;
122 * Creates a new memory database.
124 * @param soneProvider
126 * @param configuration
127 * The configuration for loading and saving elements
130 public MemoryDatabase(SoneProvider soneProvider, Configuration configuration) {
131 this.soneProvider = soneProvider;
132 this.configuration = configuration;
133 this.configurationLoader = new ConfigurationLoader(configuration);
134 memoryBookmarkDatabase =
135 new MemoryBookmarkDatabase(this, configurationLoader);
143 * Saves the database.
145 * @throws DatabaseException
146 * if an error occurs while saving
149 public void save() throws DatabaseException {
151 saveKnownPostReplies();
158 /** {@inheritDocs} */
160 protected void doStart() {
161 memoryBookmarkDatabase.start();
163 loadKnownPostReplies();
167 /** {@inheritDocs} */
169 protected void doStop() {
173 } catch (DatabaseException de1) {
179 public SoneBuilder newSoneBuilder() {
180 return new MemorySoneBuilder();
184 public void storeSone(Sone sone) {
185 lock.writeLock().lock();
189 allSones.put(sone.getId(), sone);
190 sonePosts.putAll(sone.getId(), sone.getPosts());
191 for (Post post : sone.getPosts()) {
192 allPosts.put(post.getId(), post);
194 sonePostReplies.putAll(sone.getId(), sone.getReplies());
195 for (PostReply postReply : sone.getReplies()) {
196 allPostReplies.put(postReply.getId(), postReply);
198 soneAlbums.putAll(sone.getId(), toAllAlbums.apply(sone));
199 for (Album album : toAllAlbums.apply(sone)) {
200 allAlbums.put(album.getId(), album);
202 soneImages.putAll(sone.getId(), toAllImages.apply(sone));
203 for (Image image : toAllImages.apply(sone)) {
204 allImages.put(image.getId(), image);
207 lock.writeLock().unlock();
212 public void removeSone(Sone sone) {
213 lock.writeLock().lock();
215 allSones.remove(sone.getId());
216 Collection<Post> removedPosts = sonePosts.removeAll(sone.getId());
217 for (Post removedPost : removedPosts) {
218 allPosts.remove(removedPost.getId());
220 Collection<PostReply> removedPostReplies =
221 sonePostReplies.removeAll(sone.getId());
222 for (PostReply removedPostReply : removedPostReplies) {
223 allPostReplies.remove(removedPostReply.getId());
225 Collection<Album> removedAlbums =
226 soneAlbums.removeAll(sone.getId());
227 for (Album removedAlbum : removedAlbums) {
228 allAlbums.remove(removedAlbum.getId());
230 Collection<Image> removedImages =
231 soneImages.removeAll(sone.getId());
232 for (Image removedImage : removedImages) {
233 allImages.remove(removedImage.getId());
236 lock.writeLock().unlock();
241 public Optional<Sone> getSone(String soneId) {
242 lock.readLock().lock();
244 return fromNullable(allSones.get(soneId));
246 lock.readLock().unlock();
251 public Collection<Sone> getSones() {
252 lock.readLock().lock();
254 return new HashSet<Sone>(allSones.values());
256 lock.readLock().unlock();
261 public Collection<Sone> getLocalSones() {
262 lock.readLock().lock();
264 return from(allSones.values()).filter(LOCAL_SONE_FILTER).toSet();
266 lock.readLock().unlock();
271 public Collection<Sone> getRemoteSones() {
272 lock.readLock().lock();
274 return from(allSones.values())
275 .filter(not(LOCAL_SONE_FILTER)) .toSet();
277 lock.readLock().unlock();
282 // POSTPROVIDER METHODS
285 /** {@inheritDocs} */
287 public Optional<Post> getPost(String postId) {
288 lock.readLock().lock();
290 return fromNullable(allPosts.get(postId));
292 lock.readLock().unlock();
296 /** {@inheritDocs} */
298 public Collection<Post> getPosts(String soneId) {
299 return new HashSet<Post>(getPostsFrom(soneId));
302 /** {@inheritDocs} */
304 public Collection<Post> getDirectedPosts(final String recipientId) {
305 lock.readLock().lock();
307 return from(sonePosts.values()).filter(new Predicate<Post>() {
309 public boolean apply(Post post) {
310 return post.getRecipientId().asSet().contains(recipientId);
314 lock.readLock().unlock();
319 // POSTBUILDERFACTORY METHODS
322 /** {@inheritDocs} */
324 public PostBuilder newPostBuilder() {
325 return new MemoryPostBuilder(this, soneProvider);
332 /** {@inheritDocs} */
334 public void storePost(Post post) {
335 checkNotNull(post, "post must not be null");
336 lock.writeLock().lock();
338 allPosts.put(post.getId(), post);
339 getPostsFrom(post.getSone().getId()).add(post);
341 lock.writeLock().unlock();
345 /** {@inheritDocs} */
347 public void removePost(Post post) {
348 checkNotNull(post, "post must not be null");
349 lock.writeLock().lock();
351 allPosts.remove(post.getId());
352 getPostsFrom(post.getSone().getId()).remove(post);
353 post.getSone().removePost(post);
355 lock.writeLock().unlock();
359 /** {@inheritDocs} */
361 public void storePosts(Sone sone, Collection<Post> posts) throws IllegalArgumentException {
362 checkNotNull(sone, "sone must not be null");
363 /* verify that all posts are from the same Sone. */
364 for (Post post : posts) {
365 if (!sone.equals(post.getSone())) {
366 throw new IllegalArgumentException(String.format("Post from different Sone found: %s", post));
370 lock.writeLock().lock();
372 /* remove all posts by the Sone. */
373 Collection<Post> oldPosts = getPostsFrom(sone.getId());
374 for (Post post : oldPosts) {
375 allPosts.remove(post.getId());
379 getPostsFrom(sone.getId()).addAll(posts);
380 for (Post post : posts) {
381 allPosts.put(post.getId(), post);
384 lock.writeLock().unlock();
388 /** {@inheritDocs} */
390 public void removePosts(Sone sone) {
391 checkNotNull(sone, "sone must not be null");
392 lock.writeLock().lock();
394 /* remove all posts by the Sone. */
395 getPostsFrom(sone.getId()).clear();
396 for (Post post : sone.getPosts()) {
397 allPosts.remove(post.getId());
400 lock.writeLock().unlock();
405 // POSTREPLYPROVIDER METHODS
408 /** {@inheritDocs} */
410 public Optional<PostReply> getPostReply(String id) {
411 lock.readLock().lock();
413 return fromNullable(allPostReplies.get(id));
415 lock.readLock().unlock();
419 /** {@inheritDocs} */
421 public List<PostReply> getReplies(final String postId) {
422 lock.readLock().lock();
424 return from(allPostReplies.values())
425 .filter(new Predicate<PostReply>() {
427 public boolean apply(PostReply postReply) {
428 return postReply.getPostId().equals(postId);
430 }).toSortedList(TIME_COMPARATOR);
432 lock.readLock().unlock();
437 // POSTREPLYBUILDERFACTORY METHODS
440 /** {@inheritDocs} */
442 public PostReplyBuilder newPostReplyBuilder() {
443 return new MemoryPostReplyBuilder(this, soneProvider);
447 // POSTREPLYSTORE METHODS
450 /** {@inheritDocs} */
452 public void storePostReply(PostReply postReply) {
453 lock.writeLock().lock();
455 allPostReplies.put(postReply.getId(), postReply);
457 lock.writeLock().unlock();
461 /** {@inheritDocs} */
463 public void storePostReplies(Sone sone, Collection<PostReply> postReplies) {
464 checkNotNull(sone, "sone must not be null");
465 /* verify that all posts are from the same Sone. */
466 for (PostReply postReply : postReplies) {
467 if (!sone.equals(postReply.getSone())) {
468 throw new IllegalArgumentException(String.format("PostReply from different Sone found: %s", postReply));
472 lock.writeLock().lock();
474 /* remove all post replies of the Sone. */
475 for (PostReply postReply : getRepliesFrom(sone.getId())) {
476 removePostReply(postReply);
478 for (PostReply postReply : postReplies) {
479 allPostReplies.put(postReply.getId(), postReply);
480 sonePostReplies.put(postReply.getSone().getId(), postReply);
483 lock.writeLock().unlock();
487 /** {@inheritDocs} */
489 public void removePostReply(PostReply postReply) {
490 lock.writeLock().lock();
492 allPostReplies.remove(postReply.getId());
494 lock.writeLock().unlock();
498 /** {@inheritDocs} */
500 public void removePostReplies(Sone sone) {
501 checkNotNull(sone, "sone must not be null");
503 lock.writeLock().lock();
505 for (PostReply postReply : sone.getReplies()) {
506 removePostReply(postReply);
509 lock.writeLock().unlock();
514 // ALBUMPROVDER METHODS
518 public Optional<Album> getAlbum(String albumId) {
519 lock.readLock().lock();
521 return fromNullable(allAlbums.get(albumId));
523 lock.readLock().unlock();
528 // ALBUMBUILDERFACTORY METHODS
532 public AlbumBuilder newAlbumBuilder() {
533 return new AlbumBuilderImpl();
537 // ALBUMSTORE METHODS
541 public void storeAlbum(Album album) {
542 lock.writeLock().lock();
544 allAlbums.put(album.getId(), album);
545 soneAlbums.put(album.getSone().getId(), album);
547 lock.writeLock().unlock();
552 public void removeAlbum(Album album) {
553 lock.writeLock().lock();
555 allAlbums.remove(album.getId());
556 soneAlbums.remove(album.getSone().getId(), album);
558 lock.writeLock().unlock();
563 // IMAGEPROVIDER METHODS
567 public Optional<Image> getImage(String imageId) {
568 lock.readLock().lock();
570 return fromNullable(allImages.get(imageId));
572 lock.readLock().unlock();
577 // IMAGEBUILDERFACTORY METHODS
581 public ImageBuilder newImageBuilder() {
582 return new ImageBuilderImpl();
586 // IMAGESTORE METHODS
590 public void storeImage(Image image) {
591 lock.writeLock().lock();
593 allImages.put(image.getId(), image);
594 soneImages.put(image.getSone().getId(), image);
596 lock.writeLock().unlock();
601 public void removeImage(Image image) {
602 lock.writeLock().lock();
604 allImages.remove(image.getId());
605 soneImages.remove(image.getSone().getId(), image);
607 lock.writeLock().unlock();
612 public void bookmarkPost(String postId) {
613 memoryBookmarkDatabase.bookmarkPost(postId);
617 public void bookmarkPost(Post post) {
618 memoryBookmarkDatabase.bookmarkPost(post);
622 public void unbookmarkPost(Post post) {
623 memoryBookmarkDatabase.unbookmarkPost(post);
627 public boolean isPostBookmarked(Post post) {
628 return memoryBookmarkDatabase.isPostBookmarked(post);
632 public Set<Post> getBookmarkedPosts() {
633 return memoryBookmarkDatabase.getBookmarkedPosts();
637 // PACKAGE-PRIVATE METHODS
641 * Returns whether the given post is known.
645 * @return {@code true} if the post is known, {@code false} otherwise
647 boolean isPostKnown(Post post) {
648 lock.readLock().lock();
650 return knownPosts.contains(post.getId());
652 lock.readLock().unlock();
657 * Sets whether the given post is known.
662 * {@code true} if the post is known, {@code false} otherwise
664 void setPostKnown(Post post, boolean known) {
665 lock.writeLock().lock();
668 knownPosts.add(post.getId());
670 knownPosts.remove(post.getId());
673 lock.writeLock().unlock();
678 * Returns whether the given post reply is known.
682 * @return {@code true} if the given post reply is known, {@code false}
685 boolean isPostReplyKnown(PostReply postReply) {
686 lock.readLock().lock();
688 return knownPostReplies.contains(postReply.getId());
690 lock.readLock().unlock();
695 * Sets whether the given post reply is known.
700 * {@code true} if the post reply is known, {@code false} otherwise
702 void setPostReplyKnown(PostReply postReply, boolean known) {
703 lock.writeLock().lock();
706 knownPostReplies.add(postReply.getId());
708 knownPostReplies.remove(postReply.getId());
711 lock.writeLock().unlock();
720 * Gets all posts for the given Sone, creating a new collection if there is
724 * The ID of the Sone to get the posts for
727 private Collection<Post> getPostsFrom(String soneId) {
728 lock.readLock().lock();
730 return sonePosts.get(soneId);
732 lock.readLock().unlock();
736 /** Loads the known posts. */
737 private void loadKnownPosts() {
738 Set<String> knownPosts = configurationLoader.loadKnownPosts();
739 lock.writeLock().lock();
741 this.knownPosts.clear();
742 this.knownPosts.addAll(knownPosts);
744 lock.writeLock().unlock();
749 * Saves the known posts to the configuration.
751 * @throws DatabaseException
752 * if a configuration error occurs
754 private void saveKnownPosts() throws DatabaseException {
755 lock.readLock().lock();
758 for (String knownPostId : knownPosts) {
759 configuration.getStringValue("KnownPosts/" + postCounter++ + "/ID").setValue(knownPostId);
761 configuration.getStringValue("KnownPosts/" + postCounter + "/ID").setValue(null);
762 } catch (ConfigurationException ce1) {
763 throw new DatabaseException("Could not save database.", ce1);
765 lock.readLock().unlock();
770 * Returns all replies by the given Sone.
774 * @return The post replies of the Sone, sorted by time (newest first)
776 private Collection<PostReply> getRepliesFrom(String id) {
777 lock.readLock().lock();
779 return unmodifiableCollection(sonePostReplies.get(id));
781 lock.readLock().unlock();
785 /** Loads the known post replies. */
786 private void loadKnownPostReplies() {
787 Set<String> knownPostReplies = configurationLoader.loadKnownPostReplies();
788 lock.writeLock().lock();
790 this.knownPostReplies.clear();
791 this.knownPostReplies.addAll(knownPostReplies);
793 lock.writeLock().unlock();
798 * Saves the known post replies to the configuration.
800 * @throws DatabaseException
801 * if a configuration error occurs
803 private void saveKnownPostReplies() throws DatabaseException {
804 lock.readLock().lock();
806 int replyCounter = 0;
807 for (String knownReplyId : knownPostReplies) {
808 configuration.getStringValue("KnownReplies/" + replyCounter++ + "/ID").setValue(knownReplyId);
810 configuration.getStringValue("KnownReplies/" + replyCounter + "/ID").setValue(null);
811 } catch (ConfigurationException ce1) {
812 throw new DatabaseException("Could not save database.", ce1);
814 lock.readLock().unlock();