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