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