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.emptyList;
25 import static net.pterodactylus.sone.data.Sone.LOCAL_SONE_FILTER;
27 import java.util.ArrayList;
28 import java.util.Collection;
29 import java.util.Collections;
30 import java.util.Comparator;
31 import java.util.HashMap;
32 import java.util.HashSet;
33 import java.util.List;
36 import java.util.concurrent.locks.ReadWriteLock;
37 import java.util.concurrent.locks.ReentrantReadWriteLock;
39 import net.pterodactylus.sone.data.Album;
40 import net.pterodactylus.sone.data.Image;
41 import net.pterodactylus.sone.data.Post;
42 import net.pterodactylus.sone.data.PostReply;
43 import net.pterodactylus.sone.data.Sone;
44 import net.pterodactylus.sone.data.impl.DefaultSoneBuilder;
45 import net.pterodactylus.sone.database.Database;
46 import net.pterodactylus.sone.database.DatabaseException;
47 import net.pterodactylus.sone.database.PostDatabase;
48 import net.pterodactylus.sone.database.SoneBuilder;
49 import net.pterodactylus.sone.freenet.wot.Identity;
50 import net.pterodactylus.util.config.Configuration;
51 import net.pterodactylus.util.config.ConfigurationException;
53 import com.google.common.base.Function;
54 import com.google.common.base.Optional;
55 import com.google.common.collect.ArrayListMultimap;
56 import com.google.common.collect.HashMultimap;
57 import com.google.common.collect.ListMultimap;
58 import com.google.common.collect.Maps;
59 import com.google.common.collect.Multimap;
60 import com.google.common.collect.SetMultimap;
61 import com.google.common.collect.SortedSetMultimap;
62 import com.google.common.collect.TreeMultimap;
63 import com.google.common.util.concurrent.AbstractService;
64 import com.google.inject.Inject;
67 * Memory-based {@link PostDatabase} implementation.
69 * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
71 public class MemoryDatabase extends AbstractService implements Database {
74 private final ReadWriteLock lock = new ReentrantReadWriteLock();
76 /** The configuration. */
77 private final Configuration configuration;
79 private final Map<String, Identity> identities = Maps.newHashMap();
80 private final Map<String, Sone> sones = new HashMap<String, Sone>();
82 /** All posts by their ID. */
83 private final Map<String, Post> allPosts = new HashMap<String, Post>();
85 /** All posts by their Sones. */
86 private final Multimap<String, Post> sonePosts = HashMultimap.create();
87 private final SetMultimap<String, String> likedPosts = HashMultimap.create();
89 /** All posts by their recipient. */
90 private final Multimap<String, Post> recipientPosts = HashMultimap.create();
92 /** Whether posts are known. */
93 private final Set<String> knownPosts = new HashSet<String>();
95 /** All post replies by their ID. */
96 private final Map<String, PostReply> allPostReplies = new HashMap<String, PostReply>();
98 /** Replies sorted by Sone. */
99 private final SortedSetMultimap<String, PostReply> sonePostReplies = TreeMultimap.create(new Comparator<String>() {
102 public int compare(String leftString, String rightString) {
103 return leftString.compareTo(rightString);
105 }, PostReply.TIME_COMPARATOR);
107 /** Replies by post. */
108 private final SortedSetMultimap<String, PostReply> postReplies = TreeMultimap.create(new Comparator<String>() {
111 public int compare(String leftString, String rightString) {
112 return leftString.compareTo(rightString);
114 }, PostReply.TIME_COMPARATOR);
116 /** Whether post replies are known. */
117 private final Set<String> knownPostReplies = new HashSet<String>();
119 private final Map<String, Album> allAlbums = new HashMap<String, Album>();
120 private final ListMultimap<String, String> albumChildren = ArrayListMultimap.create();
121 private final ListMultimap<String, String> albumImages = ArrayListMultimap.create();
123 private final Map<String, Image> allImages = new HashMap<String, Image>();
126 * Creates a new memory database.
128 * @param configuration
129 * The configuration for loading and saving elements
132 public MemoryDatabase(Configuration configuration) {
133 this.configuration = configuration;
141 public void save() throws DatabaseException {
143 saveKnownPostReplies();
151 protected void doStart() {
153 loadKnownPostReplies();
158 protected void doStop() {
162 } catch (DatabaseException de1) {
168 public Optional<Identity> getIdentity(String identityId) {
169 lock.readLock().lock();
171 return fromNullable(identities.get(identityId));
173 lock.readLock().unlock();
178 public Function<String, Optional<Sone>> getSone() {
179 return new Function<String, Optional<Sone>>() {
181 public Optional<Sone> apply(String soneId) {
182 return (soneId == null) ? Optional.<Sone>absent() : getSone(soneId);
188 public Optional<Sone> getSone(String soneId) {
189 lock.readLock().lock();
191 return fromNullable(sones.get(soneId));
193 lock.readLock().unlock();
198 public Collection<Sone> getSones() {
199 lock.readLock().lock();
201 return Collections.unmodifiableCollection(sones.values());
203 lock.readLock().unlock();
208 public Collection<Sone> getLocalSones() {
209 lock.readLock().lock();
211 return from(getSones()).filter(LOCAL_SONE_FILTER).toSet();
213 lock.readLock().unlock();
218 public Collection<Sone> getRemoteSones() {
219 lock.readLock().lock();
221 return from(getSones()).filter(not(LOCAL_SONE_FILTER)).toSet();
223 lock.readLock().unlock();
228 public SoneBuilder newSoneBuilder() {
229 return new DefaultSoneBuilder(this) {
231 public Sone build(Optional<SoneCreated> soneCreated) throws IllegalStateException {
232 Sone sone = super.build(soneCreated);
233 lock.writeLock().lock();
235 sones.put(sone.getId(), sone);
237 lock.writeLock().unlock();
245 // POSTPROVIDER METHODS
249 public Function<String, Optional<Post>> getPost() {
250 return new Function<String, Optional<Post>>() {
252 public Optional<Post> apply(String postId) {
253 return (postId == null) ? Optional.<Post>absent() : getPost(postId);
259 public Optional<Post> getPost(String postId) {
260 lock.readLock().lock();
262 return fromNullable(allPosts.get(postId));
264 lock.readLock().unlock();
269 public Collection<Post> getPosts(String soneId) {
270 lock.readLock().lock();
272 return new HashSet<Post>(sonePosts.get(soneId));
274 lock.readLock().unlock();
279 public Collection<Post> getDirectedPosts(String recipientId) {
280 lock.readLock().lock();
282 Collection<Post> posts = recipientPosts.get(recipientId);
283 return (posts == null) ? Collections.<Post>emptySet() : new HashSet<Post>(posts);
285 lock.readLock().unlock();
290 public void likePost(Post post, Sone localSone) {
291 lock.writeLock().lock();
293 likedPosts.put(localSone.getId(), post.getId());
295 lock.writeLock().unlock();
300 public void unlikePost(Post post, Sone localSone) {
301 lock.writeLock().lock();
303 likedPosts.remove(localSone.getId(), post.getId());
305 lock.writeLock().unlock();
314 public void storePost(Post post) {
315 checkNotNull(post, "post must not be null");
316 lock.writeLock().lock();
318 allPosts.put(post.getId(), post);
319 sonePosts.put(post.getSone().getId(), post);
320 if (post.getRecipientId().isPresent()) {
321 recipientPosts.put(post.getRecipientId().get(), post);
324 lock.writeLock().unlock();
329 public void removePost(Post post) {
330 checkNotNull(post, "post must not be null");
331 lock.writeLock().lock();
333 allPosts.remove(post.getId());
334 sonePosts.remove(post.getSone().getId(), post);
335 if (post.getRecipientId().isPresent()) {
336 recipientPosts.remove(post.getRecipientId().get(), post);
338 post.getSone().removePost(post);
340 lock.writeLock().unlock();
345 public void storePosts(Sone sone, Collection<Post> posts) throws IllegalArgumentException {
346 checkNotNull(sone, "sone must not be null");
347 /* verify that all posts are from the same Sone. */
348 for (Post post : posts) {
349 if (!sone.equals(post.getSone())) {
350 throw new IllegalArgumentException(String.format("Post from different Sone found: %s", post));
354 lock.writeLock().lock();
356 /* remove all posts by the Sone. */
357 sonePosts.removeAll(sone.getId());
358 for (Post post : posts) {
359 allPosts.remove(post.getId());
360 if (post.getRecipientId().isPresent()) {
361 recipientPosts.remove(post.getRecipientId().get(), post);
366 sonePosts.putAll(sone.getId(), posts);
367 for (Post post : posts) {
368 allPosts.put(post.getId(), post);
369 if (post.getRecipientId().isPresent()) {
370 recipientPosts.put(post.getRecipientId().get(), post);
374 lock.writeLock().unlock();
379 public void removePosts(Sone sone) {
380 checkNotNull(sone, "sone must not be null");
381 lock.writeLock().lock();
383 /* remove all posts by the Sone. */
384 sonePosts.removeAll(sone.getId());
385 for (Post post : sone.getPosts()) {
386 allPosts.remove(post.getId());
387 if (post.getRecipientId().isPresent()) {
388 recipientPosts.remove(post.getRecipientId().get(), post);
392 lock.writeLock().unlock();
397 // POSTREPLYPROVIDER METHODS
401 public Optional<PostReply> getPostReply(String id) {
402 lock.readLock().lock();
404 return fromNullable(allPostReplies.get(id));
406 lock.readLock().unlock();
411 public List<PostReply> getReplies(String postId) {
412 lock.readLock().lock();
414 if (!postReplies.containsKey(postId)) {
417 return new ArrayList<PostReply>(postReplies.get(postId));
419 lock.readLock().unlock();
424 // POSTREPLYSTORE METHODS
428 * Returns whether the given post reply is known.
432 * @return {@code true} if the given post reply is known, {@code false}
435 public boolean isPostReplyKnown(PostReply postReply) {
436 lock.readLock().lock();
438 return knownPostReplies.contains(postReply.getId());
440 lock.readLock().unlock();
445 public void setPostReplyKnown(PostReply postReply) {
446 lock.writeLock().lock();
448 knownPostReplies.add(postReply.getId());
450 lock.writeLock().unlock();
455 public void storePostReply(PostReply postReply) {
456 lock.writeLock().lock();
458 allPostReplies.put(postReply.getId(), postReply);
459 postReplies.put(postReply.getPostId(), postReply);
461 lock.writeLock().unlock();
466 public void storePostReplies(Sone sone, Collection<PostReply> postReplies) {
467 checkNotNull(sone, "sone must not be null");
468 /* verify that all posts are from the same Sone. */
469 for (PostReply postReply : postReplies) {
470 if (!sone.equals(postReply.getSone())) {
471 throw new IllegalArgumentException(String.format("PostReply from different Sone found: %s", postReply));
475 lock.writeLock().lock();
477 /* remove all post replies of the Sone. */
478 for (PostReply postReply : getRepliesFrom(sone.getId())) {
479 removePostReply(postReply);
481 for (PostReply postReply : postReplies) {
482 allPostReplies.put(postReply.getId(), postReply);
483 sonePostReplies.put(postReply.getSone().getId(), postReply);
484 this.postReplies.put(postReply.getPostId(), postReply);
487 lock.writeLock().unlock();
492 public void removePostReply(PostReply postReply) {
493 lock.writeLock().lock();
495 allPostReplies.remove(postReply.getId());
496 postReplies.remove(postReply.getPostId(), postReply);
498 lock.writeLock().unlock();
503 public void removePostReplies(Sone sone) {
504 checkNotNull(sone, "sone must not be null");
506 lock.writeLock().lock();
508 for (PostReply postReply : sone.getReplies()) {
509 removePostReply(postReply);
512 lock.writeLock().unlock();
517 // ALBUMPROVDER METHODS
521 public Optional<Album> getAlbum(String albumId) {
522 lock.readLock().lock();
524 return fromNullable(allAlbums.get(albumId));
526 lock.readLock().unlock();
531 public List<Album> getAlbums(Album parent) {
532 lock.readLock().lock();
534 return from(albumChildren.get(parent.getId())).transformAndConcat(getAlbum()).toList();
536 lock.readLock().unlock();
541 public void moveUp(Album album) {
542 lock.writeLock().lock();
544 List<String> albums = albumChildren.get(album.getParent().getId());
545 int currentIndex = albums.indexOf(album.getId());
546 if (currentIndex == 0) {
549 albums.remove(album.getId());
550 albums.add(currentIndex - 1, album.getId());
552 lock.writeLock().unlock();
557 public void moveDown(Album album) {
558 lock.writeLock().lock();
560 List<String> albums = albumChildren.get(album.getParent().getId());
561 int currentIndex = albums.indexOf(album.getId());
562 if (currentIndex == (albums.size() - 1)) {
565 albums.remove(album.getId());
566 albums.add(currentIndex + 1, album.getId());
568 lock.writeLock().unlock();
573 // ALBUMSTORE METHODS
577 public void storeAlbum(Album album) {
578 lock.writeLock().lock();
580 allAlbums.put(album.getId(), album);
581 if (!album.isRoot()) {
582 albumChildren.put(album.getParent().getId(), album.getId());
585 lock.writeLock().unlock();
590 public void removeAlbum(Album album) {
591 lock.writeLock().lock();
593 allAlbums.remove(album.getId());
594 albumChildren.remove(album.getParent().getId(), album.getId());
596 lock.writeLock().unlock();
601 // IMAGEPROVIDER METHODS
605 public Optional<Image> getImage(String imageId) {
606 lock.readLock().lock();
608 return fromNullable(allImages.get(imageId));
610 lock.readLock().unlock();
615 public List<Image> getImages(Album parent) {
616 lock.readLock().lock();
618 return from(albumImages.get(parent.getId())).transformAndConcat(getImage()).toList();
620 lock.readLock().unlock();
625 public void moveUp(Image image) {
626 lock.writeLock().lock();
628 List<String> images = albumImages.get(image.getAlbum().getId());
629 int currentIndex = images.indexOf(image.getId());
630 if (currentIndex == 0) {
633 images.remove(image.getId());
634 images.add(currentIndex - 1, image.getId());
636 lock.writeLock().unlock();
641 public void moveDown(Image image) {
642 lock.writeLock().lock();
644 List<String> images = albumChildren.get(image.getAlbum().getId());
645 int currentIndex = images.indexOf(image.getId());
646 if (currentIndex == (images.size() - 1)) {
649 images.remove(image.getId());
650 images.add(currentIndex + 1, image.getId());
652 lock.writeLock().unlock();
657 // IMAGESTORE METHODS
661 public void storeImage(Image image) {
662 lock.writeLock().lock();
664 allImages.put(image.getId(), image);
665 albumImages.put(image.getAlbum().getId(), image.getId());
667 lock.writeLock().unlock();
672 public void removeImage(Image image) {
673 lock.writeLock().lock();
675 allImages.remove(image.getId());
676 albumImages.remove(image.getAlbum().getId(), image.getId());
678 lock.writeLock().unlock();
683 // PACKAGE-PRIVATE METHODS
687 * Returns whether the given post is known.
691 * @return {@code true} if the post is known, {@code false} otherwise
693 boolean isPostKnown(Post post) {
694 lock.readLock().lock();
696 return knownPosts.contains(post.getId());
698 lock.readLock().unlock();
703 * Sets whether the given post is known.
708 * {@code true} if the post is known, {@code false} otherwise
710 void setPostKnown(Post post, boolean known) {
711 lock.writeLock().lock();
714 knownPosts.add(post.getId());
716 knownPosts.remove(post.getId());
719 lock.writeLock().unlock();
727 /** Loads the known posts. */
728 private void loadKnownPosts() {
729 lock.writeLock().lock();
733 String knownPostId = configuration.getStringValue("KnownPosts/" + postCounter++ + "/ID").getValue(null);
734 if (knownPostId == null) {
737 knownPosts.add(knownPostId);
740 lock.writeLock().unlock();
745 * Saves the known posts to the configuration.
747 * @throws DatabaseException
748 * if a configuration error occurs
750 private void saveKnownPosts() throws DatabaseException {
751 lock.readLock().lock();
754 for (String knownPostId : knownPosts) {
755 configuration.getStringValue("KnownPosts/" + postCounter++ + "/ID").setValue(knownPostId);
757 configuration.getStringValue("KnownPosts/" + postCounter + "/ID").setValue(null);
758 } catch (ConfigurationException ce1) {
759 throw new DatabaseException("Could not save database.", ce1);
761 lock.readLock().unlock();
766 * Returns all replies by the given Sone.
770 * @return The post replies of the Sone, sorted by time (newest first)
772 private Collection<PostReply> getRepliesFrom(String id) {
773 lock.readLock().lock();
775 if (sonePostReplies.containsKey(id)) {
776 return Collections.unmodifiableCollection(sonePostReplies.get(id));
778 return Collections.emptySet();
780 lock.readLock().unlock();
784 /** Loads the known post replies. */
785 private void loadKnownPostReplies() {
786 lock.writeLock().lock();
788 int replyCounter = 0;
790 String knownReplyId = configuration.getStringValue("KnownReplies/" + replyCounter++ + "/ID").getValue(null);
791 if (knownReplyId == null) {
794 knownPostReplies.add(knownReplyId);
797 lock.writeLock().unlock();
802 * Saves the known post replies to the configuration.
804 * @throws DatabaseException
805 * if a configuration error occurs
807 private void saveKnownPostReplies() throws DatabaseException {
808 lock.readLock().lock();
810 int replyCounter = 0;
811 for (String knownReplyId : knownPostReplies) {
812 configuration.getStringValue("KnownReplies/" + replyCounter++ + "/ID").setValue(knownReplyId);
814 configuration.getStringValue("KnownReplies/" + replyCounter + "/ID").setValue(null);
815 } catch (ConfigurationException ce1) {
816 throw new DatabaseException("Could not save database.", ce1);
818 lock.readLock().unlock();
822 private Function<String, Iterable<Album>> getAlbum() {
823 return new Function<String, Iterable<Album>>() {
825 public Iterable<Album> apply(String input) {
826 return (input == null) ? Collections.<Album>emptyList() : getAlbum(input).asSet();
831 private Function<String, Iterable<Image>> getImage() {
832 return new Function<String, Iterable<Image>>() {
834 public Iterable<Image> apply(String input) {
835 return (input == null) ? Collections.<Image>emptyList() : getImage(input).asSet();