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