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