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