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