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