4bf66437f544557659aa412f8b155c69fa173eb6
[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         /**
313          * Returns whether the given post is known.
314          *
315          * @param post
316          *              The post
317          * @return {@code true} if the post is known, {@code false} otherwise
318          */
319         @Override
320         public boolean isPostKnown(Post post) {
321                 lock.readLock().lock();
322                 try {
323                         return knownPosts.contains(post.getId());
324                 } finally {
325                         lock.readLock().unlock();
326                 }
327         }
328
329         /**
330          * Sets whether the given post is known.
331          *
332          * @param post
333          *              The post
334          */
335         @Override
336         public void setPostKnown(Post post) {
337                 lock.writeLock().lock();
338                 try {
339                         knownPosts.add(post.getId());
340                 } finally {
341                         lock.writeLock().unlock();
342                 }
343         }
344
345         @Override
346         public void likePost(Post post, Sone localSone) {
347                 lock.writeLock().lock();
348                 try {
349                         likedPostsBySone.put(localSone.getId(), post.getId());
350                         postLikingSones.put(post.getId(), localSone.getId());
351                 } finally {
352                         lock.writeLock().unlock();
353                 }
354         }
355
356         @Override
357         public void unlikePost(Post post, Sone localSone) {
358                 lock.writeLock().lock();
359                 try {
360                         likedPostsBySone.remove(localSone.getId(), post.getId());
361                         postLikingSones.remove(post.getId(), localSone.getId());
362                 } finally {
363                         lock.writeLock().unlock();
364                 }
365         }
366
367         public boolean isLiked(Post post, Sone sone) {
368                 lock.readLock().lock();
369                 try {
370                         return likedPostsBySone.containsEntry(sone.getId(), post.getId());
371                 } finally {
372                         lock.readLock().unlock();
373                 }
374         }
375
376         @Override
377         public Set<Sone> getLikes(Post post) {
378                 lock.readLock().lock();
379                 try {
380                         return from(postLikingSones.get(post.getId())).transform(getSone()).transformAndConcat(this.<Sone>unwrap()).toSet();
381                 } finally {
382                         lock.readLock().unlock();
383                 }
384         }
385
386         //
387         // POSTSTORE METHODS
388         //
389
390         @Override
391         public void storePost(Post post) {
392                 checkNotNull(post, "post must not be null");
393                 lock.writeLock().lock();
394                 try {
395                         allPosts.put(post.getId(), post);
396                         sonePosts.put(post.getSone().getId(), post);
397                         if (post.getRecipientId().isPresent()) {
398                                 recipientPosts.put(post.getRecipientId().get(), post);
399                         }
400                 } finally {
401                         lock.writeLock().unlock();
402                 }
403         }
404
405         @Override
406         public void removePost(Post post) {
407                 checkNotNull(post, "post must not be null");
408                 lock.writeLock().lock();
409                 try {
410                         allPosts.remove(post.getId());
411                         sonePosts.remove(post.getSone().getId(), post);
412                         if (post.getRecipientId().isPresent()) {
413                                 recipientPosts.remove(post.getRecipientId().get(), post);
414                         }
415                         post.getSone().removePost(post);
416                 } finally {
417                         lock.writeLock().unlock();
418                 }
419         }
420
421         @Override
422         public void storePosts(Sone sone, Collection<Post> posts) throws IllegalArgumentException {
423                 checkNotNull(sone, "sone must not be null");
424                 /* verify that all posts are from the same Sone. */
425                 for (Post post : posts) {
426                         if (!sone.equals(post.getSone())) {
427                                 throw new IllegalArgumentException(String.format("Post from different Sone found: %s", post));
428                         }
429                 }
430
431                 lock.writeLock().lock();
432                 try {
433                         /* remove all posts by the Sone. */
434                         sonePosts.removeAll(sone.getId());
435                         for (Post post : posts) {
436                                 allPosts.remove(post.getId());
437                                 if (post.getRecipientId().isPresent()) {
438                                         recipientPosts.remove(post.getRecipientId().get(), post);
439                                 }
440                         }
441
442                         /* add new posts. */
443                         sonePosts.putAll(sone.getId(), posts);
444                         for (Post post : posts) {
445                                 allPosts.put(post.getId(), post);
446                                 if (post.getRecipientId().isPresent()) {
447                                         recipientPosts.put(post.getRecipientId().get(), post);
448                                 }
449                         }
450                 } finally {
451                         lock.writeLock().unlock();
452                 }
453         }
454
455         @Override
456         public void removePosts(Sone sone) {
457                 checkNotNull(sone, "sone must not be null");
458                 lock.writeLock().lock();
459                 try {
460                         /* remove all posts by the Sone. */
461                         sonePosts.removeAll(sone.getId());
462                         for (Post post : sone.getPosts()) {
463                                 allPosts.remove(post.getId());
464                                 if (post.getRecipientId().isPresent()) {
465                                         recipientPosts.remove(post.getRecipientId().get(), post);
466                                 }
467                         }
468                 } finally {
469                         lock.writeLock().unlock();
470                 }
471         }
472
473         //
474         // POSTREPLYPROVIDER METHODS
475         //
476
477         @Override
478         public Optional<PostReply> getPostReply(String id) {
479                 lock.readLock().lock();
480                 try {
481                         return fromNullable(allPostReplies.get(id));
482                 } finally {
483                         lock.readLock().unlock();
484                 }
485         }
486
487         @Override
488         public List<PostReply> getReplies(String postId) {
489                 lock.readLock().lock();
490                 try {
491                         if (!postReplies.containsKey(postId)) {
492                                 return emptyList();
493                         }
494                         return new ArrayList<PostReply>(postReplies.get(postId));
495                 } finally {
496                         lock.readLock().unlock();
497                 }
498         }
499
500         @Override
501         public void likePostReply(PostReply postReply, Sone localSone) {
502                 lock.writeLock().lock();
503                 try {
504                         likedPostRepliesBySone.put(localSone.getId(), postReply.getId());
505                         postReplyLikingSones.put(postReply.getId(), localSone.getId());
506                 } finally {
507                         lock.writeLock().unlock();
508                 }
509         }
510
511         @Override
512         public void unlikePostReply(PostReply postReply, Sone localSone) {
513                 lock.writeLock().lock();
514                 try {
515                         likedPostRepliesBySone.remove(localSone.getId(), postReply.getId());
516                         postReplyLikingSones.remove(postReply.getId(), localSone.getId());
517                 } finally {
518                         lock.writeLock().unlock();
519                 }
520         }
521
522         @Override
523         public boolean isLiked(PostReply postReply, Sone sone) {
524                 lock.readLock().lock();
525                 try {
526                         return postReplyLikingSones.containsEntry(postReply.getId(), sone.getId());
527                 } finally {
528                         lock.readLock().unlock();
529                 }
530         }
531
532         @Override
533         public Set<Sone> getLikes(PostReply postReply) {
534                 lock.readLock().lock();
535                 try {
536                         return from(postReplyLikingSones.get(postReply.getId())).transform(getSone()).transformAndConcat(this.<Sone>unwrap()).toSet();
537                 } finally {
538                         lock.readLock().unlock();
539                 }
540         }
541
542         //
543         // POSTREPLYSTORE METHODS
544         //
545
546         /**
547          * Returns whether the given post reply is known.
548          *
549          * @param postReply
550          *              The post reply
551          * @return {@code true} if the given post reply is known, {@code false}
552          *         otherwise
553          */
554         public boolean isPostReplyKnown(PostReply postReply) {
555                 lock.readLock().lock();
556                 try {
557                         return knownPostReplies.contains(postReply.getId());
558                 } finally {
559                         lock.readLock().unlock();
560                 }
561         }
562
563         @Override
564         public void setPostReplyKnown(PostReply postReply) {
565                 lock.writeLock().lock();
566                 try {
567                         knownPostReplies.add(postReply.getId());
568                 } finally {
569                         lock.writeLock().unlock();
570                 }
571         }
572
573         @Override
574         public void storePostReply(PostReply postReply) {
575                 lock.writeLock().lock();
576                 try {
577                         allPostReplies.put(postReply.getId(), postReply);
578                         postReplies.put(postReply.getPostId(), postReply);
579                 } finally {
580                         lock.writeLock().unlock();
581                 }
582         }
583
584         @Override
585         public void storePostReplies(Sone sone, Collection<PostReply> postReplies) {
586                 checkNotNull(sone, "sone must not be null");
587                 /* verify that all posts are from the same Sone. */
588                 for (PostReply postReply : postReplies) {
589                         if (!sone.equals(postReply.getSone())) {
590                                 throw new IllegalArgumentException(String.format("PostReply from different Sone found: %s", postReply));
591                         }
592                 }
593
594                 lock.writeLock().lock();
595                 try {
596                         /* remove all post replies of the Sone. */
597                         for (PostReply postReply : getRepliesFrom(sone.getId())) {
598                                 removePostReply(postReply);
599                         }
600                         for (PostReply postReply : postReplies) {
601                                 allPostReplies.put(postReply.getId(), postReply);
602                                 sonePostReplies.put(postReply.getSone().getId(), postReply);
603                                 this.postReplies.put(postReply.getPostId(), postReply);
604                         }
605                 } finally {
606                         lock.writeLock().unlock();
607                 }
608         }
609
610         @Override
611         public void removePostReply(PostReply postReply) {
612                 lock.writeLock().lock();
613                 try {
614                         allPostReplies.remove(postReply.getId());
615                         postReplies.remove(postReply.getPostId(), postReply);
616                 } finally {
617                         lock.writeLock().unlock();
618                 }
619         }
620
621         @Override
622         public void removePostReplies(Sone sone) {
623                 checkNotNull(sone, "sone must not be null");
624
625                 lock.writeLock().lock();
626                 try {
627                         for (PostReply postReply : sone.getReplies()) {
628                                 removePostReply(postReply);
629                         }
630                 } finally {
631                         lock.writeLock().unlock();
632                 }
633         }
634
635         //
636         // ALBUMPROVDER METHODS
637         //
638
639         @Override
640         public Optional<Album> getAlbum(String albumId) {
641                 lock.readLock().lock();
642                 try {
643                         return fromNullable(allAlbums.get(albumId));
644                 } finally {
645                         lock.readLock().unlock();
646                 }
647         }
648
649         @Override
650         public List<Album> getAlbums(Album parent) {
651                 lock.readLock().lock();
652                 try {
653                         return from(albumChildren.get(parent.getId())).transformAndConcat(getAlbum()).toList();
654                 } finally {
655                         lock.readLock().unlock();
656                 }
657         }
658
659         @Override
660         public void moveUp(Album album) {
661                 lock.writeLock().lock();
662                 try {
663                         List<String> albums = albumChildren.get(album.getParent().getId());
664                         int currentIndex = albums.indexOf(album.getId());
665                         if (currentIndex == 0) {
666                                 return;
667                         }
668                         albums.remove(album.getId());
669                         albums.add(currentIndex - 1, album.getId());
670                 } finally {
671                         lock.writeLock().unlock();
672                 }
673         }
674
675         @Override
676         public void moveDown(Album album) {
677                 lock.writeLock().lock();
678                 try {
679                         List<String> albums = albumChildren.get(album.getParent().getId());
680                         int currentIndex = albums.indexOf(album.getId());
681                         if (currentIndex == (albums.size() - 1)) {
682                                 return;
683                         }
684                         albums.remove(album.getId());
685                         albums.add(currentIndex + 1, album.getId());
686                 } finally {
687                         lock.writeLock().unlock();
688                 }
689         }
690
691         //
692         // ALBUMSTORE METHODS
693         //
694
695         @Override
696         public void storeAlbum(Album album) {
697                 lock.writeLock().lock();
698                 try {
699                         allAlbums.put(album.getId(), album);
700                         if (!album.isRoot()) {
701                                 albumChildren.put(album.getParent().getId(), album.getId());
702                         }
703                 } finally {
704                         lock.writeLock().unlock();
705                 }
706         }
707
708         @Override
709         public void removeAlbum(Album album) {
710                 lock.writeLock().lock();
711                 try {
712                         allAlbums.remove(album.getId());
713                         albumChildren.remove(album.getParent().getId(), album.getId());
714                 } finally {
715                         lock.writeLock().unlock();
716                 }
717         }
718
719         //
720         // IMAGEPROVIDER METHODS
721         //
722
723         @Override
724         public Optional<Image> getImage(String imageId) {
725                 lock.readLock().lock();
726                 try {
727                         return fromNullable(allImages.get(imageId));
728                 } finally {
729                         lock.readLock().unlock();
730                 }
731         }
732
733         @Override
734         public List<Image> getImages(Album parent) {
735                 lock.readLock().lock();
736                 try {
737                         return from(albumImages.get(parent.getId())).transformAndConcat(getImage()).toList();
738                 } finally {
739                         lock.readLock().unlock();
740                 }
741         }
742
743         @Override
744         public void moveUp(Image image) {
745                 lock.writeLock().lock();
746                 try {
747                         List<String> images = albumImages.get(image.getAlbum().getId());
748                         int currentIndex = images.indexOf(image.getId());
749                         if (currentIndex == 0) {
750                                 return;
751                         }
752                         images.remove(image.getId());
753                         images.add(currentIndex - 1, image.getId());
754                 } finally {
755                         lock.writeLock().unlock();
756                 }
757         }
758
759         @Override
760         public void moveDown(Image image) {
761                 lock.writeLock().lock();
762                 try {
763                         List<String> images = albumChildren.get(image.getAlbum().getId());
764                         int currentIndex = images.indexOf(image.getId());
765                         if (currentIndex == (images.size() - 1)) {
766                                 return;
767                         }
768                         images.remove(image.getId());
769                         images.add(currentIndex + 1, image.getId());
770                 } finally {
771                         lock.writeLock().unlock();
772                 }
773         }
774
775         //
776         // IMAGESTORE METHODS
777         //
778
779         @Override
780         public void storeImage(Image image) {
781                 lock.writeLock().lock();
782                 try {
783                         allImages.put(image.getId(), image);
784                         albumImages.put(image.getAlbum().getId(), image.getId());
785                 } finally {
786                         lock.writeLock().unlock();
787                 }
788         }
789
790         @Override
791         public void removeImage(Image image) {
792                 lock.writeLock().lock();
793                 try {
794                         allImages.remove(image.getId());
795                         albumImages.remove(image.getAlbum().getId(), image.getId());
796                 } finally {
797                         lock.writeLock().unlock();
798                 }
799         }
800
801         //
802         // PRIVATE METHODS
803         //
804
805         /** Loads the known posts. */
806         private void loadKnownPosts() {
807                 lock.writeLock().lock();
808                 try {
809                         int postCounter = 0;
810                         while (true) {
811                                 String knownPostId = configuration.getStringValue("KnownPosts/" + postCounter++ + "/ID").getValue(null);
812                                 if (knownPostId == null) {
813                                         break;
814                                 }
815                                 knownPosts.add(knownPostId);
816                         }
817                 } finally {
818                         lock.writeLock().unlock();
819                 }
820         }
821
822         /**
823          * Saves the known posts to the configuration.
824          *
825          * @throws DatabaseException
826          *              if a configuration error occurs
827          */
828         private void saveKnownPosts() throws DatabaseException {
829                 lock.readLock().lock();
830                 try {
831                         int postCounter = 0;
832                         for (String knownPostId : knownPosts) {
833                                 configuration.getStringValue("KnownPosts/" + postCounter++ + "/ID").setValue(knownPostId);
834                         }
835                         configuration.getStringValue("KnownPosts/" + postCounter + "/ID").setValue(null);
836                 } catch (ConfigurationException ce1) {
837                         throw new DatabaseException("Could not save database.", ce1);
838                 } finally {
839                         lock.readLock().unlock();
840                 }
841         }
842
843         /**
844          * Returns all replies by the given Sone.
845          *
846          * @param id
847          *              The ID of the Sone
848          * @return The post replies of the Sone, sorted by time (newest first)
849          */
850         private Collection<PostReply> getRepliesFrom(String id) {
851                 lock.readLock().lock();
852                 try {
853                         if (sonePostReplies.containsKey(id)) {
854                                 return Collections.unmodifiableCollection(sonePostReplies.get(id));
855                         }
856                         return Collections.emptySet();
857                 } finally {
858                         lock.readLock().unlock();
859                 }
860         }
861
862         /** Loads the known post replies. */
863         private void loadKnownPostReplies() {
864                 lock.writeLock().lock();
865                 try {
866                         int replyCounter = 0;
867                         while (true) {
868                                 String knownReplyId = configuration.getStringValue("KnownReplies/" + replyCounter++ + "/ID").getValue(null);
869                                 if (knownReplyId == null) {
870                                         break;
871                                 }
872                                 knownPostReplies.add(knownReplyId);
873                         }
874                 } finally {
875                         lock.writeLock().unlock();
876                 }
877         }
878
879         /**
880          * Saves the known post replies to the configuration.
881          *
882          * @throws DatabaseException
883          *              if a configuration error occurs
884          */
885         private void saveKnownPostReplies() throws DatabaseException {
886                 lock.readLock().lock();
887                 try {
888                         int replyCounter = 0;
889                         for (String knownReplyId : knownPostReplies) {
890                                 configuration.getStringValue("KnownReplies/" + replyCounter++ + "/ID").setValue(knownReplyId);
891                         }
892                         configuration.getStringValue("KnownReplies/" + replyCounter + "/ID").setValue(null);
893                 } catch (ConfigurationException ce1) {
894                         throw new DatabaseException("Could not save database.", ce1);
895                 } finally {
896                         lock.readLock().unlock();
897                 }
898         }
899
900         private Function<String, Iterable<Album>> getAlbum() {
901                 return new Function<String, Iterable<Album>>() {
902                         @Override
903                         public Iterable<Album> apply(String input) {
904                                 return (input == null) ? Collections.<Album>emptyList() : getAlbum(input).asSet();
905                         }
906                 };
907         }
908
909         private Function<String, Iterable<Image>> getImage() {
910                 return new Function<String, Iterable<Image>>() {
911                         @Override
912                         public Iterable<Image> apply(String input) {
913                                 return (input == null) ? Collections.<Image>emptyList() : getImage(input).asSet();
914                         }
915                 };
916         }
917
918         private static <T> Function<Optional<T>, Iterable<T>> unwrap() {
919                 return new Function<Optional<T>, Iterable<T>>() {
920                         @Override
921                         public Iterable<T> apply(Optional<T> input) {
922                                 return (input == null) ? Collections.<T>emptyList() : input.asSet();
923                         }
924                 };
925         }
926
927 }