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