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.SoneProvider;
55 import net.pterodactylus.util.config.Configuration;
56 import net.pterodactylus.util.config.ConfigurationException;
58 import com.google.common.base.Optional;
59 import com.google.common.base.Predicate;
60 import com.google.common.collect.HashMultimap;
61 import com.google.common.collect.Multimap;
62 import com.google.common.collect.SortedSetMultimap;
63 import com.google.common.collect.TreeMultimap;
64 import com.google.common.util.concurrent.AbstractService;
65 import com.google.inject.Inject;
66 import com.google.inject.Singleton;
69 * Memory-based {@link PostDatabase} implementation.
71 * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
74 public class MemoryDatabase extends AbstractService implements Database {
77 private final ReadWriteLock lock = new ReentrantReadWriteLock();
79 /** The Sone provider. */
80 private final SoneProvider soneProvider;
82 /** The configuration. */
83 private final Configuration configuration;
85 private final Map<String, Sone> allSones = new HashMap<String, Sone>();
87 /** All posts by their ID. */
88 private final Map<String, Post> allPosts = new HashMap<String, Post>();
90 /** All posts by their Sones. */
91 private final Multimap<String, Post> sonePosts = 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);
108 /** Whether post replies are known. */
109 private final Set<String> knownPostReplies = new HashSet<String>();
111 private final Map<String, Album> allAlbums = new HashMap<String, Album>();
112 private final Multimap<String, Album> soneAlbums = HashMultimap.create();
114 private final Map<String, Image> allImages = new HashMap<String, Image>();
115 private final Multimap<String, Image> soneImages = HashMultimap.create();
118 * Creates a new memory database.
120 * @param soneProvider
122 * @param configuration
123 * The configuration for loading and saving elements
126 public MemoryDatabase(SoneProvider soneProvider, Configuration configuration) {
127 this.soneProvider = soneProvider;
128 this.configuration = configuration;
136 * Saves the database.
138 * @throws DatabaseException
139 * if an error occurs while saving
142 public void save() throws DatabaseException {
144 saveKnownPostReplies();
151 /** {@inheritDocs} */
153 protected void doStart() {
155 loadKnownPostReplies();
159 /** {@inheritDocs} */
161 protected void doStop() {
165 } catch (DatabaseException de1) {
171 public void storeSone(Sone sone) {
172 lock.writeLock().lock();
174 Collection<Post> removedPosts = sonePosts.removeAll(sone.getId());
175 for (Post removedPost : removedPosts) {
176 allPosts.remove(removedPost.getId());
178 Collection<PostReply> removedPostReplies =
179 sonePostReplies.removeAll(sone.getId());
180 for (PostReply removedPostReply : removedPostReplies) {
181 allPostReplies.remove(removedPostReply.getId());
183 Collection<Album> removedAlbums =
184 soneAlbums.removeAll(sone.getId());
185 for (Album removedAlbum : removedAlbums) {
186 allAlbums.remove(removedAlbum.getId());
188 Collection<Image> removedImages =
189 soneImages.removeAll(sone.getId());
190 for (Image removedImage : removedImages) {
191 allImages.remove(removedImage.getId());
194 allSones.put(sone.getId(), sone);
195 sonePosts.putAll(sone.getId(), sone.getPosts());
196 for (Post post : sone.getPosts()) {
197 allPosts.put(post.getId(), post);
199 sonePostReplies.putAll(sone.getId(), sone.getReplies());
200 for (PostReply postReply : sone.getReplies()) {
201 allPostReplies.put(postReply.getId(), postReply);
203 soneAlbums.putAll(sone.getId(), toAllAlbums.apply(sone));
204 for (Album album : toAllAlbums.apply(sone)) {
205 allAlbums.put(album.getId(), album);
207 soneImages.putAll(sone.getId(), toAllImages.apply(sone));
208 for (Image image : toAllImages.apply(sone)) {
209 allImages.put(image.getId(), image);
212 lock.writeLock().unlock();
217 public Optional<Sone> getSone(String soneId) {
218 lock.readLock().lock();
220 return fromNullable(allSones.get(soneId));
222 lock.readLock().unlock();
227 public Collection<Sone> getSones() {
228 lock.readLock().lock();
230 return unmodifiableCollection(allSones.values());
232 lock.readLock().unlock();
237 public Collection<Sone> getLocalSones() {
238 lock.readLock().lock();
240 return from(allSones.values()).filter(LOCAL_SONE_FILTER).toSet();
242 lock.readLock().unlock();
247 public Collection<Sone> getRemoteSones() {
248 lock.readLock().lock();
250 return from(allSones.values())
251 .filter(not(LOCAL_SONE_FILTER)) .toSet();
253 lock.readLock().unlock();
258 // POSTPROVIDER METHODS
261 /** {@inheritDocs} */
263 public Optional<Post> getPost(String postId) {
264 lock.readLock().lock();
266 return fromNullable(allPosts.get(postId));
268 lock.readLock().unlock();
272 /** {@inheritDocs} */
274 public Collection<Post> getPosts(String soneId) {
275 return new HashSet<Post>(getPostsFrom(soneId));
278 /** {@inheritDocs} */
280 public Collection<Post> getDirectedPosts(final String recipientId) {
281 lock.readLock().lock();
283 return from(sonePosts.values()).filter(new Predicate<Post>() {
285 public boolean apply(Post post) {
286 return post.getRecipientId().asSet().contains(recipientId);
290 lock.readLock().unlock();
295 // POSTBUILDERFACTORY METHODS
298 /** {@inheritDocs} */
300 public PostBuilder newPostBuilder() {
301 return new MemoryPostBuilder(this, soneProvider);
308 /** {@inheritDocs} */
310 public void storePost(Post post) {
311 checkNotNull(post, "post must not be null");
312 lock.writeLock().lock();
314 allPosts.put(post.getId(), post);
315 getPostsFrom(post.getSone().getId()).add(post);
317 lock.writeLock().unlock();
321 /** {@inheritDocs} */
323 public void removePost(Post post) {
324 checkNotNull(post, "post must not be null");
325 lock.writeLock().lock();
327 allPosts.remove(post.getId());
328 getPostsFrom(post.getSone().getId()).remove(post);
329 post.getSone().removePost(post);
331 lock.writeLock().unlock();
335 /** {@inheritDocs} */
337 public void storePosts(Sone sone, Collection<Post> posts) throws IllegalArgumentException {
338 checkNotNull(sone, "sone must not be null");
339 /* verify that all posts are from the same Sone. */
340 for (Post post : posts) {
341 if (!sone.equals(post.getSone())) {
342 throw new IllegalArgumentException(String.format("Post from different Sone found: %s", post));
346 lock.writeLock().lock();
348 /* remove all posts by the Sone. */
349 Collection<Post> oldPosts = getPostsFrom(sone.getId());
350 for (Post post : oldPosts) {
351 allPosts.remove(post.getId());
355 getPostsFrom(sone.getId()).addAll(posts);
356 for (Post post : posts) {
357 allPosts.put(post.getId(), post);
360 lock.writeLock().unlock();
364 /** {@inheritDocs} */
366 public void removePosts(Sone sone) {
367 checkNotNull(sone, "sone must not be null");
368 lock.writeLock().lock();
370 /* remove all posts by the Sone. */
371 getPostsFrom(sone.getId()).clear();
372 for (Post post : sone.getPosts()) {
373 allPosts.remove(post.getId());
376 lock.writeLock().unlock();
381 // POSTREPLYPROVIDER METHODS
384 /** {@inheritDocs} */
386 public Optional<PostReply> getPostReply(String id) {
387 lock.readLock().lock();
389 return fromNullable(allPostReplies.get(id));
391 lock.readLock().unlock();
395 /** {@inheritDocs} */
397 public List<PostReply> getReplies(final String postId) {
398 lock.readLock().lock();
400 return from(allPostReplies.values())
401 .filter(new Predicate<PostReply>() {
403 public boolean apply(PostReply postReply) {
404 return postReply.getPostId().equals(postId);
406 }).toSortedList(TIME_COMPARATOR);
408 lock.readLock().unlock();
413 // POSTREPLYBUILDERFACTORY METHODS
416 /** {@inheritDocs} */
418 public PostReplyBuilder newPostReplyBuilder() {
419 return new MemoryPostReplyBuilder(this, soneProvider);
423 // POSTREPLYSTORE METHODS
426 /** {@inheritDocs} */
428 public void storePostReply(PostReply postReply) {
429 lock.writeLock().lock();
431 allPostReplies.put(postReply.getId(), postReply);
433 lock.writeLock().unlock();
437 /** {@inheritDocs} */
439 public void storePostReplies(Sone sone, Collection<PostReply> postReplies) {
440 checkNotNull(sone, "sone must not be null");
441 /* verify that all posts are from the same Sone. */
442 for (PostReply postReply : postReplies) {
443 if (!sone.equals(postReply.getSone())) {
444 throw new IllegalArgumentException(String.format("PostReply from different Sone found: %s", postReply));
448 lock.writeLock().lock();
450 /* remove all post replies of the Sone. */
451 for (PostReply postReply : getRepliesFrom(sone.getId())) {
452 removePostReply(postReply);
454 for (PostReply postReply : postReplies) {
455 allPostReplies.put(postReply.getId(), postReply);
456 sonePostReplies.put(postReply.getSone().getId(), postReply);
459 lock.writeLock().unlock();
463 /** {@inheritDocs} */
465 public void removePostReply(PostReply postReply) {
466 lock.writeLock().lock();
468 allPostReplies.remove(postReply.getId());
470 lock.writeLock().unlock();
474 /** {@inheritDocs} */
476 public void removePostReplies(Sone sone) {
477 checkNotNull(sone, "sone must not be null");
479 lock.writeLock().lock();
481 for (PostReply postReply : sone.getReplies()) {
482 removePostReply(postReply);
485 lock.writeLock().unlock();
490 // ALBUMPROVDER METHODS
494 public Optional<Album> getAlbum(String albumId) {
495 lock.readLock().lock();
497 return fromNullable(allAlbums.get(albumId));
499 lock.readLock().unlock();
504 // ALBUMBUILDERFACTORY METHODS
508 public AlbumBuilder newAlbumBuilder() {
509 return new AlbumBuilderImpl();
513 // ALBUMSTORE METHODS
517 public void storeAlbum(Album album) {
518 lock.writeLock().lock();
520 allAlbums.put(album.getId(), album);
521 soneAlbums.put(album.getSone().getId(), album);
523 lock.writeLock().unlock();
528 public void removeAlbum(Album album) {
529 lock.writeLock().lock();
531 allAlbums.remove(album.getId());
532 soneAlbums.remove(album.getSone().getId(), album);
534 lock.writeLock().unlock();
539 // IMAGEPROVIDER METHODS
543 public Optional<Image> getImage(String imageId) {
544 lock.readLock().lock();
546 return fromNullable(allImages.get(imageId));
548 lock.readLock().unlock();
553 // IMAGEBUILDERFACTORY METHODS
557 public ImageBuilder newImageBuilder() {
558 return new ImageBuilderImpl();
562 // IMAGESTORE METHODS
566 public void storeImage(Image image) {
567 lock.writeLock().lock();
569 allImages.put(image.getId(), image);
570 soneImages.put(image.getSone().getId(), image);
572 lock.writeLock().unlock();
577 public void removeImage(Image image) {
578 lock.writeLock().lock();
580 allImages.remove(image.getId());
581 soneImages.remove(image.getSone().getId(), image);
583 lock.writeLock().unlock();
588 // PACKAGE-PRIVATE METHODS
592 * Returns whether the given post is known.
596 * @return {@code true} if the post is known, {@code false} otherwise
598 boolean isPostKnown(Post post) {
599 lock.readLock().lock();
601 return knownPosts.contains(post.getId());
603 lock.readLock().unlock();
608 * Sets whether the given post is known.
613 * {@code true} if the post is known, {@code false} otherwise
615 void setPostKnown(Post post, boolean known) {
616 lock.writeLock().lock();
619 knownPosts.add(post.getId());
621 knownPosts.remove(post.getId());
624 lock.writeLock().unlock();
629 * Returns whether the given post reply is known.
633 * @return {@code true} if the given post reply is known, {@code false}
636 boolean isPostReplyKnown(PostReply postReply) {
637 lock.readLock().lock();
639 return knownPostReplies.contains(postReply.getId());
641 lock.readLock().unlock();
646 * Sets whether the given post reply is known.
651 * {@code true} if the post reply is known, {@code false} otherwise
653 void setPostReplyKnown(PostReply postReply, boolean known) {
654 lock.writeLock().lock();
657 knownPostReplies.add(postReply.getId());
659 knownPostReplies.remove(postReply.getId());
662 lock.writeLock().unlock();
671 * Gets all posts for the given Sone, creating a new collection if there is
675 * The ID of the Sone to get the posts for
678 private Collection<Post> getPostsFrom(String soneId) {
679 lock.readLock().lock();
681 return sonePosts.get(soneId);
683 lock.readLock().unlock();
687 /** Loads the known posts. */
688 private void loadKnownPosts() {
689 lock.writeLock().lock();
693 String knownPostId = configuration.getStringValue("KnownPosts/" + postCounter++ + "/ID").getValue(null);
694 if (knownPostId == null) {
697 knownPosts.add(knownPostId);
700 lock.writeLock().unlock();
705 * Saves the known posts to the configuration.
707 * @throws DatabaseException
708 * if a configuration error occurs
710 private void saveKnownPosts() throws DatabaseException {
711 lock.readLock().lock();
714 for (String knownPostId : knownPosts) {
715 configuration.getStringValue("KnownPosts/" + postCounter++ + "/ID").setValue(knownPostId);
717 configuration.getStringValue("KnownPosts/" + postCounter + "/ID").setValue(null);
718 } catch (ConfigurationException ce1) {
719 throw new DatabaseException("Could not save database.", ce1);
721 lock.readLock().unlock();
726 * Returns all replies by the given Sone.
730 * @return The post replies of the Sone, sorted by time (newest first)
732 private Collection<PostReply> getRepliesFrom(String id) {
733 lock.readLock().lock();
735 return unmodifiableCollection(sonePostReplies.get(id));
737 lock.readLock().unlock();
741 /** Loads the known post replies. */
742 private void loadKnownPostReplies() {
743 lock.writeLock().lock();
745 int replyCounter = 0;
747 String knownReplyId = configuration.getStringValue("KnownReplies/" + replyCounter++ + "/ID").getValue(null);
748 if (knownReplyId == null) {
751 knownPostReplies.add(knownReplyId);
754 lock.writeLock().unlock();
759 * Saves the known post replies to the configuration.
761 * @throws DatabaseException
762 * if a configuration error occurs
764 private void saveKnownPostReplies() throws DatabaseException {
765 lock.readLock().lock();
767 int replyCounter = 0;
768 for (String knownReplyId : knownPostReplies) {
769 configuration.getStringValue("KnownReplies/" + replyCounter++ + "/ID").setValue(knownReplyId);
771 configuration.getStringValue("KnownReplies/" + replyCounter + "/ID").setValue(null);
772 } catch (ConfigurationException ce1) {
773 throw new DatabaseException("Could not save database.", ce1);
775 lock.readLock().unlock();