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