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