Return a nullable Image 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         @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 }