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