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