Add storeSone() method to sone database.
[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.Optional.fromNullable;
21 import static com.google.common.base.Preconditions.checkNotNull;
22 import static com.google.common.base.Predicates.not;
23 import static com.google.common.collect.FluentIterable.from;
24 import static java.util.Collections.emptyList;
25 import static net.pterodactylus.sone.data.Sone.LOCAL_SONE_FILTER;
26
27 import java.util.ArrayList;
28 import java.util.Collection;
29 import java.util.Collections;
30 import java.util.Comparator;
31 import java.util.HashMap;
32 import java.util.HashSet;
33 import java.util.List;
34 import java.util.Map;
35 import java.util.Set;
36 import java.util.concurrent.locks.ReadWriteLock;
37 import java.util.concurrent.locks.ReentrantReadWriteLock;
38
39 import net.pterodactylus.sone.data.Album;
40 import net.pterodactylus.sone.data.Image;
41 import net.pterodactylus.sone.data.Post;
42 import net.pterodactylus.sone.data.PostReply;
43 import net.pterodactylus.sone.data.Sone;
44 import net.pterodactylus.sone.data.impl.DefaultSoneBuilder;
45 import net.pterodactylus.sone.database.Database;
46 import net.pterodactylus.sone.database.DatabaseException;
47 import net.pterodactylus.sone.database.PostDatabase;
48 import net.pterodactylus.sone.database.SoneBuilder;
49 import net.pterodactylus.sone.freenet.wot.Identity;
50 import net.pterodactylus.util.config.Configuration;
51 import net.pterodactylus.util.config.ConfigurationException;
52
53 import com.google.common.base.Function;
54 import com.google.common.base.Optional;
55 import com.google.common.collect.ArrayListMultimap;
56 import com.google.common.collect.HashMultimap;
57 import com.google.common.collect.ListMultimap;
58 import com.google.common.collect.Maps;
59 import com.google.common.collect.Multimap;
60 import com.google.common.collect.SetMultimap;
61 import com.google.common.collect.SortedSetMultimap;
62 import com.google.common.collect.TreeMultimap;
63 import com.google.common.util.concurrent.AbstractService;
64 import com.google.inject.Inject;
65
66 /**
67  * Memory-based {@link PostDatabase} implementation.
68  *
69  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
70  */
71 public class MemoryDatabase extends AbstractService implements Database {
72
73         /** The lock. */
74         private final ReadWriteLock lock = new ReentrantReadWriteLock();
75
76         /** The configuration. */
77         private final Configuration configuration;
78
79         private final Map<String, Identity> identities = Maps.newHashMap();
80         private final Map<String, Sone> sones = new HashMap<String, Sone>();
81
82         /** All posts by their ID. */
83         private final Map<String, Post> allPosts = new HashMap<String, Post>();
84
85         /** All posts by their Sones. */
86         private final Multimap<String, Post> sonePosts = HashMultimap.create();
87         private final SetMultimap<String, String> likedPostsBySone = HashMultimap.create();
88         private final SetMultimap<String, String> postLikingSones = HashMultimap.create();
89
90         /** All posts by their recipient. */
91         private final Multimap<String, Post> recipientPosts = HashMultimap.create();
92
93         /** Whether posts are known. */
94         private final Set<String> knownPosts = new HashSet<String>();
95
96         /** All post replies by their ID. */
97         private final Map<String, PostReply> allPostReplies = new HashMap<String, PostReply>();
98         private final SetMultimap<String, String> likedPostRepliesBySone = HashMultimap.create();
99         private final SetMultimap<String, String> postReplyLikingSones = HashMultimap.create();
100
101         /** Replies sorted by Sone. */
102         private final SortedSetMultimap<String, PostReply> sonePostReplies = TreeMultimap.create(new Comparator<String>() {
103
104                 @Override
105                 public int compare(String leftString, String rightString) {
106                         return leftString.compareTo(rightString);
107                 }
108         }, PostReply.TIME_COMPARATOR);
109
110         /** Replies by post. */
111         private final SortedSetMultimap<String, PostReply> postReplies = TreeMultimap.create(new Comparator<String>() {
112
113                 @Override
114                 public int compare(String leftString, String rightString) {
115                         return leftString.compareTo(rightString);
116                 }
117         }, PostReply.TIME_COMPARATOR);
118
119         /** Whether post replies are known. */
120         private final Set<String> knownPostReplies = new HashSet<String>();
121
122         private final Map<String, Album> allAlbums = new HashMap<String, Album>();
123         private final ListMultimap<String, String> albumChildren = ArrayListMultimap.create();
124         private final ListMultimap<String, String> albumImages = ArrayListMultimap.create();
125
126         private final Map<String, Image> allImages = new HashMap<String, Image>();
127
128         /**
129          * Creates a new memory database.
130          *
131          * @param configuration
132          *              The configuration for loading and saving elements
133          */
134         @Inject
135         public MemoryDatabase(Configuration configuration) {
136                 this.configuration = configuration;
137         }
138
139         //
140         // DATABASE METHODS
141         //
142
143         @Override
144         public void save() throws DatabaseException {
145                 saveKnownPosts();
146                 saveKnownPostReplies();
147         }
148
149         //
150         // SERVICE METHODS
151         //
152
153         @Override
154         protected void doStart() {
155                 loadKnownPosts();
156                 loadKnownPostReplies();
157                 notifyStarted();
158         }
159
160         @Override
161         protected void doStop() {
162                 try {
163                         save();
164                         notifyStopped();
165                 } catch (DatabaseException de1) {
166                         notifyFailed(de1);
167                 }
168         }
169
170         @Override
171         public Optional<Identity> getIdentity(String identityId) {
172                 lock.readLock().lock();
173                 try {
174                         return fromNullable(identities.get(identityId));
175                 } finally {
176                         lock.readLock().unlock();
177                 }
178         }
179
180         @Override
181         public void storeIdentity(Identity identitiy) {
182                 lock.writeLock().lock();
183                 try {
184                         identities.put(identitiy.getId(), identitiy);
185                 } finally {
186                         lock.writeLock().unlock();
187                 }
188         }
189
190         @Override
191         public Function<String, Optional<Sone>> getSone() {
192                 return new Function<String, Optional<Sone>>() {
193                         @Override
194                         public Optional<Sone> apply(String soneId) {
195                                 return (soneId == null) ? Optional.<Sone>absent() : getSone(soneId);
196                         }
197                 };
198         }
199
200         @Override
201         public Optional<Sone> getSone(String soneId) {
202                 lock.readLock().lock();
203                 try {
204                         return fromNullable(sones.get(soneId));
205                 } finally {
206                         lock.readLock().unlock();
207                 }
208         }
209
210         @Override
211         public Collection<Sone> getSones() {
212                 lock.readLock().lock();
213                 try {
214                         return Collections.unmodifiableCollection(sones.values());
215                 } finally {
216                         lock.readLock().unlock();
217                 }
218         }
219
220         @Override
221         public Collection<Sone> getLocalSones() {
222                 lock.readLock().lock();
223                 try {
224                         return from(getSones()).filter(LOCAL_SONE_FILTER).toSet();
225                 } finally {
226                         lock.readLock().unlock();
227                 }
228         }
229
230         @Override
231         public Collection<Sone> getRemoteSones() {
232                 lock.readLock().lock();
233                 try {
234                         return from(getSones()).filter(not(LOCAL_SONE_FILTER)).toSet();
235                 } finally {
236                         lock.readLock().unlock();
237                 }
238         }
239
240         @Override
241         public void storeSone(Sone sone) {
242                 lock.writeLock().lock();
243                 try {
244                         sones.put(sone.getId(), sone);
245                 } finally {
246                         lock.writeLock().unlock();
247                 }
248         }
249
250         @Override
251         public SoneBuilder newSoneBuilder() {
252                 return new DefaultSoneBuilder(this) {
253                         @Override
254                         public Sone build(Optional<SoneCreated> soneCreated) throws IllegalStateException {
255                                 Sone sone = super.build(soneCreated);
256                                 lock.writeLock().lock();
257                                 try {
258                                         sones.put(sone.getId(), sone);
259                                 } finally {
260                                         lock.writeLock().unlock();
261                                 }
262                                 return sone;
263                         }
264                 };
265         }
266
267         //
268         // POSTPROVIDER METHODS
269         //
270
271         @Override
272         public Function<String, Optional<Post>> getPost() {
273                 return new Function<String, Optional<Post>>() {
274                         @Override
275                         public Optional<Post> apply(String postId) {
276                                 return (postId == null) ? Optional.<Post>absent() : getPost(postId);
277                         }
278                 };
279         }
280
281         @Override
282         public Optional<Post> getPost(String postId) {
283                 lock.readLock().lock();
284                 try {
285                         return fromNullable(allPosts.get(postId));
286                 } finally {
287                         lock.readLock().unlock();
288                 }
289         }
290
291         @Override
292         public Collection<Post> getPosts(String soneId) {
293                 lock.readLock().lock();
294                 try {
295                         return new HashSet<Post>(sonePosts.get(soneId));
296                 } finally {
297                         lock.readLock().unlock();
298                 }
299         }
300
301         @Override
302         public Collection<Post> getDirectedPosts(String recipientId) {
303                 lock.readLock().lock();
304                 try {
305                         Collection<Post> posts = recipientPosts.get(recipientId);
306                         return (posts == null) ? Collections.<Post>emptySet() : new HashSet<Post>(posts);
307                 } finally {
308                         lock.readLock().unlock();
309                 }
310         }
311
312         @Override
313         public void likePost(Post post, Sone localSone) {
314                 lock.writeLock().lock();
315                 try {
316                         likedPostsBySone.put(localSone.getId(), post.getId());
317                         postLikingSones.put(post.getId(), localSone.getId());
318                 } finally {
319                         lock.writeLock().unlock();
320                 }
321         }
322
323         @Override
324         public void unlikePost(Post post, Sone localSone) {
325                 lock.writeLock().lock();
326                 try {
327                         likedPostsBySone.remove(localSone.getId(), post.getId());
328                         postLikingSones.remove(post.getId(), localSone.getId());
329                 } finally {
330                         lock.writeLock().unlock();
331                 }
332         }
333
334         public boolean isLiked(Post post, Sone sone) {
335                 lock.readLock().lock();
336                 try {
337                         return likedPostsBySone.containsEntry(sone.getId(), post.getId());
338                 } finally {
339                         lock.readLock().unlock();
340                 }
341         }
342
343         @Override
344         public Set<Sone> getLikes(Post post) {
345                 lock.readLock().lock();
346                 try {
347                         return from(postLikingSones.get(post.getId())).transform(getSone()).transformAndConcat(this.<Sone>unwrap()).toSet();
348                 } finally {
349                         lock.readLock().unlock();
350                 }
351         }
352
353         //
354         // POSTSTORE METHODS
355         //
356
357         @Override
358         public void storePost(Post post) {
359                 checkNotNull(post, "post must not be null");
360                 lock.writeLock().lock();
361                 try {
362                         allPosts.put(post.getId(), post);
363                         sonePosts.put(post.getSone().getId(), post);
364                         if (post.getRecipientId().isPresent()) {
365                                 recipientPosts.put(post.getRecipientId().get(), post);
366                         }
367                 } finally {
368                         lock.writeLock().unlock();
369                 }
370         }
371
372         @Override
373         public void removePost(Post post) {
374                 checkNotNull(post, "post must not be null");
375                 lock.writeLock().lock();
376                 try {
377                         allPosts.remove(post.getId());
378                         sonePosts.remove(post.getSone().getId(), post);
379                         if (post.getRecipientId().isPresent()) {
380                                 recipientPosts.remove(post.getRecipientId().get(), post);
381                         }
382                         post.getSone().removePost(post);
383                 } finally {
384                         lock.writeLock().unlock();
385                 }
386         }
387
388         @Override
389         public void storePosts(Sone sone, Collection<Post> posts) throws IllegalArgumentException {
390                 checkNotNull(sone, "sone must not be null");
391                 /* verify that all posts are from the same Sone. */
392                 for (Post post : posts) {
393                         if (!sone.equals(post.getSone())) {
394                                 throw new IllegalArgumentException(String.format("Post from different Sone found: %s", post));
395                         }
396                 }
397
398                 lock.writeLock().lock();
399                 try {
400                         /* remove all posts by the Sone. */
401                         sonePosts.removeAll(sone.getId());
402                         for (Post post : posts) {
403                                 allPosts.remove(post.getId());
404                                 if (post.getRecipientId().isPresent()) {
405                                         recipientPosts.remove(post.getRecipientId().get(), post);
406                                 }
407                         }
408
409                         /* add new posts. */
410                         sonePosts.putAll(sone.getId(), posts);
411                         for (Post post : posts) {
412                                 allPosts.put(post.getId(), post);
413                                 if (post.getRecipientId().isPresent()) {
414                                         recipientPosts.put(post.getRecipientId().get(), post);
415                                 }
416                         }
417                 } finally {
418                         lock.writeLock().unlock();
419                 }
420         }
421
422         @Override
423         public void removePosts(Sone sone) {
424                 checkNotNull(sone, "sone must not be null");
425                 lock.writeLock().lock();
426                 try {
427                         /* remove all posts by the Sone. */
428                         sonePosts.removeAll(sone.getId());
429                         for (Post post : sone.getPosts()) {
430                                 allPosts.remove(post.getId());
431                                 if (post.getRecipientId().isPresent()) {
432                                         recipientPosts.remove(post.getRecipientId().get(), post);
433                                 }
434                         }
435                 } finally {
436                         lock.writeLock().unlock();
437                 }
438         }
439
440         //
441         // POSTREPLYPROVIDER METHODS
442         //
443
444         @Override
445         public Optional<PostReply> getPostReply(String id) {
446                 lock.readLock().lock();
447                 try {
448                         return fromNullable(allPostReplies.get(id));
449                 } finally {
450                         lock.readLock().unlock();
451                 }
452         }
453
454         @Override
455         public List<PostReply> getReplies(String postId) {
456                 lock.readLock().lock();
457                 try {
458                         if (!postReplies.containsKey(postId)) {
459                                 return emptyList();
460                         }
461                         return new ArrayList<PostReply>(postReplies.get(postId));
462                 } finally {
463                         lock.readLock().unlock();
464                 }
465         }
466
467         @Override
468         public void likePostReply(PostReply postReply, Sone localSone) {
469                 lock.writeLock().lock();
470                 try {
471                         likedPostRepliesBySone.put(localSone.getId(), postReply.getId());
472                         postReplyLikingSones.put(postReply.getId(), localSone.getId());
473                 } finally {
474                         lock.writeLock().unlock();
475                 }
476         }
477
478         @Override
479         public void unlikePostReply(PostReply postReply, Sone localSone) {
480                 lock.writeLock().lock();
481                 try {
482                         likedPostRepliesBySone.remove(localSone.getId(), postReply.getId());
483                         postReplyLikingSones.remove(postReply.getId(), localSone.getId());
484                 } finally {
485                         lock.writeLock().unlock();
486                 }
487         }
488
489         @Override
490         public boolean isLiked(PostReply postReply, Sone sone) {
491                 lock.readLock().lock();
492                 try {
493                         return postReplyLikingSones.containsEntry(postReply.getId(), sone.getId());
494                 } finally {
495                         lock.readLock().unlock();
496                 }
497         }
498
499         @Override
500         public Set<Sone> getLikes(PostReply postReply) {
501                 lock.readLock().lock();
502                 try {
503                         return from(postReplyLikingSones.get(postReply.getId())).transform(getSone()).transformAndConcat(this.<Sone>unwrap()).toSet();
504                 } finally {
505                         lock.readLock().unlock();
506                 }
507         }
508
509         //
510         // POSTREPLYSTORE METHODS
511         //
512
513         /**
514          * Returns whether the given post reply is known.
515          *
516          * @param postReply
517          *              The post reply
518          * @return {@code true} if the given post reply is known, {@code false}
519          *         otherwise
520          */
521         public boolean isPostReplyKnown(PostReply postReply) {
522                 lock.readLock().lock();
523                 try {
524                         return knownPostReplies.contains(postReply.getId());
525                 } finally {
526                         lock.readLock().unlock();
527                 }
528         }
529
530         @Override
531         public void setPostReplyKnown(PostReply postReply) {
532                 lock.writeLock().lock();
533                 try {
534                         knownPostReplies.add(postReply.getId());
535                 } finally {
536                         lock.writeLock().unlock();
537                 }
538         }
539
540         @Override
541         public void storePostReply(PostReply postReply) {
542                 lock.writeLock().lock();
543                 try {
544                         allPostReplies.put(postReply.getId(), postReply);
545                         postReplies.put(postReply.getPostId(), postReply);
546                 } finally {
547                         lock.writeLock().unlock();
548                 }
549         }
550
551         @Override
552         public void storePostReplies(Sone sone, Collection<PostReply> postReplies) {
553                 checkNotNull(sone, "sone must not be null");
554                 /* verify that all posts are from the same Sone. */
555                 for (PostReply postReply : postReplies) {
556                         if (!sone.equals(postReply.getSone())) {
557                                 throw new IllegalArgumentException(String.format("PostReply from different Sone found: %s", postReply));
558                         }
559                 }
560
561                 lock.writeLock().lock();
562                 try {
563                         /* remove all post replies of the Sone. */
564                         for (PostReply postReply : getRepliesFrom(sone.getId())) {
565                                 removePostReply(postReply);
566                         }
567                         for (PostReply postReply : postReplies) {
568                                 allPostReplies.put(postReply.getId(), postReply);
569                                 sonePostReplies.put(postReply.getSone().getId(), postReply);
570                                 this.postReplies.put(postReply.getPostId(), postReply);
571                         }
572                 } finally {
573                         lock.writeLock().unlock();
574                 }
575         }
576
577         @Override
578         public void removePostReply(PostReply postReply) {
579                 lock.writeLock().lock();
580                 try {
581                         allPostReplies.remove(postReply.getId());
582                         postReplies.remove(postReply.getPostId(), postReply);
583                 } finally {
584                         lock.writeLock().unlock();
585                 }
586         }
587
588         @Override
589         public void removePostReplies(Sone sone) {
590                 checkNotNull(sone, "sone must not be null");
591
592                 lock.writeLock().lock();
593                 try {
594                         for (PostReply postReply : sone.getReplies()) {
595                                 removePostReply(postReply);
596                         }
597                 } finally {
598                         lock.writeLock().unlock();
599                 }
600         }
601
602         //
603         // ALBUMPROVDER METHODS
604         //
605
606         @Override
607         public Optional<Album> getAlbum(String albumId) {
608                 lock.readLock().lock();
609                 try {
610                         return fromNullable(allAlbums.get(albumId));
611                 } finally {
612                         lock.readLock().unlock();
613                 }
614         }
615
616         @Override
617         public List<Album> getAlbums(Album parent) {
618                 lock.readLock().lock();
619                 try {
620                         return from(albumChildren.get(parent.getId())).transformAndConcat(getAlbum()).toList();
621                 } finally {
622                         lock.readLock().unlock();
623                 }
624         }
625
626         @Override
627         public void moveUp(Album album) {
628                 lock.writeLock().lock();
629                 try {
630                         List<String> albums = albumChildren.get(album.getParent().getId());
631                         int currentIndex = albums.indexOf(album.getId());
632                         if (currentIndex == 0) {
633                                 return;
634                         }
635                         albums.remove(album.getId());
636                         albums.add(currentIndex - 1, album.getId());
637                 } finally {
638                         lock.writeLock().unlock();
639                 }
640         }
641
642         @Override
643         public void moveDown(Album album) {
644                 lock.writeLock().lock();
645                 try {
646                         List<String> albums = albumChildren.get(album.getParent().getId());
647                         int currentIndex = albums.indexOf(album.getId());
648                         if (currentIndex == (albums.size() - 1)) {
649                                 return;
650                         }
651                         albums.remove(album.getId());
652                         albums.add(currentIndex + 1, album.getId());
653                 } finally {
654                         lock.writeLock().unlock();
655                 }
656         }
657
658         //
659         // ALBUMSTORE METHODS
660         //
661
662         @Override
663         public void storeAlbum(Album album) {
664                 lock.writeLock().lock();
665                 try {
666                         allAlbums.put(album.getId(), album);
667                         if (!album.isRoot()) {
668                                 albumChildren.put(album.getParent().getId(), album.getId());
669                         }
670                 } finally {
671                         lock.writeLock().unlock();
672                 }
673         }
674
675         @Override
676         public void removeAlbum(Album album) {
677                 lock.writeLock().lock();
678                 try {
679                         allAlbums.remove(album.getId());
680                         albumChildren.remove(album.getParent().getId(), album.getId());
681                 } finally {
682                         lock.writeLock().unlock();
683                 }
684         }
685
686         //
687         // IMAGEPROVIDER METHODS
688         //
689
690         @Override
691         public Optional<Image> getImage(String imageId) {
692                 lock.readLock().lock();
693                 try {
694                         return fromNullable(allImages.get(imageId));
695                 } finally {
696                         lock.readLock().unlock();
697                 }
698         }
699
700         @Override
701         public List<Image> getImages(Album parent) {
702                 lock.readLock().lock();
703                 try {
704                         return from(albumImages.get(parent.getId())).transformAndConcat(getImage()).toList();
705                 } finally {
706                         lock.readLock().unlock();
707                 }
708         }
709
710         @Override
711         public void moveUp(Image image) {
712                 lock.writeLock().lock();
713                 try {
714                         List<String> images = albumImages.get(image.getAlbum().getId());
715                         int currentIndex = images.indexOf(image.getId());
716                         if (currentIndex == 0) {
717                                 return;
718                         }
719                         images.remove(image.getId());
720                         images.add(currentIndex - 1, image.getId());
721                 } finally {
722                         lock.writeLock().unlock();
723                 }
724         }
725
726         @Override
727         public void moveDown(Image image) {
728                 lock.writeLock().lock();
729                 try {
730                         List<String> images = albumChildren.get(image.getAlbum().getId());
731                         int currentIndex = images.indexOf(image.getId());
732                         if (currentIndex == (images.size() - 1)) {
733                                 return;
734                         }
735                         images.remove(image.getId());
736                         images.add(currentIndex + 1, image.getId());
737                 } finally {
738                         lock.writeLock().unlock();
739                 }
740         }
741
742         //
743         // IMAGESTORE METHODS
744         //
745
746         @Override
747         public void storeImage(Image image) {
748                 lock.writeLock().lock();
749                 try {
750                         allImages.put(image.getId(), image);
751                         albumImages.put(image.getAlbum().getId(), image.getId());
752                 } finally {
753                         lock.writeLock().unlock();
754                 }
755         }
756
757         @Override
758         public void removeImage(Image image) {
759                 lock.writeLock().lock();
760                 try {
761                         allImages.remove(image.getId());
762                         albumImages.remove(image.getAlbum().getId(), image.getId());
763                 } finally {
764                         lock.writeLock().unlock();
765                 }
766         }
767
768         //
769         // PACKAGE-PRIVATE METHODS
770         //
771
772         /**
773          * Returns whether the given post is known.
774          *
775          * @param post
776          *              The post
777          * @return {@code true} if the post is known, {@code false} otherwise
778          */
779         boolean isPostKnown(Post post) {
780                 lock.readLock().lock();
781                 try {
782                         return knownPosts.contains(post.getId());
783                 } finally {
784                         lock.readLock().unlock();
785                 }
786         }
787
788         /**
789          * Sets whether the given post is known.
790          *
791          * @param post
792          *              The post
793          * @param known
794          *              {@code true} if the post is known, {@code false} otherwise
795          */
796         void setPostKnown(Post post, boolean known) {
797                 lock.writeLock().lock();
798                 try {
799                         if (known) {
800                                 knownPosts.add(post.getId());
801                         } else {
802                                 knownPosts.remove(post.getId());
803                         }
804                 } finally {
805                         lock.writeLock().unlock();
806                 }
807         }
808
809         //
810         // PRIVATE METHODS
811         //
812
813         /** Loads the known posts. */
814         private void loadKnownPosts() {
815                 lock.writeLock().lock();
816                 try {
817                         int postCounter = 0;
818                         while (true) {
819                                 String knownPostId = configuration.getStringValue("KnownPosts/" + postCounter++ + "/ID").getValue(null);
820                                 if (knownPostId == null) {
821                                         break;
822                                 }
823                                 knownPosts.add(knownPostId);
824                         }
825                 } finally {
826                         lock.writeLock().unlock();
827                 }
828         }
829
830         /**
831          * Saves the known posts to the configuration.
832          *
833          * @throws DatabaseException
834          *              if a configuration error occurs
835          */
836         private void saveKnownPosts() throws DatabaseException {
837                 lock.readLock().lock();
838                 try {
839                         int postCounter = 0;
840                         for (String knownPostId : knownPosts) {
841                                 configuration.getStringValue("KnownPosts/" + postCounter++ + "/ID").setValue(knownPostId);
842                         }
843                         configuration.getStringValue("KnownPosts/" + postCounter + "/ID").setValue(null);
844                 } catch (ConfigurationException ce1) {
845                         throw new DatabaseException("Could not save database.", ce1);
846                 } finally {
847                         lock.readLock().unlock();
848                 }
849         }
850
851         /**
852          * Returns all replies by the given Sone.
853          *
854          * @param id
855          *              The ID of the Sone
856          * @return The post replies of the Sone, sorted by time (newest first)
857          */
858         private Collection<PostReply> getRepliesFrom(String id) {
859                 lock.readLock().lock();
860                 try {
861                         if (sonePostReplies.containsKey(id)) {
862                                 return Collections.unmodifiableCollection(sonePostReplies.get(id));
863                         }
864                         return Collections.emptySet();
865                 } finally {
866                         lock.readLock().unlock();
867                 }
868         }
869
870         /** Loads the known post replies. */
871         private void loadKnownPostReplies() {
872                 lock.writeLock().lock();
873                 try {
874                         int replyCounter = 0;
875                         while (true) {
876                                 String knownReplyId = configuration.getStringValue("KnownReplies/" + replyCounter++ + "/ID").getValue(null);
877                                 if (knownReplyId == null) {
878                                         break;
879                                 }
880                                 knownPostReplies.add(knownReplyId);
881                         }
882                 } finally {
883                         lock.writeLock().unlock();
884                 }
885         }
886
887         /**
888          * Saves the known post replies to the configuration.
889          *
890          * @throws DatabaseException
891          *              if a configuration error occurs
892          */
893         private void saveKnownPostReplies() throws DatabaseException {
894                 lock.readLock().lock();
895                 try {
896                         int replyCounter = 0;
897                         for (String knownReplyId : knownPostReplies) {
898                                 configuration.getStringValue("KnownReplies/" + replyCounter++ + "/ID").setValue(knownReplyId);
899                         }
900                         configuration.getStringValue("KnownReplies/" + replyCounter + "/ID").setValue(null);
901                 } catch (ConfigurationException ce1) {
902                         throw new DatabaseException("Could not save database.", ce1);
903                 } finally {
904                         lock.readLock().unlock();
905                 }
906         }
907
908         private Function<String, Iterable<Album>> getAlbum() {
909                 return new Function<String, Iterable<Album>>() {
910                         @Override
911                         public Iterable<Album> apply(String input) {
912                                 return (input == null) ? Collections.<Album>emptyList() : getAlbum(input).asSet();
913                         }
914                 };
915         }
916
917         private Function<String, Iterable<Image>> getImage() {
918                 return new Function<String, Iterable<Image>>() {
919                         @Override
920                         public Iterable<Image> apply(String input) {
921                                 return (input == null) ? Collections.<Image>emptyList() : getImage(input).asSet();
922                         }
923                 };
924         }
925
926         private static <T> Function<Optional<T>, Iterable<T>> unwrap() {
927                 return new Function<Optional<T>, Iterable<T>>() {
928                         @Override
929                         public Iterable<T> apply(Optional<T> input) {
930                                 return (input == null) ? Collections.<T>emptyList() : input.asSet();
931                         }
932                 };
933         }
934
935 }