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