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