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