Integrate basic album storage and retrieval in memory database.
[Sone.git] / src / main / java / net / pterodactylus / sone / database / memory / MemoryDatabase.java
1 /*
2  * Sone - MemoryDatabase.java - Copyright © 2013 David Roden
3  *
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.
8  *
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.
13  *
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/>.
16  */
17
18 package net.pterodactylus.sone.database.memory;
19
20 import static com.google.common.base.Optional.fromNullable;
21 import static com.google.common.base.Preconditions.checkNotNull;
22
23 import java.util.ArrayList;
24 import java.util.Collection;
25 import java.util.Collections;
26 import java.util.Comparator;
27 import java.util.HashMap;
28 import java.util.HashSet;
29 import java.util.List;
30 import java.util.Map;
31 import java.util.Set;
32 import java.util.SortedSet;
33 import java.util.TreeSet;
34 import java.util.concurrent.locks.ReadWriteLock;
35 import java.util.concurrent.locks.ReentrantReadWriteLock;
36
37 import net.pterodactylus.sone.data.Album;
38 import net.pterodactylus.sone.data.Post;
39 import net.pterodactylus.sone.data.PostReply;
40 import net.pterodactylus.sone.data.Reply;
41 import net.pterodactylus.sone.data.Sone;
42 import net.pterodactylus.sone.database.Database;
43 import net.pterodactylus.sone.database.DatabaseException;
44 import net.pterodactylus.sone.database.PostBuilder;
45 import net.pterodactylus.sone.database.PostDatabase;
46 import net.pterodactylus.sone.database.PostReplyBuilder;
47 import net.pterodactylus.sone.database.SoneProvider;
48 import net.pterodactylus.util.config.Configuration;
49 import net.pterodactylus.util.config.ConfigurationException;
50
51 import com.google.common.base.Optional;
52 import com.google.common.collect.SortedSetMultimap;
53 import com.google.common.collect.TreeMultimap;
54 import com.google.common.util.concurrent.AbstractService;
55 import com.google.inject.Inject;
56
57 /**
58  * Memory-based {@link PostDatabase} implementation.
59  *
60  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
61  */
62 public class MemoryDatabase extends AbstractService implements Database {
63
64         /** The lock. */
65         private final ReadWriteLock lock = new ReentrantReadWriteLock();
66
67         /** The Sone provider. */
68         private final SoneProvider soneProvider;
69
70         /** The configuration. */
71         private final Configuration configuration;
72
73         /** All posts by their ID. */
74         private final Map<String, Post> allPosts = new HashMap<String, Post>();
75
76         /** All posts by their Sones. */
77         private final Map<String, Collection<Post>> sonePosts = new HashMap<String, Collection<Post>>();
78
79         /** All posts by their recipient. */
80         private final Map<String, Collection<Post>> recipientPosts = new HashMap<String, Collection<Post>>();
81
82         /** Whether posts are known. */
83         private final Set<String> knownPosts = new HashSet<String>();
84
85         /** All post replies by their ID. */
86         private final Map<String, PostReply> allPostReplies = new HashMap<String, PostReply>();
87
88         /** Replies sorted by Sone. */
89         private final SortedSetMultimap<String, PostReply> sonePostReplies = TreeMultimap.create(new Comparator<String>() {
90
91                 @Override
92                 public int compare(String leftString, String rightString) {
93                         return leftString.compareTo(rightString);
94                 }
95         }, PostReply.TIME_COMPARATOR);
96
97         /** Replies by post. */
98         private final Map<String, SortedSet<PostReply>> postReplies = new HashMap<String, SortedSet<PostReply>>();
99
100         /** Whether post replies are known. */
101         private final Set<String> knownPostReplies = new HashSet<String>();
102
103         private final Map<String, Album> allAlbums = new HashMap<String, Album>();
104
105         /**
106          * Creates a new memory database.
107          *
108          * @param soneProvider
109          *              The Sone provider
110          * @param configuration
111          *              The configuration for loading and saving elements
112          */
113         @Inject
114         public MemoryDatabase(SoneProvider soneProvider, Configuration configuration) {
115                 this.soneProvider = soneProvider;
116                 this.configuration = configuration;
117         }
118
119         //
120         // DATABASE METHODS
121         //
122
123         /**
124          * Saves the database.
125          *
126          * @throws DatabaseException
127          *              if an error occurs while saving
128          */
129         @Override
130         public void save() throws DatabaseException {
131                 saveKnownPosts();
132                 saveKnownPostReplies();
133         }
134
135         //
136         // SERVICE METHODS
137         //
138
139         /** {@inheritDocs} */
140         @Override
141         protected void doStart() {
142                 loadKnownPosts();
143                 loadKnownPostReplies();
144                 notifyStarted();
145         }
146
147         /** {@inheritDocs} */
148         @Override
149         protected void doStop() {
150                 try {
151                         save();
152                         notifyStopped();
153                 } catch (DatabaseException de1) {
154                         notifyFailed(de1);
155                 }
156         }
157
158         //
159         // POSTPROVIDER METHODS
160         //
161
162         /** {@inheritDocs} */
163         @Override
164         public Optional<Post> getPost(String postId) {
165                 lock.readLock().lock();
166                 try {
167                         return fromNullable(allPosts.get(postId));
168                 } finally {
169                         lock.readLock().unlock();
170                 }
171         }
172
173         /** {@inheritDocs} */
174         @Override
175         public Collection<Post> getPosts(String soneId) {
176                 return new HashSet<Post>(getPostsFrom(soneId));
177         }
178
179         /** {@inheritDocs} */
180         @Override
181         public Collection<Post> getDirectedPosts(String recipientId) {
182                 lock.readLock().lock();
183                 try {
184                         Collection<Post> posts = recipientPosts.get(recipientId);
185                         return (posts == null) ? Collections.<Post>emptySet() : new HashSet<Post>(posts);
186                 } finally {
187                         lock.readLock().unlock();
188                 }
189         }
190
191         //
192         // POSTBUILDERFACTORY METHODS
193         //
194
195         /** {@inheritDocs} */
196         @Override
197         public PostBuilder newPostBuilder() {
198                 return new MemoryPostBuilder(this, soneProvider);
199         }
200
201         //
202         // POSTSTORE METHODS
203         //
204
205         /** {@inheritDocs} */
206         @Override
207         public void storePost(Post post) {
208                 checkNotNull(post, "post must not be null");
209                 lock.writeLock().lock();
210                 try {
211                         allPosts.put(post.getId(), post);
212                         getPostsFrom(post.getSone().getId()).add(post);
213                         if (post.getRecipientId().isPresent()) {
214                                 getPostsTo(post.getRecipientId().get()).add(post);
215                         }
216                 } finally {
217                         lock.writeLock().unlock();
218                 }
219         }
220
221         /** {@inheritDocs} */
222         @Override
223         public void removePost(Post post) {
224                 checkNotNull(post, "post must not be null");
225                 lock.writeLock().lock();
226                 try {
227                         allPosts.remove(post.getId());
228                         getPostsFrom(post.getSone().getId()).remove(post);
229                         if (post.getRecipientId().isPresent()) {
230                                 getPostsTo(post.getRecipientId().get()).remove(post);
231                         }
232                         post.getSone().removePost(post);
233                 } finally {
234                         lock.writeLock().unlock();
235                 }
236         }
237
238         /** {@inheritDocs} */
239         @Override
240         public void storePosts(Sone sone, Collection<Post> posts) throws IllegalArgumentException {
241                 checkNotNull(sone, "sone must not be null");
242                 /* verify that all posts are from the same Sone. */
243                 for (Post post : posts) {
244                         if (!sone.equals(post.getSone())) {
245                                 throw new IllegalArgumentException(String.format("Post from different Sone found: %s", post));
246                         }
247                 }
248
249                 lock.writeLock().lock();
250                 try {
251                         /* remove all posts by the Sone. */
252                         getPostsFrom(sone.getId()).clear();
253                         for (Post post : posts) {
254                                 allPosts.remove(post.getId());
255                                 if (post.getRecipientId().isPresent()) {
256                                         getPostsTo(post.getRecipientId().get()).remove(post);
257                                 }
258                         }
259
260                         /* add new posts. */
261                         getPostsFrom(sone.getId()).addAll(posts);
262                         for (Post post : posts) {
263                                 allPosts.put(post.getId(), post);
264                                 if (post.getRecipientId().isPresent()) {
265                                         getPostsTo(post.getRecipientId().get()).add(post);
266                                 }
267                         }
268                 } finally {
269                         lock.writeLock().unlock();
270                 }
271         }
272
273         /** {@inheritDocs} */
274         @Override
275         public void removePosts(Sone sone) {
276                 checkNotNull(sone, "sone must not be null");
277                 lock.writeLock().lock();
278                 try {
279                         /* remove all posts by the Sone. */
280                         getPostsFrom(sone.getId()).clear();
281                         for (Post post : sone.getPosts()) {
282                                 allPosts.remove(post.getId());
283                                 if (post.getRecipientId().isPresent()) {
284                                         getPostsTo(post.getRecipientId().get()).remove(post);
285                                 }
286                         }
287                 } finally {
288                         lock.writeLock().unlock();
289                 }
290         }
291
292         //
293         // POSTREPLYPROVIDER METHODS
294         //
295
296         /** {@inheritDocs} */
297         @Override
298         public Optional<PostReply> getPostReply(String id) {
299                 lock.readLock().lock();
300                 try {
301                         return fromNullable(allPostReplies.get(id));
302                 } finally {
303                         lock.readLock().unlock();
304                 }
305         }
306
307         /** {@inheritDocs} */
308         @Override
309         public List<PostReply> getReplies(String postId) {
310                 lock.readLock().lock();
311                 try {
312                         if (!postReplies.containsKey(postId)) {
313                                 return Collections.emptyList();
314                         }
315                         return new ArrayList<PostReply>(postReplies.get(postId));
316                 } finally {
317                         lock.readLock().unlock();
318                 }
319         }
320
321         //
322         // POSTREPLYBUILDERFACTORY METHODS
323         //
324
325         /** {@inheritDocs} */
326         @Override
327         public PostReplyBuilder newPostReplyBuilder() {
328                 return new MemoryPostReplyBuilder(this, soneProvider);
329         }
330
331         //
332         // POSTREPLYSTORE METHODS
333         //
334
335         /** {@inheritDocs} */
336         @Override
337         public void storePostReply(PostReply postReply) {
338                 lock.writeLock().lock();
339                 try {
340                         allPostReplies.put(postReply.getId(), postReply);
341                         if (postReplies.containsKey(postReply.getPostId())) {
342                                 postReplies.get(postReply.getPostId()).add(postReply);
343                         } else {
344                                 TreeSet<PostReply> replies = new TreeSet<PostReply>(Reply.TIME_COMPARATOR);
345                                 replies.add(postReply);
346                                 postReplies.put(postReply.getPostId(), replies);
347                         }
348                 } finally {
349                         lock.writeLock().unlock();
350                 }
351         }
352
353         /** {@inheritDocs} */
354         @Override
355         public void storePostReplies(Sone sone, Collection<PostReply> postReplies) {
356                 checkNotNull(sone, "sone must not be null");
357                 /* verify that all posts are from the same Sone. */
358                 for (PostReply postReply : postReplies) {
359                         if (!sone.equals(postReply.getSone())) {
360                                 throw new IllegalArgumentException(String.format("PostReply from different Sone found: %s", postReply));
361                         }
362                 }
363
364                 lock.writeLock().lock();
365                 try {
366                         /* remove all post replies of the Sone. */
367                         for (PostReply postReply : getRepliesFrom(sone.getId())) {
368                                 removePostReply(postReply);
369                         }
370                         for (PostReply postReply : postReplies) {
371                                 allPostReplies.put(postReply.getId(), postReply);
372                                 sonePostReplies.put(postReply.getSone().getId(), postReply);
373                                 if (this.postReplies.containsKey(postReply.getPostId())) {
374                                         this.postReplies.get(postReply.getPostId()).add(postReply);
375                                 } else {
376                                         TreeSet<PostReply> replies = new TreeSet<PostReply>(Reply.TIME_COMPARATOR);
377                                         replies.add(postReply);
378                                         this.postReplies.put(postReply.getPostId(), replies);
379                                 }
380                         }
381                 } finally {
382                         lock.writeLock().unlock();
383                 }
384         }
385
386         /** {@inheritDocs} */
387         @Override
388         public void removePostReply(PostReply postReply) {
389                 lock.writeLock().lock();
390                 try {
391                         allPostReplies.remove(postReply.getId());
392                         if (postReplies.containsKey(postReply.getPostId())) {
393                                 postReplies.get(postReply.getPostId()).remove(postReply);
394                                 if (postReplies.get(postReply.getPostId()).isEmpty()) {
395                                         postReplies.remove(postReply.getPostId());
396                                 }
397                         }
398                 } finally {
399                         lock.writeLock().unlock();
400                 }
401         }
402
403         /** {@inheritDocs} */
404         @Override
405         public void removePostReplies(Sone sone) {
406                 checkNotNull(sone, "sone must not be null");
407
408                 lock.writeLock().lock();
409                 try {
410                         for (PostReply postReply : sone.getReplies()) {
411                                 removePostReply(postReply);
412                         }
413                 } finally {
414                         lock.writeLock().unlock();
415                 }
416         }
417
418         //
419         // ALBUMPROVDER METHODS
420         //
421
422         @Override
423         public Optional<Album> getAlbum(String albumId) {
424                 lock.readLock().lock();
425                 try {
426                         return fromNullable(allAlbums.get(albumId));
427                 } finally {
428                         lock.readLock().unlock();
429                 }
430         }
431
432         //
433         // ALBUMSTORE METHODS
434         //
435
436         @Override
437         public void storeAlbum(Album album) {
438                 lock.writeLock().lock();
439                 try {
440                         allAlbums.put(album.getId(), album);
441                 } finally {
442                         lock.writeLock().unlock();
443                 }
444         }
445
446         @Override
447         public void removeAlbum(Album album) {
448                 lock.writeLock().lock();
449                 try {
450                         allAlbums.remove(album.getId());
451                 } finally {
452                         lock.writeLock().unlock();
453                 }
454         }
455
456         //
457         // PACKAGE-PRIVATE METHODS
458         //
459
460         /**
461          * Returns whether the given post is known.
462          *
463          * @param post
464          *              The post
465          * @return {@code true} if the post is known, {@code false} otherwise
466          */
467         boolean isPostKnown(Post post) {
468                 lock.readLock().lock();
469                 try {
470                         return knownPosts.contains(post.getId());
471                 } finally {
472                         lock.readLock().unlock();
473                 }
474         }
475
476         /**
477          * Sets whether the given post is known.
478          *
479          * @param post
480          *              The post
481          * @param known
482          *              {@code true} if the post is known, {@code false} otherwise
483          */
484         void setPostKnown(Post post, boolean known) {
485                 lock.writeLock().lock();
486                 try {
487                         if (known) {
488                                 knownPosts.add(post.getId());
489                         } else {
490                                 knownPosts.remove(post.getId());
491                         }
492                 } finally {
493                         lock.writeLock().unlock();
494                 }
495         }
496
497         /**
498          * Returns whether the given post reply is known.
499          *
500          * @param postReply
501          *              The post reply
502          * @return {@code true} if the given post reply is known, {@code false}
503          *         otherwise
504          */
505         boolean isPostReplyKnown(PostReply postReply) {
506                 lock.readLock().lock();
507                 try {
508                         return knownPostReplies.contains(postReply.getId());
509                 } finally {
510                         lock.readLock().unlock();
511                 }
512         }
513
514         /**
515          * Sets whether the given post reply is known.
516          *
517          * @param postReply
518          *              The post reply
519          * @param known
520          *              {@code true} if the post reply is known, {@code false} otherwise
521          */
522         void setPostReplyKnown(PostReply postReply, boolean known) {
523                 lock.writeLock().lock();
524                 try {
525                         if (known) {
526                                 knownPostReplies.add(postReply.getId());
527                         } else {
528                                 knownPostReplies.remove(postReply.getId());
529                         }
530                 } finally {
531                         lock.writeLock().unlock();
532                 }
533         }
534
535         //
536         // PRIVATE METHODS
537         //
538
539         /**
540          * Gets all posts for the given Sone, creating a new collection if there is
541          * none yet.
542          *
543          * @param soneId
544          *              The ID of the Sone to get the posts for
545          * @return All posts
546          */
547         private Collection<Post> getPostsFrom(String soneId) {
548                 Collection<Post> posts = null;
549                 lock.readLock().lock();
550                 try {
551                         posts = sonePosts.get(soneId);
552                 } finally {
553                         lock.readLock().unlock();
554                 }
555                 if (posts != null) {
556                         return posts;
557                 }
558
559                 posts = new HashSet<Post>();
560                 lock.writeLock().lock();
561                 try {
562                         sonePosts.put(soneId, posts);
563                 } finally {
564                         lock.writeLock().unlock();
565                 }
566
567                 return posts;
568         }
569
570         /**
571          * Gets all posts that are directed the given Sone, creating a new collection
572          * if there is none yet.
573          *
574          * @param recipientId
575          *              The ID of the Sone to get the posts for
576          * @return All posts
577          */
578         private Collection<Post> getPostsTo(String recipientId) {
579                 Collection<Post> posts = null;
580                 lock.readLock().lock();
581                 try {
582                         posts = recipientPosts.get(recipientId);
583                 } finally {
584                         lock.readLock().unlock();
585                 }
586                 if (posts != null) {
587                         return posts;
588                 }
589
590                 posts = new HashSet<Post>();
591                 lock.writeLock().lock();
592                 try {
593                         recipientPosts.put(recipientId, posts);
594                 } finally {
595                         lock.writeLock().unlock();
596                 }
597
598                 return posts;
599         }
600
601         /** Loads the known posts. */
602         private void loadKnownPosts() {
603                 lock.writeLock().lock();
604                 try {
605                         int postCounter = 0;
606                         while (true) {
607                                 String knownPostId = configuration.getStringValue("KnownPosts/" + postCounter++ + "/ID").getValue(null);
608                                 if (knownPostId == null) {
609                                         break;
610                                 }
611                                 knownPosts.add(knownPostId);
612                         }
613                 } finally {
614                         lock.writeLock().unlock();
615                 }
616         }
617
618         /**
619          * Saves the known posts to the configuration.
620          *
621          * @throws DatabaseException
622          *              if a configuration error occurs
623          */
624         private void saveKnownPosts() throws DatabaseException {
625                 lock.readLock().lock();
626                 try {
627                         int postCounter = 0;
628                         for (String knownPostId : knownPosts) {
629                                 configuration.getStringValue("KnownPosts/" + postCounter++ + "/ID").setValue(knownPostId);
630                         }
631                         configuration.getStringValue("KnownPosts/" + postCounter + "/ID").setValue(null);
632                 } catch (ConfigurationException ce1) {
633                         throw new DatabaseException("Could not save database.", ce1);
634                 } finally {
635                         lock.readLock().unlock();
636                 }
637         }
638
639         /**
640          * Returns all replies by the given Sone.
641          *
642          * @param id
643          *              The ID of the Sone
644          * @return The post replies of the Sone, sorted by time (newest first)
645          */
646         private Collection<PostReply> getRepliesFrom(String id) {
647                 lock.readLock().lock();
648                 try {
649                         if (sonePostReplies.containsKey(id)) {
650                                 return Collections.unmodifiableCollection(sonePostReplies.get(id));
651                         }
652                         return Collections.emptySet();
653                 } finally {
654                         lock.readLock().unlock();
655                 }
656         }
657
658         /** Loads the known post replies. */
659         private void loadKnownPostReplies() {
660                 lock.writeLock().lock();
661                 try {
662                         int replyCounter = 0;
663                         while (true) {
664                                 String knownReplyId = configuration.getStringValue("KnownReplies/" + replyCounter++ + "/ID").getValue(null);
665                                 if (knownReplyId == null) {
666                                         break;
667                                 }
668                                 knownPostReplies.add(knownReplyId);
669                         }
670                 } finally {
671                         lock.writeLock().unlock();
672                 }
673         }
674
675         /**
676          * Saves the known post replies to the configuration.
677          *
678          * @throws DatabaseException
679          *              if a configuration error occurs
680          */
681         private void saveKnownPostReplies() throws DatabaseException {
682                 lock.readLock().lock();
683                 try {
684                         int replyCounter = 0;
685                         for (String knownReplyId : knownPostReplies) {
686                                 configuration.getStringValue("KnownReplies/" + replyCounter++ + "/ID").setValue(knownReplyId);
687                         }
688                         configuration.getStringValue("KnownReplies/" + replyCounter + "/ID").setValue(null);
689                 } catch (ConfigurationException ce1) {
690                         throw new DatabaseException("Could not save database.", ce1);
691                 } finally {
692                         lock.readLock().unlock();
693                 }
694         }
695
696 }