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