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 net.pterodactylus.sone.data.Reply.TIME_COMPARATOR;
24 import java.util.ArrayList;
25 import java.util.Collection;
26 import java.util.Collections;
27 import java.util.Comparator;
28 import java.util.HashMap;
29 import java.util.HashSet;
30 import java.util.List;
33 import java.util.concurrent.locks.ReadWriteLock;
34 import java.util.concurrent.locks.ReentrantReadWriteLock;
36 import net.pterodactylus.sone.data.Album;
37 import net.pterodactylus.sone.data.Image;
38 import net.pterodactylus.sone.data.Post;
39 import net.pterodactylus.sone.data.PostReply;
40 import net.pterodactylus.sone.data.Sone;
41 import net.pterodactylus.sone.data.impl.AlbumBuilderImpl;
42 import net.pterodactylus.sone.data.impl.ImageBuilderImpl;
43 import net.pterodactylus.sone.database.AlbumBuilder;
44 import net.pterodactylus.sone.database.Database;
45 import net.pterodactylus.sone.database.DatabaseException;
46 import net.pterodactylus.sone.database.ImageBuilder;
47 import net.pterodactylus.sone.database.PostBuilder;
48 import net.pterodactylus.sone.database.PostDatabase;
49 import net.pterodactylus.sone.database.PostReplyBuilder;
50 import net.pterodactylus.sone.database.SoneProvider;
51 import net.pterodactylus.util.config.Configuration;
52 import net.pterodactylus.util.config.ConfigurationException;
54 import com.google.common.base.Optional;
55 import com.google.common.collect.HashMultimap;
56 import com.google.common.collect.Multimap;
57 import com.google.common.collect.SortedSetMultimap;
58 import com.google.common.collect.TreeMultimap;
59 import com.google.common.util.concurrent.AbstractService;
60 import com.google.inject.Inject;
63 * Memory-based {@link PostDatabase} implementation.
65 * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
67 public class MemoryDatabase extends AbstractService implements Database {
70 private final ReadWriteLock lock = new ReentrantReadWriteLock();
72 /** The Sone provider. */
73 private final SoneProvider soneProvider;
75 /** The configuration. */
76 private final Configuration configuration;
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 Multimap<String, Post> sonePosts = HashMultimap.create();
84 /** All posts by their recipient. */
85 private final Multimap<String, Post> recipientPosts = HashMultimap.create();
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);
102 /** Replies by post. */
103 private final SortedSetMultimap<String, PostReply> postReplies = TreeMultimap.create(new Comparator<String>() {
106 public int compare(String leftString, String rightString) {
107 return leftString.compareTo(rightString);
111 /** Whether post replies are known. */
112 private final Set<String> knownPostReplies = new HashSet<String>();
114 private final Map<String, Album> allAlbums = new HashMap<String, Album>();
116 private final Map<String, Image> allImages = new HashMap<String, Image>();
119 * Creates a new memory database.
121 * @param soneProvider
123 * @param configuration
124 * The configuration for loading and saving elements
127 public MemoryDatabase(SoneProvider soneProvider, Configuration configuration) {
128 this.soneProvider = soneProvider;
129 this.configuration = configuration;
137 * Saves the database.
139 * @throws DatabaseException
140 * if an error occurs while saving
143 public void save() throws DatabaseException {
145 saveKnownPostReplies();
152 /** {@inheritDocs} */
154 protected void doStart() {
156 loadKnownPostReplies();
160 /** {@inheritDocs} */
162 protected void doStop() {
166 } catch (DatabaseException de1) {
172 // POSTPROVIDER METHODS
175 /** {@inheritDocs} */
177 public Optional<Post> getPost(String postId) {
178 lock.readLock().lock();
180 return fromNullable(allPosts.get(postId));
182 lock.readLock().unlock();
186 /** {@inheritDocs} */
188 public Collection<Post> getPosts(String soneId) {
189 return new HashSet<Post>(getPostsFrom(soneId));
192 /** {@inheritDocs} */
194 public Collection<Post> getDirectedPosts(String recipientId) {
195 lock.readLock().lock();
197 Collection<Post> posts = recipientPosts.get(recipientId);
198 return (posts == null) ? Collections.<Post>emptySet() : new HashSet<Post>(posts);
200 lock.readLock().unlock();
205 // POSTBUILDERFACTORY METHODS
208 /** {@inheritDocs} */
210 public PostBuilder newPostBuilder() {
211 return new MemoryPostBuilder(this, soneProvider);
218 /** {@inheritDocs} */
220 public void storePost(Post post) {
221 checkNotNull(post, "post must not be null");
222 lock.writeLock().lock();
224 allPosts.put(post.getId(), post);
225 getPostsFrom(post.getSone().getId()).add(post);
226 if (post.getRecipientId().isPresent()) {
227 getPostsTo(post.getRecipientId().get()).add(post);
230 lock.writeLock().unlock();
234 /** {@inheritDocs} */
236 public void removePost(Post post) {
237 checkNotNull(post, "post must not be null");
238 lock.writeLock().lock();
240 allPosts.remove(post.getId());
241 getPostsFrom(post.getSone().getId()).remove(post);
242 if (post.getRecipientId().isPresent()) {
243 getPostsTo(post.getRecipientId().get()).remove(post);
245 post.getSone().removePost(post);
247 lock.writeLock().unlock();
251 /** {@inheritDocs} */
253 public void storePosts(Sone sone, Collection<Post> posts) throws IllegalArgumentException {
254 checkNotNull(sone, "sone must not be null");
255 /* verify that all posts are from the same Sone. */
256 for (Post post : posts) {
257 if (!sone.equals(post.getSone())) {
258 throw new IllegalArgumentException(String.format("Post from different Sone found: %s", post));
262 lock.writeLock().lock();
264 /* remove all posts by the Sone. */
265 Collection<Post> oldPosts = getPostsFrom(sone.getId());
266 for (Post post : oldPosts) {
267 allPosts.remove(post.getId());
268 if (post.getRecipientId().isPresent()) {
269 getPostsTo(post.getRecipientId().get()).remove(post);
274 getPostsFrom(sone.getId()).addAll(posts);
275 for (Post post : posts) {
276 allPosts.put(post.getId(), post);
277 if (post.getRecipientId().isPresent()) {
278 getPostsTo(post.getRecipientId().get()).add(post);
282 lock.writeLock().unlock();
286 /** {@inheritDocs} */
288 public void removePosts(Sone sone) {
289 checkNotNull(sone, "sone must not be null");
290 lock.writeLock().lock();
292 /* remove all posts by the Sone. */
293 getPostsFrom(sone.getId()).clear();
294 for (Post post : sone.getPosts()) {
295 allPosts.remove(post.getId());
296 if (post.getRecipientId().isPresent()) {
297 getPostsTo(post.getRecipientId().get()).remove(post);
301 lock.writeLock().unlock();
306 // POSTREPLYPROVIDER METHODS
309 /** {@inheritDocs} */
311 public Optional<PostReply> getPostReply(String id) {
312 lock.readLock().lock();
314 return fromNullable(allPostReplies.get(id));
316 lock.readLock().unlock();
320 /** {@inheritDocs} */
322 public List<PostReply> getReplies(String postId) {
323 lock.readLock().lock();
325 if (!postReplies.containsKey(postId)) {
326 return Collections.emptyList();
328 return new ArrayList<PostReply>(postReplies.get(postId));
330 lock.readLock().unlock();
335 // POSTREPLYBUILDERFACTORY METHODS
338 /** {@inheritDocs} */
340 public PostReplyBuilder newPostReplyBuilder() {
341 return new MemoryPostReplyBuilder(this, soneProvider);
345 // POSTREPLYSTORE METHODS
348 /** {@inheritDocs} */
350 public void storePostReply(PostReply postReply) {
351 lock.writeLock().lock();
353 allPostReplies.put(postReply.getId(), postReply);
354 postReplies.put(postReply.getPostId(), postReply);
356 lock.writeLock().unlock();
360 /** {@inheritDocs} */
362 public void storePostReplies(Sone sone, Collection<PostReply> postReplies) {
363 checkNotNull(sone, "sone must not be null");
364 /* verify that all posts are from the same Sone. */
365 for (PostReply postReply : postReplies) {
366 if (!sone.equals(postReply.getSone())) {
367 throw new IllegalArgumentException(String.format("PostReply from different Sone found: %s", postReply));
371 lock.writeLock().lock();
373 /* remove all post replies of the Sone. */
374 for (PostReply postReply : getRepliesFrom(sone.getId())) {
375 removePostReply(postReply);
377 for (PostReply postReply : postReplies) {
378 allPostReplies.put(postReply.getId(), postReply);
379 sonePostReplies.put(postReply.getSone().getId(), postReply);
380 this.postReplies.put(postReply.getPostId(), postReply);
383 lock.writeLock().unlock();
387 /** {@inheritDocs} */
389 public void removePostReply(PostReply postReply) {
390 lock.writeLock().lock();
392 allPostReplies.remove(postReply.getId());
393 if (postReplies.containsKey(postReply.getPostId())) {
394 postReplies.get(postReply.getPostId()).remove(postReply);
397 lock.writeLock().unlock();
401 /** {@inheritDocs} */
403 public void removePostReplies(Sone sone) {
404 checkNotNull(sone, "sone must not be null");
406 lock.writeLock().lock();
408 for (PostReply postReply : sone.getReplies()) {
409 removePostReply(postReply);
412 lock.writeLock().unlock();
417 // ALBUMPROVDER METHODS
421 public Optional<Album> getAlbum(String albumId) {
422 lock.readLock().lock();
424 return fromNullable(allAlbums.get(albumId));
426 lock.readLock().unlock();
431 // ALBUMBUILDERFACTORY METHODS
435 public AlbumBuilder newAlbumBuilder() {
436 return new AlbumBuilderImpl();
440 // ALBUMSTORE METHODS
444 public void storeAlbum(Album album) {
445 lock.writeLock().lock();
447 allAlbums.put(album.getId(), album);
449 lock.writeLock().unlock();
454 public void removeAlbum(Album album) {
455 lock.writeLock().lock();
457 allAlbums.remove(album.getId());
459 lock.writeLock().unlock();
464 // IMAGEPROVIDER METHODS
468 public Optional<Image> getImage(String imageId) {
469 lock.readLock().lock();
471 return fromNullable(allImages.get(imageId));
473 lock.readLock().unlock();
478 // IMAGEBUILDERFACTORY METHODS
482 public ImageBuilder newImageBuilder() {
483 return new ImageBuilderImpl();
487 // IMAGESTORE METHODS
491 public void storeImage(Image image) {
492 lock.writeLock().lock();
494 allImages.put(image.getId(), image);
496 lock.writeLock().unlock();
501 public void removeImage(Image image) {
502 lock.writeLock().lock();
504 allImages.remove(image.getId());
506 lock.writeLock().unlock();
511 // PACKAGE-PRIVATE METHODS
515 * Returns whether the given post is known.
519 * @return {@code true} if the post is known, {@code false} otherwise
521 boolean isPostKnown(Post post) {
522 lock.readLock().lock();
524 return knownPosts.contains(post.getId());
526 lock.readLock().unlock();
531 * Sets whether the given post is known.
536 * {@code true} if the post is known, {@code false} otherwise
538 void setPostKnown(Post post, boolean known) {
539 lock.writeLock().lock();
542 knownPosts.add(post.getId());
544 knownPosts.remove(post.getId());
547 lock.writeLock().unlock();
552 * Returns whether the given post reply is known.
556 * @return {@code true} if the given post reply is known, {@code false}
559 boolean isPostReplyKnown(PostReply postReply) {
560 lock.readLock().lock();
562 return knownPostReplies.contains(postReply.getId());
564 lock.readLock().unlock();
569 * Sets whether the given post reply is known.
574 * {@code true} if the post reply is known, {@code false} otherwise
576 void setPostReplyKnown(PostReply postReply, boolean known) {
577 lock.writeLock().lock();
580 knownPostReplies.add(postReply.getId());
582 knownPostReplies.remove(postReply.getId());
585 lock.writeLock().unlock();
594 * Gets all posts for the given Sone, creating a new collection if there is
598 * The ID of the Sone to get the posts for
601 private Collection<Post> getPostsFrom(String soneId) {
602 lock.readLock().lock();
604 return sonePosts.get(soneId);
606 lock.readLock().unlock();
611 * Gets all posts that are directed the given Sone, creating a new collection
612 * if there is none yet.
615 * The ID of the Sone to get the posts for
618 private Collection<Post> getPostsTo(String recipientId) {
619 lock.readLock().lock();
621 return recipientPosts.get(recipientId);
623 lock.readLock().unlock();
627 /** Loads the known posts. */
628 private void loadKnownPosts() {
629 lock.writeLock().lock();
633 String knownPostId = configuration.getStringValue("KnownPosts/" + postCounter++ + "/ID").getValue(null);
634 if (knownPostId == null) {
637 knownPosts.add(knownPostId);
640 lock.writeLock().unlock();
645 * Saves the known posts to the configuration.
647 * @throws DatabaseException
648 * if a configuration error occurs
650 private void saveKnownPosts() throws DatabaseException {
651 lock.readLock().lock();
654 for (String knownPostId : knownPosts) {
655 configuration.getStringValue("KnownPosts/" + postCounter++ + "/ID").setValue(knownPostId);
657 configuration.getStringValue("KnownPosts/" + postCounter + "/ID").setValue(null);
658 } catch (ConfigurationException ce1) {
659 throw new DatabaseException("Could not save database.", ce1);
661 lock.readLock().unlock();
666 * Returns all replies by the given Sone.
670 * @return The post replies of the Sone, sorted by time (newest first)
672 private Collection<PostReply> getRepliesFrom(String id) {
673 lock.readLock().lock();
675 return Collections.unmodifiableCollection(sonePostReplies.get(id));
677 lock.readLock().unlock();
681 /** Loads the known post replies. */
682 private void loadKnownPostReplies() {
683 lock.writeLock().lock();
685 int replyCounter = 0;
687 String knownReplyId = configuration.getStringValue("KnownReplies/" + replyCounter++ + "/ID").getValue(null);
688 if (knownReplyId == null) {
691 knownPostReplies.add(knownReplyId);
694 lock.writeLock().unlock();
699 * Saves the known post replies to the configuration.
701 * @throws DatabaseException
702 * if a configuration error occurs
704 private void saveKnownPostReplies() throws DatabaseException {
705 lock.readLock().lock();
707 int replyCounter = 0;
708 for (String knownReplyId : knownPostReplies) {
709 configuration.getStringValue("KnownReplies/" + replyCounter++ + "/ID").setValue(knownReplyId);
711 configuration.getStringValue("KnownReplies/" + replyCounter + "/ID").setValue(null);
712 } catch (ConfigurationException ce1) {
713 throw new DatabaseException("Could not save database.", ce1);
715 lock.readLock().unlock();