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