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