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