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