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