Merge branch 'release-0.9.8'
[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.sone.database.SoneProvider;
59 import net.pterodactylus.util.config.Configuration;
60 import net.pterodactylus.util.config.ConfigurationException;
61
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         @Nullable
335         @Override
336         public Post getPost(@Nonnull String postId) {
337                 lock.readLock().lock();
338                 try {
339                         return 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         @Nullable
413         @Override
414         public PostReply getPostReply(String id) {
415                 lock.readLock().lock();
416                 try {
417                         return 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         @Nullable
481         @Override
482         public Album getAlbum(@Nonnull String albumId) {
483                 lock.readLock().lock();
484                 try {
485                         return allAlbums.get(albumId);
486                 } finally {
487                         lock.readLock().unlock();
488                 }
489         }
490
491         //
492         // ALBUMBUILDERFACTORY METHODS
493         //
494
495         @Override
496         public AlbumBuilder newAlbumBuilder() {
497                 return new AlbumBuilderImpl();
498         }
499
500         //
501         // ALBUMSTORE METHODS
502         //
503
504         @Override
505         public void storeAlbum(Album album) {
506                 lock.writeLock().lock();
507                 try {
508                         allAlbums.put(album.getId(), album);
509                         soneAlbums.put(album.getSone().getId(), album);
510                 } finally {
511                         lock.writeLock().unlock();
512                 }
513         }
514
515         @Override
516         public void removeAlbum(Album album) {
517                 lock.writeLock().lock();
518                 try {
519                         allAlbums.remove(album.getId());
520                         soneAlbums.remove(album.getSone().getId(), album);
521                 } finally {
522                         lock.writeLock().unlock();
523                 }
524         }
525
526         //
527         // IMAGEPROVIDER METHODS
528         //
529
530         @Nullable
531         @Override
532         public Image getImage(@Nonnull String imageId) {
533                 lock.readLock().lock();
534                 try {
535                         return allImages.get(imageId);
536                 } finally {
537                         lock.readLock().unlock();
538                 }
539         }
540
541         //
542         // IMAGEBUILDERFACTORY METHODS
543         //
544
545         @Override
546         public ImageBuilder newImageBuilder() {
547                 return new ImageBuilderImpl();
548         }
549
550         //
551         // IMAGESTORE METHODS
552         //
553
554         @Override
555         public void storeImage(Image image) {
556                 lock.writeLock().lock();
557                 try {
558                         allImages.put(image.getId(), image);
559                         soneImages.put(image.getSone().getId(), image);
560                 } finally {
561                         lock.writeLock().unlock();
562                 }
563         }
564
565         @Override
566         public void removeImage(Image image) {
567                 lock.writeLock().lock();
568                 try {
569                         allImages.remove(image.getId());
570                         soneImages.remove(image.getSone().getId(), image);
571                 } finally {
572                         lock.writeLock().unlock();
573                 }
574         }
575
576         @Override
577         public void bookmarkPost(Post post) {
578                 memoryBookmarkDatabase.bookmarkPost(post);
579         }
580
581         @Override
582         public void unbookmarkPost(Post post) {
583                 memoryBookmarkDatabase.unbookmarkPost(post);
584         }
585
586         @Override
587         public boolean isPostBookmarked(Post post) {
588                 return memoryBookmarkDatabase.isPostBookmarked(post);
589         }
590
591         @Override
592         public Set<Post> getBookmarkedPosts() {
593                 return memoryBookmarkDatabase.getBookmarkedPosts();
594         }
595
596         //
597         // PACKAGE-PRIVATE METHODS
598         //
599
600         /**
601          * Returns whether the given post is known.
602          *
603          * @param post
604          *              The post
605          * @return {@code true} if the post is known, {@code false} otherwise
606          */
607         boolean isPostKnown(Post post) {
608                 lock.readLock().lock();
609                 try {
610                         return knownPosts.contains(post.getId());
611                 } finally {
612                         lock.readLock().unlock();
613                 }
614         }
615
616         /**
617          * Sets whether the given post is known.
618          *
619          * @param post
620          *              The post
621          * @param known
622          *              {@code true} if the post is known, {@code false} otherwise
623          */
624         void setPostKnown(Post post, boolean known) {
625                 lock.writeLock().lock();
626                 try {
627                         if (known) {
628                                 knownPosts.add(post.getId());
629                         } else {
630                                 knownPosts.remove(post.getId());
631                         }
632                 } finally {
633                         lock.writeLock().unlock();
634                 }
635         }
636
637         /**
638          * Returns whether the given post reply is known.
639          *
640          * @param postReply
641          *              The post reply
642          * @return {@code true} if the given post reply is known, {@code false}
643          *         otherwise
644          */
645         boolean isPostReplyKnown(PostReply postReply) {
646                 lock.readLock().lock();
647                 try {
648                         return knownPostReplies.contains(postReply.getId());
649                 } finally {
650                         lock.readLock().unlock();
651                 }
652         }
653
654         /**
655          * Sets whether the given post reply is known.
656          *
657          * @param postReply
658          *              The post reply
659          * @param known
660          *              {@code true} if the post reply is known, {@code false} otherwise
661          */
662         void setPostReplyKnown(PostReply postReply, boolean known) {
663                 lock.writeLock().lock();
664                 try {
665                         if (known) {
666                                 knownPostReplies.add(postReply.getId());
667                         } else {
668                                 knownPostReplies.remove(postReply.getId());
669                         }
670                 } finally {
671                         lock.writeLock().unlock();
672                 }
673         }
674
675         //
676         // PRIVATE METHODS
677         //
678
679         /**
680          * Gets all posts for the given Sone, creating a new collection if there is
681          * none yet.
682          *
683          * @param soneId
684          *              The ID of the Sone to get the posts for
685          * @return All posts
686          */
687         private Collection<Post> getPostsFrom(String soneId) {
688                 lock.readLock().lock();
689                 try {
690                         return sonePosts.get(soneId);
691                 } finally {
692                         lock.readLock().unlock();
693                 }
694         }
695
696         /** Loads the known posts. */
697         private void loadKnownPosts() {
698                 Set<String> knownPosts = configurationLoader.loadKnownPosts();
699                 lock.writeLock().lock();
700                 try {
701                         this.knownPosts.clear();
702                         this.knownPosts.addAll(knownPosts);
703                 } finally {
704                         lock.writeLock().unlock();
705                 }
706         }
707
708         /**
709          * Saves the known posts to the configuration.
710          *
711          * @throws DatabaseException
712          *              if a configuration error occurs
713          */
714         private void saveKnownPosts() throws DatabaseException {
715                 lock.readLock().lock();
716                 try {
717                         int postCounter = 0;
718                         for (String knownPostId : knownPosts) {
719                                 configuration.getStringValue("KnownPosts/" + postCounter++ + "/ID").setValue(knownPostId);
720                         }
721                         configuration.getStringValue("KnownPosts/" + postCounter + "/ID").setValue(null);
722                 } catch (ConfigurationException ce1) {
723                         throw new DatabaseException("Could not save database.", ce1);
724                 } finally {
725                         lock.readLock().unlock();
726                 }
727         }
728
729         /** Loads the known post replies. */
730         private void loadKnownPostReplies() {
731                 Set<String> knownPostReplies = configurationLoader.loadKnownPostReplies();
732                 lock.writeLock().lock();
733                 try {
734                         this.knownPostReplies.clear();
735                         this.knownPostReplies.addAll(knownPostReplies);
736                 } finally {
737                         lock.writeLock().unlock();
738                 }
739         }
740
741         /**
742          * Saves the known post replies to the configuration.
743          *
744          * @throws DatabaseException
745          *              if a configuration error occurs
746          */
747         private void saveKnownPostReplies() throws DatabaseException {
748                 lock.readLock().lock();
749                 try {
750                         int replyCounter = 0;
751                         for (String knownReplyId : knownPostReplies) {
752                                 configuration.getStringValue("KnownReplies/" + replyCounter++ + "/ID").setValue(knownReplyId);
753                         }
754                         configuration.getStringValue("KnownReplies/" + replyCounter + "/ID").setValue(null);
755                 } catch (ConfigurationException ce1) {
756                         throw new DatabaseException("Could not save database.", ce1);
757                 } finally {
758                         lock.readLock().unlock();
759                 }
760         }
761
762 }