07bcf2f9db2b669d9ba2cc428e4e9319d4b395de
[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         //
324         // POSTPROVIDER METHODS
325         //
326
327         @Nullable
328         @Override
329         public Post getPost(@Nonnull String postId) {
330                 lock.readLock().lock();
331                 try {
332                         return allPosts.get(postId);
333                 } finally {
334                         lock.readLock().unlock();
335                 }
336         }
337
338         /** {@inheritDocs} */
339         @Override
340         public Collection<Post> getPosts(String soneId) {
341                 return new HashSet<Post>(getPostsFrom(soneId));
342         }
343
344         /** {@inheritDocs} */
345         @Override
346         public Collection<Post> getDirectedPosts(final String recipientId) {
347                 lock.readLock().lock();
348                 try {
349                         return from(sonePosts.values()).filter(new Predicate<Post>() {
350                                 @Override
351                                 public boolean apply(Post post) {
352                                         return post.getRecipientId().asSet().contains(recipientId);
353                                 }
354                         }).toSet();
355                 } finally {
356                         lock.readLock().unlock();
357                 }
358         }
359
360         //
361         // POSTBUILDERFACTORY METHODS
362         //
363
364         /** {@inheritDocs} */
365         @Override
366         public PostBuilder newPostBuilder() {
367                 return new MemoryPostBuilder(this, this);
368         }
369
370         //
371         // POSTSTORE METHODS
372         //
373
374         /** {@inheritDocs} */
375         @Override
376         public void storePost(Post post) {
377                 checkNotNull(post, "post must not be null");
378                 lock.writeLock().lock();
379                 try {
380                         allPosts.put(post.getId(), post);
381                         getPostsFrom(post.getSone().getId()).add(post);
382                 } finally {
383                         lock.writeLock().unlock();
384                 }
385         }
386
387         /** {@inheritDocs} */
388         @Override
389         public void removePost(Post post) {
390                 checkNotNull(post, "post must not be null");
391                 lock.writeLock().lock();
392                 try {
393                         allPosts.remove(post.getId());
394                         getPostsFrom(post.getSone().getId()).remove(post);
395                         post.getSone().removePost(post);
396                 } finally {
397                         lock.writeLock().unlock();
398                 }
399         }
400
401         //
402         // POSTREPLYPROVIDER METHODS
403         //
404
405         @Nullable
406         @Override
407         public PostReply getPostReply(String id) {
408                 lock.readLock().lock();
409                 try {
410                         return allPostReplies.get(id);
411                 } finally {
412                         lock.readLock().unlock();
413                 }
414         }
415
416         /** {@inheritDocs} */
417         @Override
418         public List<PostReply> getReplies(final String postId) {
419                 lock.readLock().lock();
420                 try {
421                         return from(allPostReplies.values())
422                                         .filter(new Predicate<PostReply>() {
423                                                 @Override
424                                                 public boolean apply(PostReply postReply) {
425                                                         return postReply.getPostId().equals(postId);
426                                                 }
427                                         }).toSortedList(TIME_COMPARATOR);
428                 } finally {
429                         lock.readLock().unlock();
430                 }
431         }
432
433         //
434         // POSTREPLYBUILDERFACTORY METHODS
435         //
436
437         /** {@inheritDocs} */
438         @Override
439         public PostReplyBuilder newPostReplyBuilder() {
440                 return new MemoryPostReplyBuilder(this, this);
441         }
442
443         //
444         // POSTREPLYSTORE METHODS
445         //
446
447         /** {@inheritDocs} */
448         @Override
449         public void storePostReply(PostReply postReply) {
450                 lock.writeLock().lock();
451                 try {
452                         allPostReplies.put(postReply.getId(), postReply);
453                 } finally {
454                         lock.writeLock().unlock();
455                 }
456         }
457
458         /** {@inheritDocs} */
459         @Override
460         public void removePostReply(PostReply postReply) {
461                 lock.writeLock().lock();
462                 try {
463                         allPostReplies.remove(postReply.getId());
464                 } finally {
465                         lock.writeLock().unlock();
466                 }
467         }
468
469         //
470         // ALBUMPROVDER METHODS
471         //
472
473         @Nullable
474         @Override
475         public Album getAlbum(@Nonnull String albumId) {
476                 lock.readLock().lock();
477                 try {
478                         return allAlbums.get(albumId);
479                 } finally {
480                         lock.readLock().unlock();
481                 }
482         }
483
484         //
485         // ALBUMBUILDERFACTORY METHODS
486         //
487
488         @Override
489         public AlbumBuilder newAlbumBuilder() {
490                 return new AlbumBuilderImpl();
491         }
492
493         //
494         // ALBUMSTORE METHODS
495         //
496
497         @Override
498         public void storeAlbum(Album album) {
499                 lock.writeLock().lock();
500                 try {
501                         allAlbums.put(album.getId(), album);
502                         soneAlbums.put(album.getSone().getId(), album);
503                 } finally {
504                         lock.writeLock().unlock();
505                 }
506         }
507
508         @Override
509         public void removeAlbum(Album album) {
510                 lock.writeLock().lock();
511                 try {
512                         allAlbums.remove(album.getId());
513                         soneAlbums.remove(album.getSone().getId(), album);
514                 } finally {
515                         lock.writeLock().unlock();
516                 }
517         }
518
519         //
520         // IMAGEPROVIDER METHODS
521         //
522
523         @Nullable
524         @Override
525         public Image getImage(@Nonnull String imageId) {
526                 lock.readLock().lock();
527                 try {
528                         return allImages.get(imageId);
529                 } finally {
530                         lock.readLock().unlock();
531                 }
532         }
533
534         //
535         // IMAGEBUILDERFACTORY METHODS
536         //
537
538         @Override
539         public ImageBuilder newImageBuilder() {
540                 return new ImageBuilderImpl();
541         }
542
543         //
544         // IMAGESTORE METHODS
545         //
546
547         @Override
548         public void storeImage(Image image) {
549                 lock.writeLock().lock();
550                 try {
551                         allImages.put(image.getId(), image);
552                         soneImages.put(image.getSone().getId(), image);
553                 } finally {
554                         lock.writeLock().unlock();
555                 }
556         }
557
558         @Override
559         public void removeImage(Image image) {
560                 lock.writeLock().lock();
561                 try {
562                         allImages.remove(image.getId());
563                         soneImages.remove(image.getSone().getId(), image);
564                 } finally {
565                         lock.writeLock().unlock();
566                 }
567         }
568
569         @Override
570         public void bookmarkPost(Post post) {
571                 memoryBookmarkDatabase.bookmarkPost(post);
572         }
573
574         @Override
575         public void unbookmarkPost(Post post) {
576                 memoryBookmarkDatabase.unbookmarkPost(post);
577         }
578
579         @Override
580         public boolean isPostBookmarked(Post post) {
581                 return memoryBookmarkDatabase.isPostBookmarked(post);
582         }
583
584         @Override
585         public Set<Post> getBookmarkedPosts() {
586                 return memoryBookmarkDatabase.getBookmarkedPosts();
587         }
588
589         //
590         // PACKAGE-PRIVATE METHODS
591         //
592
593         /**
594          * Returns whether the given post is known.
595          *
596          * @param post
597          *              The post
598          * @return {@code true} if the post is known, {@code false} otherwise
599          */
600         boolean isPostKnown(Post post) {
601                 lock.readLock().lock();
602                 try {
603                         return knownPosts.contains(post.getId());
604                 } finally {
605                         lock.readLock().unlock();
606                 }
607         }
608
609         /**
610          * Sets whether the given post is known.
611          *
612          * @param post
613          *              The post
614          * @param known
615          *              {@code true} if the post is known, {@code false} otherwise
616          */
617         void setPostKnown(Post post, boolean known) {
618                 lock.writeLock().lock();
619                 try {
620                         if (known) {
621                                 knownPosts.add(post.getId());
622                         } else {
623                                 knownPosts.remove(post.getId());
624                         }
625                 } finally {
626                         lock.writeLock().unlock();
627                 }
628         }
629
630         /**
631          * Returns whether the given post reply is known.
632          *
633          * @param postReply
634          *              The post reply
635          * @return {@code true} if the given post reply is known, {@code false}
636          *         otherwise
637          */
638         boolean isPostReplyKnown(PostReply postReply) {
639                 lock.readLock().lock();
640                 try {
641                         return knownPostReplies.contains(postReply.getId());
642                 } finally {
643                         lock.readLock().unlock();
644                 }
645         }
646
647         /**
648          * Sets whether the given post reply is known.
649          *
650          * @param postReply
651          *              The post reply
652          * @param known
653          *              {@code true} if the post reply is known, {@code false} otherwise
654          */
655         void setPostReplyKnown(PostReply postReply, boolean known) {
656                 lock.writeLock().lock();
657                 try {
658                         if (known) {
659                                 knownPostReplies.add(postReply.getId());
660                         } else {
661                                 knownPostReplies.remove(postReply.getId());
662                         }
663                 } finally {
664                         lock.writeLock().unlock();
665                 }
666         }
667
668         //
669         // PRIVATE METHODS
670         //
671
672         /**
673          * Gets all posts for the given Sone, creating a new collection if there is
674          * none yet.
675          *
676          * @param soneId
677          *              The ID of the Sone to get the posts for
678          * @return All posts
679          */
680         private Collection<Post> getPostsFrom(String soneId) {
681                 lock.readLock().lock();
682                 try {
683                         return sonePosts.get(soneId);
684                 } finally {
685                         lock.readLock().unlock();
686                 }
687         }
688
689         /** Loads the known posts. */
690         private void loadKnownPosts() {
691                 Set<String> knownPosts = configurationLoader.loadKnownPosts();
692                 lock.writeLock().lock();
693                 try {
694                         this.knownPosts.clear();
695                         this.knownPosts.addAll(knownPosts);
696                 } finally {
697                         lock.writeLock().unlock();
698                 }
699         }
700
701         /**
702          * Saves the known posts to the configuration.
703          *
704          * @throws DatabaseException
705          *              if a configuration error occurs
706          */
707         private void saveKnownPosts() throws DatabaseException {
708                 lock.readLock().lock();
709                 try {
710                         int postCounter = 0;
711                         for (String knownPostId : knownPosts) {
712                                 configuration.getStringValue("KnownPosts/" + postCounter++ + "/ID").setValue(knownPostId);
713                         }
714                         configuration.getStringValue("KnownPosts/" + postCounter + "/ID").setValue(null);
715                 } catch (ConfigurationException ce1) {
716                         throw new DatabaseException("Could not save database.", ce1);
717                 } finally {
718                         lock.readLock().unlock();
719                 }
720         }
721
722         /** Loads the known post replies. */
723         private void loadKnownPostReplies() {
724                 Set<String> knownPostReplies = configurationLoader.loadKnownPostReplies();
725                 lock.writeLock().lock();
726                 try {
727                         this.knownPostReplies.clear();
728                         this.knownPostReplies.addAll(knownPostReplies);
729                 } finally {
730                         lock.writeLock().unlock();
731                 }
732         }
733
734         /**
735          * Saves the known post replies to the configuration.
736          *
737          * @throws DatabaseException
738          *              if a configuration error occurs
739          */
740         private void saveKnownPostReplies() throws DatabaseException {
741                 lock.readLock().lock();
742                 try {
743                         int replyCounter = 0;
744                         for (String knownReplyId : knownPostReplies) {
745                                 configuration.getStringValue("KnownReplies/" + replyCounter++ + "/ID").setValue(knownReplyId);
746                         }
747                         configuration.getStringValue("KnownReplies/" + replyCounter + "/ID").setValue(null);
748                 } catch (ConfigurationException ce1) {
749                         throw new DatabaseException("Could not save database.", ce1);
750                 } finally {
751                         lock.readLock().unlock();
752                 }
753         }
754
755 }