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.SoneBuilder;
55 import net.pterodactylus.sone.database.SoneProvider;
56 import net.pterodactylus.util.config.Configuration;
57 import net.pterodactylus.util.config.ConfigurationException;
59 import com.google.common.base.Function;
60 import com.google.common.base.Optional;
61 import com.google.common.base.Predicate;
62 import com.google.common.collect.HashMultimap;
63 import com.google.common.collect.Multimap;
64 import com.google.common.collect.SortedSetMultimap;
65 import com.google.common.collect.TreeMultimap;
66 import com.google.common.util.concurrent.AbstractService;
67 import com.google.inject.Inject;
68 import com.google.inject.Singleton;
71 * Memory-based {@link PostDatabase} implementation.
73 * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
76 public class MemoryDatabase extends AbstractService implements Database {
79 private final ReadWriteLock lock = new ReentrantReadWriteLock();
81 /** The Sone provider. */
82 private final SoneProvider soneProvider;
84 /** The configuration. */
85 private final Configuration configuration;
86 private final ConfigurationLoader configurationLoader;
88 private final Map<String, Sone> allSones = new HashMap<String, Sone>();
90 /** All posts by their ID. */
91 private final Map<String, Post> allPosts = new HashMap<String, Post>();
93 /** All posts by their Sones. */
94 private final Multimap<String, Post> sonePosts = HashMultimap.create();
96 /** Whether posts are known. */
97 private final Set<String> knownPosts = new HashSet<String>();
99 /** All post replies by their ID. */
100 private final Map<String, PostReply> allPostReplies = new HashMap<String, PostReply>();
102 /** Replies sorted by Sone. */
103 private final SortedSetMultimap<String, PostReply> sonePostReplies = 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>();
115 private final Multimap<String, Album> soneAlbums = HashMultimap.create();
117 private final Map<String, Image> allImages = new HashMap<String, Image>();
118 private final Multimap<String, Image> soneImages = HashMultimap.create();
120 private final MemoryBookmarkDatabase memoryBookmarkDatabase;
123 * Creates a new memory database.
125 * @param soneProvider
127 * @param configuration
128 * The configuration for loading and saving elements
131 public MemoryDatabase(SoneProvider soneProvider, Configuration configuration) {
132 this.soneProvider = soneProvider;
133 this.configuration = configuration;
134 this.configurationLoader = new ConfigurationLoader(configuration);
135 memoryBookmarkDatabase =
136 new MemoryBookmarkDatabase(this, configurationLoader);
144 * Saves the database.
146 * @throws DatabaseException
147 * if an error occurs while saving
150 public void save() throws DatabaseException {
152 saveKnownPostReplies();
159 /** {@inheritDocs} */
161 protected void doStart() {
162 memoryBookmarkDatabase.start();
164 loadKnownPostReplies();
168 /** {@inheritDocs} */
170 protected void doStop() {
172 memoryBookmarkDatabase.stop();
175 } catch (DatabaseException de1) {
181 public SoneBuilder newSoneBuilder() {
182 return new MemorySoneBuilder();
186 public void storeSone(Sone sone) {
187 lock.writeLock().lock();
191 allSones.put(sone.getId(), sone);
192 sonePosts.putAll(sone.getId(), sone.getPosts());
193 for (Post post : sone.getPosts()) {
194 allPosts.put(post.getId(), post);
196 sonePostReplies.putAll(sone.getId(), sone.getReplies());
197 for (PostReply postReply : sone.getReplies()) {
198 allPostReplies.put(postReply.getId(), postReply);
200 soneAlbums.putAll(sone.getId(), toAllAlbums.apply(sone));
201 for (Album album : toAllAlbums.apply(sone)) {
202 allAlbums.put(album.getId(), album);
204 soneImages.putAll(sone.getId(), toAllImages.apply(sone));
205 for (Image image : toAllImages.apply(sone)) {
206 allImages.put(image.getId(), image);
209 lock.writeLock().unlock();
214 public void removeSone(Sone sone) {
215 lock.writeLock().lock();
217 allSones.remove(sone.getId());
218 Collection<Post> removedPosts = sonePosts.removeAll(sone.getId());
219 for (Post removedPost : removedPosts) {
220 allPosts.remove(removedPost.getId());
222 Collection<PostReply> removedPostReplies =
223 sonePostReplies.removeAll(sone.getId());
224 for (PostReply removedPostReply : removedPostReplies) {
225 allPostReplies.remove(removedPostReply.getId());
227 Collection<Album> removedAlbums =
228 soneAlbums.removeAll(sone.getId());
229 for (Album removedAlbum : removedAlbums) {
230 allAlbums.remove(removedAlbum.getId());
232 Collection<Image> removedImages =
233 soneImages.removeAll(sone.getId());
234 for (Image removedImage : removedImages) {
235 allImages.remove(removedImage.getId());
238 lock.writeLock().unlock();
243 public Function<String, Optional<Sone>> soneLoader() {
244 return new Function<String, Optional<Sone>>() {
246 public Optional<Sone> apply(String soneId) {
247 return getSone(soneId);
253 public Optional<Sone> getSone(String soneId) {
254 lock.readLock().lock();
256 return fromNullable(allSones.get(soneId));
258 lock.readLock().unlock();
263 public Collection<Sone> getSones() {
264 lock.readLock().lock();
266 return new HashSet<Sone>(allSones.values());
268 lock.readLock().unlock();
273 public Collection<Sone> getLocalSones() {
274 lock.readLock().lock();
276 return from(allSones.values()).filter(LOCAL_SONE_FILTER).toSet();
278 lock.readLock().unlock();
283 public Collection<Sone> getRemoteSones() {
284 lock.readLock().lock();
286 return from(allSones.values())
287 .filter(not(LOCAL_SONE_FILTER)) .toSet();
289 lock.readLock().unlock();
294 // POSTPROVIDER METHODS
297 /** {@inheritDocs} */
299 public Optional<Post> getPost(String postId) {
300 lock.readLock().lock();
302 return fromNullable(allPosts.get(postId));
304 lock.readLock().unlock();
308 /** {@inheritDocs} */
310 public Collection<Post> getPosts(String soneId) {
311 return new HashSet<Post>(getPostsFrom(soneId));
314 /** {@inheritDocs} */
316 public Collection<Post> getDirectedPosts(final String recipientId) {
317 lock.readLock().lock();
319 return from(sonePosts.values()).filter(new Predicate<Post>() {
321 public boolean apply(Post post) {
322 return post.getRecipientId().asSet().contains(recipientId);
326 lock.readLock().unlock();
331 // POSTBUILDERFACTORY METHODS
334 /** {@inheritDocs} */
336 public PostBuilder newPostBuilder() {
337 return new MemoryPostBuilder(this, soneProvider);
344 /** {@inheritDocs} */
346 public void storePost(Post post) {
347 checkNotNull(post, "post must not be null");
348 lock.writeLock().lock();
350 allPosts.put(post.getId(), post);
351 getPostsFrom(post.getSone().getId()).add(post);
353 lock.writeLock().unlock();
357 /** {@inheritDocs} */
359 public void removePost(Post post) {
360 checkNotNull(post, "post must not be null");
361 lock.writeLock().lock();
363 allPosts.remove(post.getId());
364 getPostsFrom(post.getSone().getId()).remove(post);
365 post.getSone().removePost(post);
367 lock.writeLock().unlock();
372 // POSTREPLYPROVIDER METHODS
375 /** {@inheritDocs} */
377 public Optional<PostReply> getPostReply(String id) {
378 lock.readLock().lock();
380 return fromNullable(allPostReplies.get(id));
382 lock.readLock().unlock();
386 /** {@inheritDocs} */
388 public List<PostReply> getReplies(final String postId) {
389 lock.readLock().lock();
391 return from(allPostReplies.values())
392 .filter(new Predicate<PostReply>() {
394 public boolean apply(PostReply postReply) {
395 return postReply.getPostId().equals(postId);
397 }).toSortedList(TIME_COMPARATOR);
399 lock.readLock().unlock();
404 // POSTREPLYBUILDERFACTORY METHODS
407 /** {@inheritDocs} */
409 public PostReplyBuilder newPostReplyBuilder() {
410 return new MemoryPostReplyBuilder(this, soneProvider);
414 // POSTREPLYSTORE METHODS
417 /** {@inheritDocs} */
419 public void storePostReply(PostReply postReply) {
420 lock.writeLock().lock();
422 allPostReplies.put(postReply.getId(), postReply);
424 lock.writeLock().unlock();
428 /** {@inheritDocs} */
430 public void removePostReply(PostReply postReply) {
431 lock.writeLock().lock();
433 allPostReplies.remove(postReply.getId());
435 lock.writeLock().unlock();
440 // ALBUMPROVDER METHODS
444 public Optional<Album> getAlbum(String albumId) {
445 lock.readLock().lock();
447 return fromNullable(allAlbums.get(albumId));
449 lock.readLock().unlock();
454 // ALBUMBUILDERFACTORY METHODS
458 public AlbumBuilder newAlbumBuilder() {
459 return new AlbumBuilderImpl();
463 // ALBUMSTORE METHODS
467 public void storeAlbum(Album album) {
468 lock.writeLock().lock();
470 allAlbums.put(album.getId(), album);
471 soneAlbums.put(album.getSone().getId(), album);
473 lock.writeLock().unlock();
478 public void removeAlbum(Album album) {
479 lock.writeLock().lock();
481 allAlbums.remove(album.getId());
482 soneAlbums.remove(album.getSone().getId(), album);
484 lock.writeLock().unlock();
489 // IMAGEPROVIDER METHODS
493 public Optional<Image> getImage(String imageId) {
494 lock.readLock().lock();
496 return fromNullable(allImages.get(imageId));
498 lock.readLock().unlock();
503 // IMAGEBUILDERFACTORY METHODS
507 public ImageBuilder newImageBuilder() {
508 return new ImageBuilderImpl();
512 // IMAGESTORE METHODS
516 public void storeImage(Image image) {
517 lock.writeLock().lock();
519 allImages.put(image.getId(), image);
520 soneImages.put(image.getSone().getId(), image);
522 lock.writeLock().unlock();
527 public void removeImage(Image image) {
528 lock.writeLock().lock();
530 allImages.remove(image.getId());
531 soneImages.remove(image.getSone().getId(), image);
533 lock.writeLock().unlock();
538 public void bookmarkPost(Post post) {
539 memoryBookmarkDatabase.bookmarkPost(post);
543 public void unbookmarkPost(Post post) {
544 memoryBookmarkDatabase.unbookmarkPost(post);
548 public boolean isPostBookmarked(Post post) {
549 return memoryBookmarkDatabase.isPostBookmarked(post);
553 public Set<Post> getBookmarkedPosts() {
554 return memoryBookmarkDatabase.getBookmarkedPosts();
558 // PACKAGE-PRIVATE METHODS
562 * Returns whether the given post is known.
566 * @return {@code true} if the post is known, {@code false} otherwise
568 boolean isPostKnown(Post post) {
569 lock.readLock().lock();
571 return knownPosts.contains(post.getId());
573 lock.readLock().unlock();
578 * Sets whether the given post is known.
583 * {@code true} if the post is known, {@code false} otherwise
585 void setPostKnown(Post post, boolean known) {
586 lock.writeLock().lock();
589 knownPosts.add(post.getId());
591 knownPosts.remove(post.getId());
594 lock.writeLock().unlock();
599 * Returns whether the given post reply is known.
603 * @return {@code true} if the given post reply is known, {@code false}
606 boolean isPostReplyKnown(PostReply postReply) {
607 lock.readLock().lock();
609 return knownPostReplies.contains(postReply.getId());
611 lock.readLock().unlock();
616 * Sets whether the given post reply is known.
621 * {@code true} if the post reply is known, {@code false} otherwise
623 void setPostReplyKnown(PostReply postReply, boolean known) {
624 lock.writeLock().lock();
627 knownPostReplies.add(postReply.getId());
629 knownPostReplies.remove(postReply.getId());
632 lock.writeLock().unlock();
641 * Gets all posts for the given Sone, creating a new collection if there is
645 * The ID of the Sone to get the posts for
648 private Collection<Post> getPostsFrom(String soneId) {
649 lock.readLock().lock();
651 return sonePosts.get(soneId);
653 lock.readLock().unlock();
657 /** Loads the known posts. */
658 private void loadKnownPosts() {
659 Set<String> knownPosts = configurationLoader.loadKnownPosts();
660 lock.writeLock().lock();
662 this.knownPosts.clear();
663 this.knownPosts.addAll(knownPosts);
665 lock.writeLock().unlock();
670 * Saves the known posts to the configuration.
672 * @throws DatabaseException
673 * if a configuration error occurs
675 private void saveKnownPosts() throws DatabaseException {
676 lock.readLock().lock();
679 for (String knownPostId : knownPosts) {
680 configuration.getStringValue("KnownPosts/" + postCounter++ + "/ID").setValue(knownPostId);
682 configuration.getStringValue("KnownPosts/" + postCounter + "/ID").setValue(null);
683 } catch (ConfigurationException ce1) {
684 throw new DatabaseException("Could not save database.", ce1);
686 lock.readLock().unlock();
690 /** Loads the known post replies. */
691 private void loadKnownPostReplies() {
692 Set<String> knownPostReplies = configurationLoader.loadKnownPostReplies();
693 lock.writeLock().lock();
695 this.knownPostReplies.clear();
696 this.knownPostReplies.addAll(knownPostReplies);
698 lock.writeLock().unlock();
703 * Saves the known post replies to the configuration.
705 * @throws DatabaseException
706 * if a configuration error occurs
708 private void saveKnownPostReplies() throws DatabaseException {
709 lock.readLock().lock();
711 int replyCounter = 0;
712 for (String knownReplyId : knownPostReplies) {
713 configuration.getStringValue("KnownReplies/" + replyCounter++ + "/ID").setValue(knownReplyId);
715 configuration.getStringValue("KnownReplies/" + replyCounter + "/ID").setValue(null);
716 } catch (ConfigurationException ce1) {
717 throw new DatabaseException("Could not save database.", ce1);
719 lock.readLock().unlock();