Move memory post reply builder to its own file.
[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                         for (PostReply postReply : postReplies) {
381                                 allPostReplies.put(postReply.getId(), postReply);
382                                 if (this.postReplies.containsKey(postReply.getPostId())) {
383                                         this.postReplies.get(postReply.getPostId()).add(postReply);
384                                 } else {
385                                         TreeSet<PostReply> replies = new TreeSet<PostReply>(Reply.TIME_COMPARATOR);
386                                         replies.add(postReply);
387                                         this.postReplies.put(postReply.getPostId(), replies);
388                                 }
389                         }
390                 } finally {
391                         lock.writeLock().unlock();
392                 }
393         }
394
395         /**
396          * {@inheritDocs}
397          */
398         @Override
399         public void removePostReply(PostReply postReply) {
400                 lock.writeLock().lock();
401                 try {
402                         allPostReplies.remove(postReply.getId());
403                         if (postReplies.containsKey(postReply.getPostId())) {
404                                 postReplies.get(postReply.getPostId()).remove(postReply);
405                                 if (postReplies.get(postReply.getPostId()).isEmpty()) {
406                                         postReplies.remove(postReply.getPostId());
407                                 }
408                         }
409                 } finally {
410                         lock.writeLock().unlock();
411                 }
412         }
413
414         /**
415          * {@inheritDocs}
416          */
417         @Override
418         public void removePostReplies(Sone sone) {
419                 checkNotNull(sone, "sone must not be null");
420
421                 lock.writeLock().lock();
422                 try {
423                         for (PostReply postReply : sone.getReplies()) {
424                                 removePostReply(postReply);
425                         }
426                 } finally {
427                         lock.writeLock().unlock();
428                 }
429         }
430
431         //
432         // PACKAGE-PRIVATE METHODS
433         //
434
435         /**
436          * Returns whether the given post is known.
437          *
438          * @param post
439          *            The post
440          * @return {@code true} if the post is known, {@code false} otherwise
441          */
442         boolean isPostKnown(Post post) {
443                 lock.readLock().lock();
444                 try {
445                         return knownPosts.contains(post.getId());
446                 } finally {
447                         lock.readLock().unlock();
448                 }
449         }
450
451         /**
452          * Sets whether the given post is known.
453          *
454          * @param post
455          *            The post
456          * @param known
457          *            {@code true} if the post is known, {@code false} otherwise
458          */
459         void setPostKnown(Post post, boolean known) {
460                 lock.writeLock().lock();
461                 try {
462                         if (known) {
463                                 knownPosts.add(post.getId());
464                         } else {
465                                 knownPosts.remove(post.getId());
466                         }
467                 } finally {
468                         lock.writeLock().unlock();
469                 }
470         }
471
472         /**
473          * Returns whether the given post reply is known.
474          *
475          * @param postReply
476          *            The post reply
477          * @return {@code true} if the given post reply is known, {@code false}
478          *         otherwise
479          */
480         boolean isPostReplyKnown(PostReply postReply) {
481                 lock.readLock().lock();
482                 try {
483                         return knownPostReplies.contains(postReply.getId());
484                 } finally {
485                         lock.readLock().unlock();
486                 }
487         }
488
489         /**
490          * Sets whether the given post reply is known.
491          *
492          * @param postReply
493          *            The post reply
494          * @param known
495          *            {@code true} if the post reply is known, {@code false}
496          *            otherwise
497          */
498         void setPostReplyKnown(PostReply postReply, boolean known) {
499                 lock.writeLock().lock();
500                 try {
501                         if (known) {
502                                 knownPostReplies.add(postReply.getId());
503                         } else {
504                                 knownPostReplies.remove(postReply.getId());
505                         }
506                 } finally {
507                         lock.writeLock().unlock();
508                 }
509         }
510
511         //
512         // PRIVATE METHODS
513         //
514
515         /**
516          * Gets all posts for the given Sone, creating a new collection if there is
517          * none yet.
518          *
519          * @param soneId
520          *            The ID of the Sone to get the posts for
521          * @return All posts
522          */
523         private Collection<Post> getPostsFrom(String soneId) {
524                 Collection<Post> posts = null;
525                 lock.readLock().lock();
526                 try {
527                         posts = sonePosts.get(soneId);
528                 } finally {
529                         lock.readLock().unlock();
530                 }
531                 if (posts != null) {
532                         return posts;
533                 }
534
535                 posts = new HashSet<Post>();
536                 lock.writeLock().lock();
537                 try {
538                         sonePosts.put(soneId, posts);
539                 } finally {
540                         lock.writeLock().unlock();
541                 }
542
543                 return posts;
544         }
545
546         /**
547          * Gets all posts that are directed the given Sone, creating a new
548          * collection if there is none yet.
549          *
550          * @param recipientId
551          *            The ID of the Sone to get the posts for
552          * @return All posts
553          */
554         private Collection<Post> getPostsTo(String recipientId) {
555                 Collection<Post> posts = null;
556                 lock.readLock().lock();
557                 try {
558                         posts = recipientPosts.get(recipientId);
559                 } finally {
560                         lock.readLock().unlock();
561                 }
562                 if (posts != null) {
563                         return posts;
564                 }
565
566                 posts = new HashSet<Post>();
567                 lock.writeLock().lock();
568                 try {
569                         recipientPosts.put(recipientId, posts);
570                 } finally {
571                         lock.writeLock().unlock();
572                 }
573
574                 return posts;
575         }
576
577         /**
578          * Loads the known posts.
579          */
580         private void loadKnownPosts() {
581                 lock.writeLock().lock();
582                 try {
583                         int postCounter = 0;
584                         while (true) {
585                                 String knownPostId = configuration.getStringValue("KnownPosts/" + postCounter++ + "/ID").getValue(null);
586                                 if (knownPostId == null) {
587                                         break;
588                                 }
589                                 knownPosts.add(knownPostId);
590                         }
591                 } finally {
592                         lock.writeLock().unlock();
593                 }
594         }
595
596         /**
597          * Saves the known posts to the configuration.
598          *
599          * @throws DatabaseException
600          *             if a configuration error occurs
601          */
602         private void saveKnownPosts() throws DatabaseException {
603                 lock.readLock().lock();
604                 try {
605                         int postCounter = 0;
606                         for (String knownPostId : knownPosts) {
607                                 configuration.getStringValue("KnownPosts/" + postCounter++ + "/ID").setValue(knownPostId);
608                         }
609                         configuration.getStringValue("KnownPosts/" + postCounter + "/ID").setValue(null);
610                 } catch (ConfigurationException ce1) {
611                         throw new DatabaseException("Could not save database.", ce1);
612                 } finally {
613                         lock.readLock().unlock();
614                 }
615         }
616
617         /**
618          * Loads the known post replies.
619          */
620         private void loadKnownPostReplies() {
621                 lock.writeLock().lock();
622                 try {
623                         int replyCounter = 0;
624                         while (true) {
625                                 String knownReplyId = configuration.getStringValue("KnownReplies/" + replyCounter++ + "/ID").getValue(null);
626                                 if (knownReplyId == null) {
627                                         break;
628                                 }
629                                 knownPostReplies.add(knownReplyId);
630                         }
631                 } finally {
632                         lock.writeLock().unlock();
633                 }
634         }
635
636         /**
637          * Saves the known post replies to the configuration.
638          *
639          * @throws DatabaseException
640          *             if a configuration error occurs
641          */
642         private void saveKnownPostReplies() throws DatabaseException {
643                 lock.readLock().lock();
644                 try {
645                         int replyCounter = 0;
646                         for (String knownReplyId : knownPostReplies) {
647                                 configuration.getStringValue("KnownReplies/" + replyCounter++ + "/ID").setValue(knownReplyId);
648                         }
649                         configuration.getStringValue("KnownReplies/" + replyCounter + "/ID").setValue(null);
650                 } catch (ConfigurationException ce1) {
651                         throw new DatabaseException("Could not save database.", ce1);
652                 } finally {
653                         lock.readLock().unlock();
654                 }
655         }
656
657 }