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> likedPostsBySone = HashMultimap.create();
88 private final SetMultimap<String, String> postLikingSones = HashMultimap.create();
90 /** All posts by their recipient. */
91 private final Multimap<String, Post> recipientPosts = HashMultimap.create();
93 /** Whether posts are known. */
94 private final Set<String> knownPosts = new HashSet<String>();
96 /** All post replies by their ID. */
97 private final Map<String, PostReply> allPostReplies = new HashMap<String, PostReply>();
99 /** Replies sorted by Sone. */
100 private final SortedSetMultimap<String, PostReply> sonePostReplies = TreeMultimap.create(new Comparator<String>() {
103 public int compare(String leftString, String rightString) {
104 return leftString.compareTo(rightString);
106 }, PostReply.TIME_COMPARATOR);
108 /** Replies by post. */
109 private final SortedSetMultimap<String, PostReply> postReplies = TreeMultimap.create(new Comparator<String>() {
112 public int compare(String leftString, String rightString) {
113 return leftString.compareTo(rightString);
115 }, PostReply.TIME_COMPARATOR);
117 /** Whether post replies are known. */
118 private final Set<String> knownPostReplies = new HashSet<String>();
120 private final Map<String, Album> allAlbums = new HashMap<String, Album>();
121 private final ListMultimap<String, String> albumChildren = ArrayListMultimap.create();
122 private final ListMultimap<String, String> albumImages = ArrayListMultimap.create();
124 private final Map<String, Image> allImages = new HashMap<String, Image>();
127 * Creates a new memory database.
129 * @param configuration
130 * The configuration for loading and saving elements
133 public MemoryDatabase(Configuration configuration) {
134 this.configuration = configuration;
142 public void save() throws DatabaseException {
144 saveKnownPostReplies();
152 protected void doStart() {
154 loadKnownPostReplies();
159 protected void doStop() {
163 } catch (DatabaseException de1) {
169 public Optional<Identity> getIdentity(String identityId) {
170 lock.readLock().lock();
172 return fromNullable(identities.get(identityId));
174 lock.readLock().unlock();
179 public Function<String, Optional<Sone>> getSone() {
180 return new Function<String, Optional<Sone>>() {
182 public Optional<Sone> apply(String soneId) {
183 return (soneId == null) ? Optional.<Sone>absent() : getSone(soneId);
189 public Optional<Sone> getSone(String soneId) {
190 lock.readLock().lock();
192 return fromNullable(sones.get(soneId));
194 lock.readLock().unlock();
199 public Collection<Sone> getSones() {
200 lock.readLock().lock();
202 return Collections.unmodifiableCollection(sones.values());
204 lock.readLock().unlock();
209 public Collection<Sone> getLocalSones() {
210 lock.readLock().lock();
212 return from(getSones()).filter(LOCAL_SONE_FILTER).toSet();
214 lock.readLock().unlock();
219 public Collection<Sone> getRemoteSones() {
220 lock.readLock().lock();
222 return from(getSones()).filter(not(LOCAL_SONE_FILTER)).toSet();
224 lock.readLock().unlock();
229 public SoneBuilder newSoneBuilder() {
230 return new DefaultSoneBuilder(this) {
232 public Sone build(Optional<SoneCreated> soneCreated) throws IllegalStateException {
233 Sone sone = super.build(soneCreated);
234 lock.writeLock().lock();
236 sones.put(sone.getId(), sone);
238 lock.writeLock().unlock();
246 // POSTPROVIDER METHODS
250 public Function<String, Optional<Post>> getPost() {
251 return new Function<String, Optional<Post>>() {
253 public Optional<Post> apply(String postId) {
254 return (postId == null) ? Optional.<Post>absent() : getPost(postId);
260 public Optional<Post> getPost(String postId) {
261 lock.readLock().lock();
263 return fromNullable(allPosts.get(postId));
265 lock.readLock().unlock();
270 public Collection<Post> getPosts(String soneId) {
271 lock.readLock().lock();
273 return new HashSet<Post>(sonePosts.get(soneId));
275 lock.readLock().unlock();
280 public Collection<Post> getDirectedPosts(String recipientId) {
281 lock.readLock().lock();
283 Collection<Post> posts = recipientPosts.get(recipientId);
284 return (posts == null) ? Collections.<Post>emptySet() : new HashSet<Post>(posts);
286 lock.readLock().unlock();
291 public void likePost(Post post, Sone localSone) {
292 lock.writeLock().lock();
294 likedPostsBySone.put(localSone.getId(), post.getId());
295 postLikingSones.put(post.getId(), localSone.getId());
297 lock.writeLock().unlock();
302 public void unlikePost(Post post, Sone localSone) {
303 lock.writeLock().lock();
305 likedPostsBySone.remove(localSone.getId(), post.getId());
306 postLikingSones.remove(post.getId(), localSone.getId());
308 lock.writeLock().unlock();
313 public Set<Sone> getLikes(Post post) {
314 lock.readLock().lock();
316 return from(postLikingSones.get(post.getId())).transform(getSone()).transformAndConcat(this.<Sone>unwrap()).toSet();
318 lock.readLock().unlock();
327 public void storePost(Post post) {
328 checkNotNull(post, "post must not be null");
329 lock.writeLock().lock();
331 allPosts.put(post.getId(), post);
332 sonePosts.put(post.getSone().getId(), post);
333 if (post.getRecipientId().isPresent()) {
334 recipientPosts.put(post.getRecipientId().get(), post);
337 lock.writeLock().unlock();
342 public void removePost(Post post) {
343 checkNotNull(post, "post must not be null");
344 lock.writeLock().lock();
346 allPosts.remove(post.getId());
347 sonePosts.remove(post.getSone().getId(), post);
348 if (post.getRecipientId().isPresent()) {
349 recipientPosts.remove(post.getRecipientId().get(), post);
351 post.getSone().removePost(post);
353 lock.writeLock().unlock();
358 public void storePosts(Sone sone, Collection<Post> posts) throws IllegalArgumentException {
359 checkNotNull(sone, "sone must not be null");
360 /* verify that all posts are from the same Sone. */
361 for (Post post : posts) {
362 if (!sone.equals(post.getSone())) {
363 throw new IllegalArgumentException(String.format("Post from different Sone found: %s", post));
367 lock.writeLock().lock();
369 /* remove all posts by the Sone. */
370 sonePosts.removeAll(sone.getId());
371 for (Post post : posts) {
372 allPosts.remove(post.getId());
373 if (post.getRecipientId().isPresent()) {
374 recipientPosts.remove(post.getRecipientId().get(), post);
379 sonePosts.putAll(sone.getId(), posts);
380 for (Post post : posts) {
381 allPosts.put(post.getId(), post);
382 if (post.getRecipientId().isPresent()) {
383 recipientPosts.put(post.getRecipientId().get(), post);
387 lock.writeLock().unlock();
392 public void removePosts(Sone sone) {
393 checkNotNull(sone, "sone must not be null");
394 lock.writeLock().lock();
396 /* remove all posts by the Sone. */
397 sonePosts.removeAll(sone.getId());
398 for (Post post : sone.getPosts()) {
399 allPosts.remove(post.getId());
400 if (post.getRecipientId().isPresent()) {
401 recipientPosts.remove(post.getRecipientId().get(), post);
405 lock.writeLock().unlock();
410 // POSTREPLYPROVIDER METHODS
414 public Optional<PostReply> getPostReply(String id) {
415 lock.readLock().lock();
417 return fromNullable(allPostReplies.get(id));
419 lock.readLock().unlock();
424 public List<PostReply> getReplies(String postId) {
425 lock.readLock().lock();
427 if (!postReplies.containsKey(postId)) {
430 return new ArrayList<PostReply>(postReplies.get(postId));
432 lock.readLock().unlock();
437 // POSTREPLYSTORE METHODS
441 * Returns whether the given post reply is known.
445 * @return {@code true} if the given post reply is known, {@code false}
448 public boolean isPostReplyKnown(PostReply postReply) {
449 lock.readLock().lock();
451 return knownPostReplies.contains(postReply.getId());
453 lock.readLock().unlock();
458 public void setPostReplyKnown(PostReply postReply) {
459 lock.writeLock().lock();
461 knownPostReplies.add(postReply.getId());
463 lock.writeLock().unlock();
468 public void storePostReply(PostReply postReply) {
469 lock.writeLock().lock();
471 allPostReplies.put(postReply.getId(), postReply);
472 postReplies.put(postReply.getPostId(), postReply);
474 lock.writeLock().unlock();
479 public void storePostReplies(Sone sone, Collection<PostReply> postReplies) {
480 checkNotNull(sone, "sone must not be null");
481 /* verify that all posts are from the same Sone. */
482 for (PostReply postReply : postReplies) {
483 if (!sone.equals(postReply.getSone())) {
484 throw new IllegalArgumentException(String.format("PostReply from different Sone found: %s", postReply));
488 lock.writeLock().lock();
490 /* remove all post replies of the Sone. */
491 for (PostReply postReply : getRepliesFrom(sone.getId())) {
492 removePostReply(postReply);
494 for (PostReply postReply : postReplies) {
495 allPostReplies.put(postReply.getId(), postReply);
496 sonePostReplies.put(postReply.getSone().getId(), postReply);
497 this.postReplies.put(postReply.getPostId(), postReply);
500 lock.writeLock().unlock();
505 public void removePostReply(PostReply postReply) {
506 lock.writeLock().lock();
508 allPostReplies.remove(postReply.getId());
509 postReplies.remove(postReply.getPostId(), postReply);
511 lock.writeLock().unlock();
516 public void removePostReplies(Sone sone) {
517 checkNotNull(sone, "sone must not be null");
519 lock.writeLock().lock();
521 for (PostReply postReply : sone.getReplies()) {
522 removePostReply(postReply);
525 lock.writeLock().unlock();
530 // ALBUMPROVDER METHODS
534 public Optional<Album> getAlbum(String albumId) {
535 lock.readLock().lock();
537 return fromNullable(allAlbums.get(albumId));
539 lock.readLock().unlock();
544 public List<Album> getAlbums(Album parent) {
545 lock.readLock().lock();
547 return from(albumChildren.get(parent.getId())).transformAndConcat(getAlbum()).toList();
549 lock.readLock().unlock();
554 public void moveUp(Album album) {
555 lock.writeLock().lock();
557 List<String> albums = albumChildren.get(album.getParent().getId());
558 int currentIndex = albums.indexOf(album.getId());
559 if (currentIndex == 0) {
562 albums.remove(album.getId());
563 albums.add(currentIndex - 1, album.getId());
565 lock.writeLock().unlock();
570 public void moveDown(Album album) {
571 lock.writeLock().lock();
573 List<String> albums = albumChildren.get(album.getParent().getId());
574 int currentIndex = albums.indexOf(album.getId());
575 if (currentIndex == (albums.size() - 1)) {
578 albums.remove(album.getId());
579 albums.add(currentIndex + 1, album.getId());
581 lock.writeLock().unlock();
586 // ALBUMSTORE METHODS
590 public void storeAlbum(Album album) {
591 lock.writeLock().lock();
593 allAlbums.put(album.getId(), album);
594 if (!album.isRoot()) {
595 albumChildren.put(album.getParent().getId(), album.getId());
598 lock.writeLock().unlock();
603 public void removeAlbum(Album album) {
604 lock.writeLock().lock();
606 allAlbums.remove(album.getId());
607 albumChildren.remove(album.getParent().getId(), album.getId());
609 lock.writeLock().unlock();
614 // IMAGEPROVIDER METHODS
618 public Optional<Image> getImage(String imageId) {
619 lock.readLock().lock();
621 return fromNullable(allImages.get(imageId));
623 lock.readLock().unlock();
628 public List<Image> getImages(Album parent) {
629 lock.readLock().lock();
631 return from(albumImages.get(parent.getId())).transformAndConcat(getImage()).toList();
633 lock.readLock().unlock();
638 public void moveUp(Image image) {
639 lock.writeLock().lock();
641 List<String> images = albumImages.get(image.getAlbum().getId());
642 int currentIndex = images.indexOf(image.getId());
643 if (currentIndex == 0) {
646 images.remove(image.getId());
647 images.add(currentIndex - 1, image.getId());
649 lock.writeLock().unlock();
654 public void moveDown(Image image) {
655 lock.writeLock().lock();
657 List<String> images = albumChildren.get(image.getAlbum().getId());
658 int currentIndex = images.indexOf(image.getId());
659 if (currentIndex == (images.size() - 1)) {
662 images.remove(image.getId());
663 images.add(currentIndex + 1, image.getId());
665 lock.writeLock().unlock();
670 // IMAGESTORE METHODS
674 public void storeImage(Image image) {
675 lock.writeLock().lock();
677 allImages.put(image.getId(), image);
678 albumImages.put(image.getAlbum().getId(), image.getId());
680 lock.writeLock().unlock();
685 public void removeImage(Image image) {
686 lock.writeLock().lock();
688 allImages.remove(image.getId());
689 albumImages.remove(image.getAlbum().getId(), image.getId());
691 lock.writeLock().unlock();
696 // PACKAGE-PRIVATE METHODS
700 * Returns whether the given post is known.
704 * @return {@code true} if the post is known, {@code false} otherwise
706 boolean isPostKnown(Post post) {
707 lock.readLock().lock();
709 return knownPosts.contains(post.getId());
711 lock.readLock().unlock();
716 * Sets whether the given post is known.
721 * {@code true} if the post is known, {@code false} otherwise
723 void setPostKnown(Post post, boolean known) {
724 lock.writeLock().lock();
727 knownPosts.add(post.getId());
729 knownPosts.remove(post.getId());
732 lock.writeLock().unlock();
740 /** Loads the known posts. */
741 private void loadKnownPosts() {
742 lock.writeLock().lock();
746 String knownPostId = configuration.getStringValue("KnownPosts/" + postCounter++ + "/ID").getValue(null);
747 if (knownPostId == null) {
750 knownPosts.add(knownPostId);
753 lock.writeLock().unlock();
758 * Saves the known posts to the configuration.
760 * @throws DatabaseException
761 * if a configuration error occurs
763 private void saveKnownPosts() throws DatabaseException {
764 lock.readLock().lock();
767 for (String knownPostId : knownPosts) {
768 configuration.getStringValue("KnownPosts/" + postCounter++ + "/ID").setValue(knownPostId);
770 configuration.getStringValue("KnownPosts/" + postCounter + "/ID").setValue(null);
771 } catch (ConfigurationException ce1) {
772 throw new DatabaseException("Could not save database.", ce1);
774 lock.readLock().unlock();
779 * Returns all replies by the given Sone.
783 * @return The post replies of the Sone, sorted by time (newest first)
785 private Collection<PostReply> getRepliesFrom(String id) {
786 lock.readLock().lock();
788 if (sonePostReplies.containsKey(id)) {
789 return Collections.unmodifiableCollection(sonePostReplies.get(id));
791 return Collections.emptySet();
793 lock.readLock().unlock();
797 /** Loads the known post replies. */
798 private void loadKnownPostReplies() {
799 lock.writeLock().lock();
801 int replyCounter = 0;
803 String knownReplyId = configuration.getStringValue("KnownReplies/" + replyCounter++ + "/ID").getValue(null);
804 if (knownReplyId == null) {
807 knownPostReplies.add(knownReplyId);
810 lock.writeLock().unlock();
815 * Saves the known post replies to the configuration.
817 * @throws DatabaseException
818 * if a configuration error occurs
820 private void saveKnownPostReplies() throws DatabaseException {
821 lock.readLock().lock();
823 int replyCounter = 0;
824 for (String knownReplyId : knownPostReplies) {
825 configuration.getStringValue("KnownReplies/" + replyCounter++ + "/ID").setValue(knownReplyId);
827 configuration.getStringValue("KnownReplies/" + replyCounter + "/ID").setValue(null);
828 } catch (ConfigurationException ce1) {
829 throw new DatabaseException("Could not save database.", ce1);
831 lock.readLock().unlock();
835 private Function<String, Iterable<Album>> getAlbum() {
836 return new Function<String, Iterable<Album>>() {
838 public Iterable<Album> apply(String input) {
839 return (input == null) ? Collections.<Album>emptyList() : getAlbum(input).asSet();
844 private Function<String, Iterable<Image>> getImage() {
845 return new Function<String, Iterable<Image>>() {
847 public Iterable<Image> apply(String input) {
848 return (input == null) ? Collections.<Image>emptyList() : getImage(input).asSet();
853 private static <T> Function<Optional<T>, Iterable<T>> unwrap() {
854 return new Function<Optional<T>, Iterable<T>>() {
856 public Iterable<T> apply(Optional<T> input) {
857 return (input == null) ? Collections.<T>emptyList() : input.asSet();