Replace Sone provider interface with Kotlin version
[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
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.sone.database.SoneProvider;
58 import net.pterodactylus.util.config.Configuration;
59 import net.pterodactylus.util.config.ConfigurationException;
60
61 import com.google.common.base.Optional;
62 import com.google.common.base.Predicate;
63 import com.google.common.collect.HashMultimap;
64 import com.google.common.collect.Multimap;
65 import com.google.common.collect.SortedSetMultimap;
66 import com.google.common.collect.TreeMultimap;
67 import com.google.common.util.concurrent.AbstractService;
68 import com.google.inject.Inject;
69 import com.google.inject.Singleton;
70 import kotlin.jvm.functions.Function1;
71
72 /**
73  * Memory-based {@link PostDatabase} implementation.
74  *
75  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
76  */
77 @Singleton
78 public class MemoryDatabase extends AbstractService implements Database {
79
80         /** The lock. */
81         private final ReadWriteLock lock = new ReentrantReadWriteLock();
82
83         /** The Sone provider. */
84         private final SoneProvider soneProvider;
85
86         /** The configuration. */
87         private final Configuration configuration;
88         private final ConfigurationLoader configurationLoader;
89
90         private final Map<String, Sone> allSones = new HashMap<String, Sone>();
91
92         /** All posts by their ID. */
93         private final Map<String, Post> allPosts = new HashMap<String, Post>();
94
95         /** All posts by their Sones. */
96         private final Multimap<String, Post> sonePosts = HashMultimap.create();
97
98         /** Whether posts are known. */
99         private final Set<String> knownPosts = new HashSet<String>();
100
101         /** All post replies by their ID. */
102         private final Map<String, PostReply> allPostReplies = new HashMap<String, PostReply>();
103
104         /** Replies sorted by Sone. */
105         private final SortedSetMultimap<String, PostReply> sonePostReplies = TreeMultimap.create(new Comparator<String>() {
106
107                 @Override
108                 public int compare(String leftString, String rightString) {
109                         return leftString.compareTo(rightString);
110                 }
111         }, TIME_COMPARATOR);
112
113         /** Whether post replies are known. */
114         private final Set<String> knownPostReplies = new HashSet<String>();
115
116         private final Map<String, Album> allAlbums = new HashMap<String, Album>();
117         private final Multimap<String, Album> soneAlbums = HashMultimap.create();
118
119         private final Map<String, Image> allImages = new HashMap<String, Image>();
120         private final Multimap<String, Image> soneImages = HashMultimap.create();
121
122         private final MemoryBookmarkDatabase memoryBookmarkDatabase;
123         private final MemoryFriendDatabase memoryFriendDatabase;
124
125         /**
126          * Creates a new memory database.
127          *
128          * @param soneProvider
129          *              The Sone provider
130          * @param configuration
131          *              The configuration for loading and saving elements
132          */
133         @Inject
134         public MemoryDatabase(SoneProvider soneProvider, Configuration configuration) {
135                 this.soneProvider = soneProvider;
136                 this.configuration = configuration;
137                 this.configurationLoader = new ConfigurationLoader(configuration);
138                 memoryBookmarkDatabase =
139                                 new MemoryBookmarkDatabase(this, configurationLoader);
140                 memoryFriendDatabase = new MemoryFriendDatabase(configurationLoader);
141         }
142
143         //
144         // DATABASE METHODS
145         //
146
147         /**
148          * Saves the database.
149          *
150          * @throws DatabaseException
151          *              if an error occurs while saving
152          */
153         @Override
154         public void save() throws DatabaseException {
155                 saveKnownPosts();
156                 saveKnownPostReplies();
157         }
158
159         //
160         // SERVICE METHODS
161         //
162
163         /** {@inheritDocs} */
164         @Override
165         protected void doStart() {
166                 memoryBookmarkDatabase.start();
167                 loadKnownPosts();
168                 loadKnownPostReplies();
169                 notifyStarted();
170         }
171
172         /** {@inheritDocs} */
173         @Override
174         protected void doStop() {
175                 try {
176                         memoryBookmarkDatabase.stop();
177                         save();
178                         notifyStopped();
179                 } catch (DatabaseException de1) {
180                         notifyFailed(de1);
181                 }
182         }
183
184         @Override
185         public SoneBuilder newSoneBuilder() {
186                 return new MemorySoneBuilder(this);
187         }
188
189         @Override
190         public void storeSone(Sone sone) {
191                 lock.writeLock().lock();
192                 try {
193                         removeSone(sone);
194
195                         allSones.put(sone.getId(), sone);
196                         sonePosts.putAll(sone.getId(), sone.getPosts());
197                         for (Post post : sone.getPosts()) {
198                                 allPosts.put(post.getId(), post);
199                         }
200                         sonePostReplies.putAll(sone.getId(), sone.getReplies());
201                         for (PostReply postReply : sone.getReplies()) {
202                                 allPostReplies.put(postReply.getId(), postReply);
203                         }
204                         soneAlbums.putAll(sone.getId(), toAllAlbums.apply(sone));
205                         for (Album album : toAllAlbums.apply(sone)) {
206                                 allAlbums.put(album.getId(), album);
207                         }
208                         soneImages.putAll(sone.getId(), toAllImages.apply(sone));
209                         for (Image image : toAllImages.apply(sone)) {
210                                 allImages.put(image.getId(), image);
211                         }
212                 } finally {
213                         lock.writeLock().unlock();
214                 }
215         }
216
217         @Override
218         public void removeSone(Sone sone) {
219                 lock.writeLock().lock();
220                 try {
221                         allSones.remove(sone.getId());
222                         Collection<Post> removedPosts = sonePosts.removeAll(sone.getId());
223                         for (Post removedPost : removedPosts) {
224                                 allPosts.remove(removedPost.getId());
225                         }
226                         Collection<PostReply> removedPostReplies =
227                                         sonePostReplies.removeAll(sone.getId());
228                         for (PostReply removedPostReply : removedPostReplies) {
229                                 allPostReplies.remove(removedPostReply.getId());
230                         }
231                         Collection<Album> removedAlbums =
232                                         soneAlbums.removeAll(sone.getId());
233                         for (Album removedAlbum : removedAlbums) {
234                                 allAlbums.remove(removedAlbum.getId());
235                         }
236                         Collection<Image> removedImages =
237                                         soneImages.removeAll(sone.getId());
238                         for (Image removedImage : removedImages) {
239                                 allImages.remove(removedImage.getId());
240                         }
241                 } finally {
242                         lock.writeLock().unlock();
243                 }
244         }
245
246         @Nonnull
247         @Override
248         public Function1<String, Sone> getSoneLoader() {
249                 return new Function1<String, Sone>() {
250                         @Override
251                         public Sone invoke(String soneId) {
252                                 return getSone(soneId);
253                         }
254                 };
255         }
256
257         @Override
258         public Sone getSone(String soneId) {
259                 lock.readLock().lock();
260                 try {
261                         return allSones.get(soneId);
262                 } finally {
263                         lock.readLock().unlock();
264                 }
265         }
266
267         @Override
268         public Collection<Sone> getSones() {
269                 lock.readLock().lock();
270                 try {
271                         return new HashSet<Sone>(allSones.values());
272                 } finally {
273                         lock.readLock().unlock();
274                 }
275         }
276
277         @Override
278         public Collection<Sone> getLocalSones() {
279                 lock.readLock().lock();
280                 try {
281                         return from(allSones.values()).filter(LOCAL_SONE_FILTER).toSet();
282                 } finally {
283                         lock.readLock().unlock();
284                 }
285         }
286
287         @Override
288         public Collection<Sone> getRemoteSones() {
289                 lock.readLock().lock();
290                 try {
291                         return from(allSones.values())
292                                         .filter(not(LOCAL_SONE_FILTER)) .toSet();
293                 } finally {
294                         lock.readLock().unlock();
295                 }
296         }
297
298         @Override
299         public Collection<String> getFriends(Sone localSone) {
300                 if (!localSone.isLocal()) {
301                         return Collections.emptySet();
302                 }
303                 return memoryFriendDatabase.getFriends(localSone.getId());
304         }
305
306         @Override
307         public boolean isFriend(Sone localSone, String friendSoneId) {
308                 if (!localSone.isLocal()) {
309                         return false;
310                 }
311                 return memoryFriendDatabase.isFriend(localSone.getId(), friendSoneId);
312         }
313
314         @Override
315         public void addFriend(Sone localSone, String friendSoneId) {
316                 if (!localSone.isLocal()) {
317                         return;
318                 }
319                 memoryFriendDatabase.addFriend(localSone.getId(), friendSoneId);
320         }
321
322         @Override
323         public void removeFriend(Sone localSone, String friendSoneId) {
324                 if (!localSone.isLocal()) {
325                         return;
326                 }
327                 memoryFriendDatabase.removeFriend(localSone.getId(), friendSoneId);
328         }
329
330         //
331         // POSTPROVIDER METHODS
332         //
333
334         /** {@inheritDocs} */
335         @Override
336         public Optional<Post> getPost(String postId) {
337                 lock.readLock().lock();
338                 try {
339                         return fromNullable(allPosts.get(postId));
340                 } finally {
341                         lock.readLock().unlock();
342                 }
343         }
344
345         /** {@inheritDocs} */
346         @Override
347         public Collection<Post> getPosts(String soneId) {
348                 return new HashSet<Post>(getPostsFrom(soneId));
349         }
350
351         /** {@inheritDocs} */
352         @Override
353         public Collection<Post> getDirectedPosts(final String recipientId) {
354                 lock.readLock().lock();
355                 try {
356                         return from(sonePosts.values()).filter(new Predicate<Post>() {
357                                 @Override
358                                 public boolean apply(Post post) {
359                                         return post.getRecipientId().asSet().contains(recipientId);
360                                 }
361                         }).toSet();
362                 } finally {
363                         lock.readLock().unlock();
364                 }
365         }
366
367         //
368         // POSTBUILDERFACTORY METHODS
369         //
370
371         /** {@inheritDocs} */
372         @Override
373         public PostBuilder newPostBuilder() {
374                 return new MemoryPostBuilder(this, soneProvider);
375         }
376
377         //
378         // POSTSTORE METHODS
379         //
380
381         /** {@inheritDocs} */
382         @Override
383         public void storePost(Post post) {
384                 checkNotNull(post, "post must not be null");
385                 lock.writeLock().lock();
386                 try {
387                         allPosts.put(post.getId(), post);
388                         getPostsFrom(post.getSone().getId()).add(post);
389                 } finally {
390                         lock.writeLock().unlock();
391                 }
392         }
393
394         /** {@inheritDocs} */
395         @Override
396         public void removePost(Post post) {
397                 checkNotNull(post, "post must not be null");
398                 lock.writeLock().lock();
399                 try {
400                         allPosts.remove(post.getId());
401                         getPostsFrom(post.getSone().getId()).remove(post);
402                         post.getSone().removePost(post);
403                 } finally {
404                         lock.writeLock().unlock();
405                 }
406         }
407
408         //
409         // POSTREPLYPROVIDER METHODS
410         //
411
412         /** {@inheritDocs} */
413         @Override
414         public Optional<PostReply> getPostReply(String id) {
415                 lock.readLock().lock();
416                 try {
417                         return fromNullable(allPostReplies.get(id));
418                 } finally {
419                         lock.readLock().unlock();
420                 }
421         }
422
423         /** {@inheritDocs} */
424         @Override
425         public List<PostReply> getReplies(final String postId) {
426                 lock.readLock().lock();
427                 try {
428                         return from(allPostReplies.values())
429                                         .filter(new Predicate<PostReply>() {
430                                                 @Override
431                                                 public boolean apply(PostReply postReply) {
432                                                         return postReply.getPostId().equals(postId);
433                                                 }
434                                         }).toSortedList(TIME_COMPARATOR);
435                 } finally {
436                         lock.readLock().unlock();
437                 }
438         }
439
440         //
441         // POSTREPLYBUILDERFACTORY METHODS
442         //
443
444         /** {@inheritDocs} */
445         @Override
446         public PostReplyBuilder newPostReplyBuilder() {
447                 return new MemoryPostReplyBuilder(this, soneProvider);
448         }
449
450         //
451         // POSTREPLYSTORE METHODS
452         //
453
454         /** {@inheritDocs} */
455         @Override
456         public void storePostReply(PostReply postReply) {
457                 lock.writeLock().lock();
458                 try {
459                         allPostReplies.put(postReply.getId(), postReply);
460                 } finally {
461                         lock.writeLock().unlock();
462                 }
463         }
464
465         /** {@inheritDocs} */
466         @Override
467         public void removePostReply(PostReply postReply) {
468                 lock.writeLock().lock();
469                 try {
470                         allPostReplies.remove(postReply.getId());
471                 } finally {
472                         lock.writeLock().unlock();
473                 }
474         }
475
476         //
477         // ALBUMPROVDER METHODS
478         //
479
480         @Override
481         public Optional<Album> getAlbum(String albumId) {
482                 lock.readLock().lock();
483                 try {
484                         return fromNullable(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         @Override
530         public Optional<Image> getImage(String imageId) {
531                 lock.readLock().lock();
532                 try {
533                         return fromNullable(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 }