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