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