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