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