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