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