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