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 net.pterodactylus.sone.data.Sone.LOCAL_SONE_FILTER;
26 import java.util.ArrayList;
27 import java.util.Collection;
28 import java.util.Collections;
29 import java.util.Comparator;
30 import java.util.HashMap;
31 import java.util.HashSet;
32 import java.util.List;
35 import java.util.SortedSet;
36 import java.util.TreeSet;
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.Reply;
45 import net.pterodactylus.sone.data.Sone;
46 import net.pterodactylus.sone.database.Database;
47 import net.pterodactylus.sone.database.DatabaseException;
48 import net.pterodactylus.sone.database.PostBuilder;
49 import net.pterodactylus.sone.database.PostDatabase;
50 import net.pterodactylus.sone.database.PostReplyBuilder;
51 import net.pterodactylus.sone.database.SoneBuilder;
52 import net.pterodactylus.util.config.Configuration;
53 import net.pterodactylus.util.config.ConfigurationException;
55 import com.google.common.base.Optional;
56 import com.google.common.collect.ArrayListMultimap;
57 import com.google.common.collect.ListMultimap;
58 import com.google.common.collect.SortedSetMultimap;
59 import com.google.common.collect.TreeMultimap;
60 import com.google.common.util.concurrent.AbstractService;
61 import com.google.inject.Inject;
64 * Memory-based {@link PostDatabase} implementation.
66 * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
68 public class MemoryDatabase extends AbstractService implements Database {
71 private final ReadWriteLock lock = new ReentrantReadWriteLock();
73 /** The configuration. */
74 private final Configuration configuration;
76 private final Map<String, Sone> sones = new HashMap<String, Sone>();
78 /** All posts by their ID. */
79 private final Map<String, Post> allPosts = new HashMap<String, Post>();
81 /** All posts by their Sones. */
82 private final Map<String, Collection<Post>> sonePosts = new HashMap<String, Collection<Post>>();
84 /** All posts by their recipient. */
85 private final Map<String, Collection<Post>> recipientPosts = new HashMap<String, Collection<Post>>();
87 /** Whether posts are known. */
88 private final Set<String> knownPosts = new HashSet<String>();
90 /** All post replies by their ID. */
91 private final Map<String, PostReply> allPostReplies = new HashMap<String, PostReply>();
93 /** Replies sorted by Sone. */
94 private final SortedSetMultimap<String, PostReply> sonePostReplies = TreeMultimap.create(new Comparator<String>() {
97 public int compare(String leftString, String rightString) {
98 return leftString.compareTo(rightString);
100 }, PostReply.TIME_COMPARATOR);
102 /** Replies by post. */
103 private final Map<String, SortedSet<PostReply>> postReplies = new HashMap<String, SortedSet<PostReply>>();
105 /** Whether post replies are known. */
106 private final Set<String> knownPostReplies = new HashSet<String>();
108 private final Map<String, Album> allAlbums = new HashMap<String, Album>();
109 private final ListMultimap<String, String> albumChildren = ArrayListMultimap.create();
110 private final ListMultimap<String, String> albumImages = ArrayListMultimap.create();
112 private final Map<String, Image> allImages = new HashMap<String, Image>();
115 * Creates a new memory database.
117 * @param configuration
118 * The configuration for loading and saving elements
121 public MemoryDatabase(Configuration configuration) {
122 this.configuration = configuration;
130 * Saves the database.
132 * @throws DatabaseException
133 * if an error occurs while saving
136 public void save() throws DatabaseException {
138 saveKnownPostReplies();
145 /** {@inheritDocs} */
147 protected void doStart() {
149 loadKnownPostReplies();
153 /** {@inheritDocs} */
155 protected void doStop() {
159 } catch (DatabaseException de1) {
165 public Optional<Sone> getSone(String soneId) {
166 lock.readLock().lock();
168 return fromNullable(sones.get(soneId));
170 lock.readLock().unlock();
175 public Collection<Sone> getSones() {
176 lock.readLock().lock();
178 return Collections.unmodifiableCollection(sones.values());
180 lock.readLock().unlock();
185 public Collection<Sone> getLocalSones() {
186 lock.readLock().lock();
188 return from(getSones()).filter(LOCAL_SONE_FILTER).toSet();
190 lock.readLock().unlock();
195 public Collection<Sone> getRemoteSones() {
196 lock.readLock().lock();
198 return from(getSones()).filter(not(LOCAL_SONE_FILTER)).toSet();
200 lock.readLock().unlock();
205 public SoneBuilder newSoneBuilder() {
210 // POSTPROVIDER METHODS
213 /** {@inheritDocs} */
215 public Optional<Post> getPost(String postId) {
216 lock.readLock().lock();
218 return fromNullable(allPosts.get(postId));
220 lock.readLock().unlock();
224 /** {@inheritDocs} */
226 public Collection<Post> getPosts(String soneId) {
227 return new HashSet<Post>(getPostsFrom(soneId));
230 /** {@inheritDocs} */
232 public Collection<Post> getDirectedPosts(String recipientId) {
233 lock.readLock().lock();
235 Collection<Post> posts = recipientPosts.get(recipientId);
236 return (posts == null) ? Collections.<Post>emptySet() : new HashSet<Post>(posts);
238 lock.readLock().unlock();
243 // POSTBUILDERFACTORY METHODS
246 /** {@inheritDocs} */
248 public PostBuilder newPostBuilder() {
249 return new MemoryPostBuilder(this);
256 /** {@inheritDocs} */
258 public void storePost(Post post) {
259 checkNotNull(post, "post must not be null");
260 lock.writeLock().lock();
262 allPosts.put(post.getId(), post);
263 getPostsFrom(post.getSone().getId()).add(post);
264 if (post.getRecipientId().isPresent()) {
265 getPostsTo(post.getRecipientId().get()).add(post);
268 lock.writeLock().unlock();
272 /** {@inheritDocs} */
274 public void removePost(Post post) {
275 checkNotNull(post, "post must not be null");
276 lock.writeLock().lock();
278 allPosts.remove(post.getId());
279 getPostsFrom(post.getSone().getId()).remove(post);
280 if (post.getRecipientId().isPresent()) {
281 getPostsTo(post.getRecipientId().get()).remove(post);
283 post.getSone().removePost(post);
285 lock.writeLock().unlock();
289 /** {@inheritDocs} */
291 public void storePosts(Sone sone, Collection<Post> posts) throws IllegalArgumentException {
292 checkNotNull(sone, "sone must not be null");
293 /* verify that all posts are from the same Sone. */
294 for (Post post : posts) {
295 if (!sone.equals(post.getSone())) {
296 throw new IllegalArgumentException(String.format("Post from different Sone found: %s", post));
300 lock.writeLock().lock();
302 /* remove all posts by the Sone. */
303 getPostsFrom(sone.getId()).clear();
304 for (Post post : posts) {
305 allPosts.remove(post.getId());
306 if (post.getRecipientId().isPresent()) {
307 getPostsTo(post.getRecipientId().get()).remove(post);
312 getPostsFrom(sone.getId()).addAll(posts);
313 for (Post post : posts) {
314 allPosts.put(post.getId(), post);
315 if (post.getRecipientId().isPresent()) {
316 getPostsTo(post.getRecipientId().get()).add(post);
320 lock.writeLock().unlock();
324 /** {@inheritDocs} */
326 public void removePosts(Sone sone) {
327 checkNotNull(sone, "sone must not be null");
328 lock.writeLock().lock();
330 /* remove all posts by the Sone. */
331 getPostsFrom(sone.getId()).clear();
332 for (Post post : sone.getPosts()) {
333 allPosts.remove(post.getId());
334 if (post.getRecipientId().isPresent()) {
335 getPostsTo(post.getRecipientId().get()).remove(post);
339 lock.writeLock().unlock();
344 // POSTREPLYPROVIDER METHODS
347 /** {@inheritDocs} */
349 public Optional<PostReply> getPostReply(String id) {
350 lock.readLock().lock();
352 return fromNullable(allPostReplies.get(id));
354 lock.readLock().unlock();
358 /** {@inheritDocs} */
360 public List<PostReply> getReplies(String postId) {
361 lock.readLock().lock();
363 if (!postReplies.containsKey(postId)) {
364 return Collections.emptyList();
366 return new ArrayList<PostReply>(postReplies.get(postId));
368 lock.readLock().unlock();
373 // POSTREPLYBUILDERFACTORY METHODS
376 /** {@inheritDocs} */
378 public PostReplyBuilder newPostReplyBuilder() {
379 return new MemoryPostReplyBuilder(this, this);
383 // POSTREPLYSTORE METHODS
386 /** {@inheritDocs} */
388 public void storePostReply(PostReply postReply) {
389 lock.writeLock().lock();
391 allPostReplies.put(postReply.getId(), postReply);
392 if (postReplies.containsKey(postReply.getPostId())) {
393 postReplies.get(postReply.getPostId()).add(postReply);
395 TreeSet<PostReply> replies = new TreeSet<PostReply>(Reply.TIME_COMPARATOR);
396 replies.add(postReply);
397 postReplies.put(postReply.getPostId(), replies);
400 lock.writeLock().unlock();
404 /** {@inheritDocs} */
406 public void storePostReplies(Sone sone, Collection<PostReply> postReplies) {
407 checkNotNull(sone, "sone must not be null");
408 /* verify that all posts are from the same Sone. */
409 for (PostReply postReply : postReplies) {
410 if (!sone.equals(postReply.getSone())) {
411 throw new IllegalArgumentException(String.format("PostReply from different Sone found: %s", postReply));
415 lock.writeLock().lock();
417 /* remove all post replies of the Sone. */
418 for (PostReply postReply : getRepliesFrom(sone.getId())) {
419 removePostReply(postReply);
421 for (PostReply postReply : postReplies) {
422 allPostReplies.put(postReply.getId(), postReply);
423 sonePostReplies.put(postReply.getSone().getId(), postReply);
424 if (this.postReplies.containsKey(postReply.getPostId())) {
425 this.postReplies.get(postReply.getPostId()).add(postReply);
427 TreeSet<PostReply> replies = new TreeSet<PostReply>(Reply.TIME_COMPARATOR);
428 replies.add(postReply);
429 this.postReplies.put(postReply.getPostId(), replies);
433 lock.writeLock().unlock();
437 /** {@inheritDocs} */
439 public void removePostReply(PostReply postReply) {
440 lock.writeLock().lock();
442 allPostReplies.remove(postReply.getId());
443 if (postReplies.containsKey(postReply.getPostId())) {
444 postReplies.get(postReply.getPostId()).remove(postReply);
445 if (postReplies.get(postReply.getPostId()).isEmpty()) {
446 postReplies.remove(postReply.getPostId());
450 lock.writeLock().unlock();
454 /** {@inheritDocs} */
456 public void removePostReplies(Sone sone) {
457 checkNotNull(sone, "sone must not be null");
459 lock.writeLock().lock();
461 for (PostReply postReply : sone.getReplies()) {
462 removePostReply(postReply);
465 lock.writeLock().unlock();
470 // ALBUMPROVDER METHODS
474 public Optional<Album> getAlbum(String albumId) {
475 lock.readLock().lock();
477 return fromNullable(allAlbums.get(albumId));
479 lock.readLock().unlock();
484 // ALBUMSTORE METHODS
488 public void storeAlbum(Album album) {
489 lock.writeLock().lock();
491 allAlbums.put(album.getId(), album);
492 albumChildren.put(album.getParent().getId(), album.getId());
494 lock.writeLock().unlock();
499 public void removeAlbum(Album album) {
500 lock.writeLock().lock();
502 allAlbums.remove(album.getId());
503 albumChildren.remove(album.getParent().getId(), album.getId());
505 lock.writeLock().unlock();
510 // IMAGEPROVIDER METHODS
514 public Optional<Image> getImage(String imageId) {
515 lock.readLock().lock();
517 return fromNullable(allImages.get(imageId));
519 lock.readLock().unlock();
524 // IMAGESTORE METHODS
528 public void storeImage(Image image) {
529 lock.writeLock().lock();
531 allImages.put(image.getId(), image);
532 albumImages.put(image.getAlbum().getId(), image.getId());
534 lock.writeLock().unlock();
539 public void removeImage(Image image) {
540 lock.writeLock().lock();
542 allImages.remove(image.getId());
543 albumImages.remove(image.getAlbum().getId(), image.getId());
545 lock.writeLock().unlock();
550 // PACKAGE-PRIVATE METHODS
554 * Returns whether the given post is known.
558 * @return {@code true} if the post is known, {@code false} otherwise
560 boolean isPostKnown(Post post) {
561 lock.readLock().lock();
563 return knownPosts.contains(post.getId());
565 lock.readLock().unlock();
570 * Sets whether the given post is known.
575 * {@code true} if the post is known, {@code false} otherwise
577 void setPostKnown(Post post, boolean known) {
578 lock.writeLock().lock();
581 knownPosts.add(post.getId());
583 knownPosts.remove(post.getId());
586 lock.writeLock().unlock();
591 * Returns whether the given post reply is known.
595 * @return {@code true} if the given post reply is known, {@code false}
598 boolean isPostReplyKnown(PostReply postReply) {
599 lock.readLock().lock();
601 return knownPostReplies.contains(postReply.getId());
603 lock.readLock().unlock();
608 * Sets whether the given post reply is known.
613 * {@code true} if the post reply is known, {@code false} otherwise
615 void setPostReplyKnown(PostReply postReply, boolean known) {
616 lock.writeLock().lock();
619 knownPostReplies.add(postReply.getId());
621 knownPostReplies.remove(postReply.getId());
624 lock.writeLock().unlock();
628 void moveUp(Album album) {
629 lock.writeLock().lock();
631 List<String> albums = albumChildren.get(album.getParent().getId());
632 int currentIndex = albums.indexOf(album.getId());
633 if (currentIndex == 0) {
636 albums.remove(album.getId());
637 albums.add(currentIndex - 1, album.getId());
639 lock.writeLock().unlock();
643 void moveDown(Album album) {
644 lock.writeLock().lock();
646 List<String> albums = albumChildren.get(album.getParent().getId());
647 int currentIndex = albums.indexOf(album.getId());
648 if (currentIndex == (albums.size() - 1)) {
651 albums.remove(album.getId());
652 albums.add(currentIndex + 1, album.getId());
654 lock.writeLock().unlock();
658 void moveUp(Image image) {
659 lock.writeLock().lock();
661 List<String> images = albumImages.get(image.getAlbum().getId());
662 int currentIndex = images.indexOf(image.getId());
663 if (currentIndex == 0) {
666 images.remove(image.getId());
667 images.add(currentIndex - 1, image.getId());
669 lock.writeLock().unlock();
673 void moveDown(Image image) {
674 lock.writeLock().lock();
676 List<String> images = albumChildren.get(image.getAlbum().getId());
677 int currentIndex = images.indexOf(image.getId());
678 if (currentIndex == (images.size() - 1)) {
681 images.remove(image.getId());
682 images.add(currentIndex + 1, image.getId());
684 lock.writeLock().unlock();
693 * Gets all posts for the given Sone, creating a new collection if there is
697 * The ID of the Sone to get the posts for
700 private Collection<Post> getPostsFrom(String soneId) {
701 Collection<Post> posts = null;
702 lock.readLock().lock();
704 posts = sonePosts.get(soneId);
706 lock.readLock().unlock();
712 posts = new HashSet<Post>();
713 lock.writeLock().lock();
715 sonePosts.put(soneId, posts);
717 lock.writeLock().unlock();
724 * Gets all posts that are directed the given Sone, creating a new collection
725 * if there is none yet.
728 * The ID of the Sone to get the posts for
731 private Collection<Post> getPostsTo(String recipientId) {
732 Collection<Post> posts = null;
733 lock.readLock().lock();
735 posts = recipientPosts.get(recipientId);
737 lock.readLock().unlock();
743 posts = new HashSet<Post>();
744 lock.writeLock().lock();
746 recipientPosts.put(recipientId, posts);
748 lock.writeLock().unlock();
754 /** Loads the known posts. */
755 private void loadKnownPosts() {
756 lock.writeLock().lock();
760 String knownPostId = configuration.getStringValue("KnownPosts/" + postCounter++ + "/ID").getValue(null);
761 if (knownPostId == null) {
764 knownPosts.add(knownPostId);
767 lock.writeLock().unlock();
772 * Saves the known posts to the configuration.
774 * @throws DatabaseException
775 * if a configuration error occurs
777 private void saveKnownPosts() throws DatabaseException {
778 lock.readLock().lock();
781 for (String knownPostId : knownPosts) {
782 configuration.getStringValue("KnownPosts/" + postCounter++ + "/ID").setValue(knownPostId);
784 configuration.getStringValue("KnownPosts/" + postCounter + "/ID").setValue(null);
785 } catch (ConfigurationException ce1) {
786 throw new DatabaseException("Could not save database.", ce1);
788 lock.readLock().unlock();
793 * Returns all replies by the given Sone.
797 * @return The post replies of the Sone, sorted by time (newest first)
799 private Collection<PostReply> getRepliesFrom(String id) {
800 lock.readLock().lock();
802 if (sonePostReplies.containsKey(id)) {
803 return Collections.unmodifiableCollection(sonePostReplies.get(id));
805 return Collections.emptySet();
807 lock.readLock().unlock();
811 /** Loads the known post replies. */
812 private void loadKnownPostReplies() {
813 lock.writeLock().lock();
815 int replyCounter = 0;
817 String knownReplyId = configuration.getStringValue("KnownReplies/" + replyCounter++ + "/ID").getValue(null);
818 if (knownReplyId == null) {
821 knownPostReplies.add(knownReplyId);
824 lock.writeLock().unlock();
829 * Saves the known post replies to the configuration.
831 * @throws DatabaseException
832 * if a configuration error occurs
834 private void saveKnownPostReplies() throws DatabaseException {
835 lock.readLock().lock();
837 int replyCounter = 0;
838 for (String knownReplyId : knownPostReplies) {
839 configuration.getStringValue("KnownReplies/" + replyCounter++ + "/ID").setValue(knownReplyId);
841 configuration.getStringValue("KnownReplies/" + replyCounter + "/ID").setValue(null);
842 } catch (ConfigurationException ce1) {
843 throw new DatabaseException("Could not save database.", ce1);
845 lock.readLock().unlock();