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