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