Remove getAllAlbums() method, use album flattener.
[Sone.git] / src / main / java / net / pterodactylus / sone / data / Sone.java
1 /*
2  * Sone - Sone.java - Copyright © 2010–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.data;
19
20 import static com.google.common.base.Preconditions.checkArgument;
21 import static com.google.common.base.Preconditions.checkNotNull;
22
23 import java.util.ArrayList;
24 import java.util.Collection;
25 import java.util.Collections;
26 import java.util.Comparator;
27 import java.util.List;
28 import java.util.Set;
29 import java.util.concurrent.CopyOnWriteArrayList;
30 import java.util.concurrent.CopyOnWriteArraySet;
31 import java.util.logging.Level;
32 import java.util.logging.Logger;
33
34 import net.pterodactylus.sone.core.Options;
35 import net.pterodactylus.sone.freenet.wot.Identity;
36 import net.pterodactylus.sone.freenet.wot.OwnIdentity;
37 import net.pterodactylus.sone.template.SoneAccessor;
38 import net.pterodactylus.util.logging.Logging;
39
40 import com.google.common.base.Predicate;
41 import com.google.common.collect.FluentIterable;
42 import com.google.common.hash.Hasher;
43 import com.google.common.hash.Hashing;
44
45 import freenet.keys.FreenetURI;
46
47 /**
48  * A Sone defines everything about a user: her profile, her status updates, her
49  * replies, her likes and dislikes, etc.
50  * <p>
51  * Operations that modify the Sone need to synchronize on the Sone in question.
52  *
53  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
54  */
55 public class Sone implements Fingerprintable, Comparable<Sone> {
56
57         /**
58          * Enumeration for the possible states of a {@link Sone}.
59          *
60          * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
61          */
62         public enum SoneStatus {
63
64                 /** The Sone is unknown, i.e. not yet downloaded. */
65                 unknown,
66
67                 /** The Sone is idle, i.e. not being downloaded or inserted. */
68                 idle,
69
70                 /** The Sone is currently being inserted. */
71                 inserting,
72
73                 /** The Sone is currently being downloaded. */
74                 downloading,
75         }
76
77         /**
78          * The possible values for the “show custom avatars” option.
79          *
80          * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
81          */
82         public static enum ShowCustomAvatars {
83
84                 /** Never show custom avatars. */
85                 NEVER,
86
87                 /** Only show custom avatars of followed Sones. */
88                 FOLLOWED,
89
90                 /** Only show custom avatars of Sones you manually trust. */
91                 MANUALLY_TRUSTED,
92
93                 /** Only show custom avatars of automatically trusted Sones. */
94                 TRUSTED,
95
96                 /** Always show custom avatars. */
97                 ALWAYS,
98
99         }
100
101         /** comparator that sorts Sones by their nice name. */
102         public static final Comparator<Sone> NICE_NAME_COMPARATOR = new Comparator<Sone>() {
103
104                 @Override
105                 public int compare(Sone leftSone, Sone rightSone) {
106                         int diff = SoneAccessor.getNiceName(leftSone).compareToIgnoreCase(SoneAccessor.getNiceName(rightSone));
107                         if (diff != 0) {
108                                 return diff;
109                         }
110                         return leftSone.getId().compareToIgnoreCase(rightSone.getId());
111                 }
112
113         };
114
115         /**
116          * Comparator that sorts Sones by last activity (least recent active first).
117          */
118         public static final Comparator<Sone> LAST_ACTIVITY_COMPARATOR = new Comparator<Sone>() {
119
120                 @Override
121                 public int compare(Sone firstSone, Sone secondSone) {
122                         return (int) Math.min(Integer.MAX_VALUE, Math.max(Integer.MIN_VALUE, secondSone.getTime() - firstSone.getTime()));
123                 }
124         };
125
126         /** Comparator that sorts Sones by numbers of posts (descending). */
127         public static final Comparator<Sone> POST_COUNT_COMPARATOR = new Comparator<Sone>() {
128
129                 /**
130                  * {@inheritDoc}
131                  */
132                 @Override
133                 public int compare(Sone leftSone, Sone rightSone) {
134                         return (leftSone.getPosts().size() != rightSone.getPosts().size()) ? (rightSone.getPosts().size() - leftSone.getPosts().size()) : (rightSone.getReplies().size() - leftSone.getReplies().size());
135                 }
136         };
137
138         /** Comparator that sorts Sones by number of images (descending). */
139         public static final Comparator<Sone> IMAGE_COUNT_COMPARATOR = new Comparator<Sone>() {
140
141                 /**
142                  * {@inheritDoc}
143                  */
144                 @Override
145                 public int compare(Sone leftSone, Sone rightSone) {
146                         return rightSone.getAllImages().size() - leftSone.getAllImages().size();
147                 }
148         };
149
150         /** Filter to remove Sones that have not been downloaded. */
151         public static final Predicate<Sone> EMPTY_SONE_FILTER = new Predicate<Sone>() {
152
153                 @Override
154                 public boolean apply(Sone sone) {
155                         return sone.getTime() != 0;
156                 }
157         };
158
159         /** Filter that matches all {@link Sone#isLocal() local Sones}. */
160         public static final Predicate<Sone> LOCAL_SONE_FILTER = new Predicate<Sone>() {
161
162                 @Override
163                 public boolean apply(Sone sone) {
164                         return sone.getIdentity() instanceof OwnIdentity;
165                 }
166
167         };
168
169         /** Filter that matches Sones that have at least one album. */
170         public static final Predicate<Sone> HAS_ALBUM_FILTER = new Predicate<Sone>() {
171
172                 @Override
173                 public boolean apply(Sone sone) {
174                         return !sone.getAlbums().isEmpty();
175                 }
176         };
177
178         /** The logger. */
179         private static final Logger logger = Logging.getLogger(Sone.class);
180
181         /** The ID of this Sone. */
182         private final String id;
183
184         /** Whether the Sone is local. */
185         private final boolean local;
186
187         /** The identity of this Sone. */
188         private Identity identity;
189
190         /** The URI under which the Sone is stored in Freenet. */
191         private volatile FreenetURI requestUri;
192
193         /** The URI used to insert a new version of this Sone. */
194         /* This will be null for remote Sones! */
195         private volatile FreenetURI insertUri;
196
197         /** The latest edition of the Sone. */
198         private volatile long latestEdition;
199
200         /** The time of the last inserted update. */
201         private volatile long time;
202
203         /** The status of this Sone. */
204         private volatile SoneStatus status = SoneStatus.unknown;
205
206         /** The profile of this Sone. */
207         private volatile Profile profile = new Profile(this);
208
209         /** The client used by the Sone. */
210         private volatile Client client;
211
212         /** Whether this Sone is known. */
213         private volatile boolean known;
214
215         /** All friend Sones. */
216         private final Set<String> friendSones = new CopyOnWriteArraySet<String>();
217
218         /** All posts. */
219         private final Set<Post> posts = new CopyOnWriteArraySet<Post>();
220
221         /** All replies. */
222         private final Set<PostReply> replies = new CopyOnWriteArraySet<PostReply>();
223
224         /** The IDs of all liked posts. */
225         private final Set<String> likedPostIds = new CopyOnWriteArraySet<String>();
226
227         /** The IDs of all liked replies. */
228         private final Set<String> likedReplyIds = new CopyOnWriteArraySet<String>();
229
230         /** The albums of this Sone. */
231         private final List<Album> albums = new CopyOnWriteArrayList<Album>();
232
233         /** Sone-specific options. */
234         private Options options = new Options();
235
236         /**
237          * Creates a new Sone.
238          *
239          * @param id
240          *            The ID of the Sone
241          * @param local
242          *            {@code true} if the Sone is a local Sone, {@code false}
243          *            otherwise
244          */
245         public Sone(String id, boolean local) {
246                 this.id = id;
247                 this.local = local;
248         }
249
250         //
251         // ACCESSORS
252         //
253
254         /**
255          * Returns the identity of this Sone.
256          *
257          * @return The identity of this Sone
258          */
259         public String getId() {
260                 return id;
261         }
262
263         /**
264          * Returns the identity of this Sone.
265          *
266          * @return The identity of this Sone
267          */
268         public Identity getIdentity() {
269                 return identity;
270         }
271
272         /**
273          * Sets the identity of this Sone. The {@link Identity#getId() ID} of the
274          * identity has to match this Sone’s {@link #getId()}.
275          *
276          * @param identity
277          *            The identity of this Sone
278          * @return This Sone (for method chaining)
279          * @throws IllegalArgumentException
280          *             if the ID of the identity does not match this Sone’s ID
281          */
282         public Sone setIdentity(Identity identity) throws IllegalArgumentException {
283                 if (!identity.getId().equals(id)) {
284                         throw new IllegalArgumentException("Identity’s ID does not match Sone’s ID!");
285                 }
286                 this.identity = identity;
287                 return this;
288         }
289
290         /**
291          * Returns the name of this Sone.
292          *
293          * @return The name of this Sone
294          */
295         public String getName() {
296                 return (identity != null) ? identity.getNickname() : null;
297         }
298
299         /**
300          * Returns whether this Sone is a local Sone.
301          *
302          * @return {@code true} if this Sone is a local Sone, {@code false}
303          *         otherwise
304          */
305         public boolean isLocal() {
306                 return local;
307         }
308
309         /**
310          * Returns the request URI of this Sone.
311          *
312          * @return The request URI of this Sone
313          */
314         public FreenetURI getRequestUri() {
315                 return (requestUri != null) ? requestUri.setSuggestedEdition(latestEdition) : null;
316         }
317
318         /**
319          * Sets the request URI of this Sone.
320          *
321          * @param requestUri
322          *            The request URI of this Sone
323          * @return This Sone (for method chaining)
324          */
325         public Sone setRequestUri(FreenetURI requestUri) {
326                 if (this.requestUri == null) {
327                         this.requestUri = requestUri.setKeyType("USK").setDocName("Sone").setMetaString(new String[0]);
328                         return this;
329                 }
330                 if (!this.requestUri.equalsKeypair(requestUri)) {
331                         logger.log(Level.WARNING, String.format("Request URI %s tried to overwrite %s!", requestUri, this.requestUri));
332                         return this;
333                 }
334                 return this;
335         }
336
337         /**
338          * Returns the insert URI of this Sone.
339          *
340          * @return The insert URI of this Sone
341          */
342         public FreenetURI getInsertUri() {
343                 return (insertUri != null) ? insertUri.setSuggestedEdition(latestEdition) : null;
344         }
345
346         /**
347          * Sets the insert URI of this Sone.
348          *
349          * @param insertUri
350          *            The insert URI of this Sone
351          * @return This Sone (for method chaining)
352          */
353         public Sone setInsertUri(FreenetURI insertUri) {
354                 if (this.insertUri == null) {
355                         this.insertUri = insertUri.setKeyType("USK").setDocName("Sone").setMetaString(new String[0]);
356                         return this;
357                 }
358                 if (!this.insertUri.equalsKeypair(insertUri)) {
359                         logger.log(Level.WARNING, String.format("Request URI %s tried to overwrite %s!", insertUri, this.insertUri));
360                         return this;
361                 }
362                 return this;
363         }
364
365         /**
366          * Returns the latest edition of this Sone.
367          *
368          * @return The latest edition of this Sone
369          */
370         public long getLatestEdition() {
371                 return latestEdition;
372         }
373
374         /**
375          * Sets the latest edition of this Sone. If the given latest edition is not
376          * greater than the current latest edition, the latest edition of this Sone
377          * is not changed.
378          *
379          * @param latestEdition
380          *            The latest edition of this Sone
381          */
382         public void setLatestEdition(long latestEdition) {
383                 if (!(latestEdition > this.latestEdition)) {
384                         logger.log(Level.FINE, String.format("New latest edition %d is not greater than current latest edition %d!", latestEdition, this.latestEdition));
385                         return;
386                 }
387                 this.latestEdition = latestEdition;
388         }
389
390         /**
391          * Return the time of the last inserted update of this Sone.
392          *
393          * @return The time of the update (in milliseconds since Jan 1, 1970 UTC)
394          */
395         public long getTime() {
396                 return time;
397         }
398
399         /**
400          * Sets the time of the last inserted update of this Sone.
401          *
402          * @param time
403          *            The time of the update (in milliseconds since Jan 1, 1970 UTC)
404          * @return This Sone (for method chaining)
405          */
406         public Sone setTime(long time) {
407                 this.time = time;
408                 return this;
409         }
410
411         /**
412          * Returns the status of this Sone.
413          *
414          * @return The status of this Sone
415          */
416         public SoneStatus getStatus() {
417                 return status;
418         }
419
420         /**
421          * Sets the new status of this Sone.
422          *
423          * @param status
424          *            The new status of this Sone
425          * @return This Sone
426          * @throws IllegalArgumentException
427          *             if {@code status} is {@code null}
428          */
429         public Sone setStatus(SoneStatus status) {
430                 this.status = checkNotNull(status, "status must not be null");
431                 return this;
432         }
433
434         /**
435          * Returns a copy of the profile. If you want to update values in the
436          * profile of this Sone, update the values in the returned {@link Profile}
437          * and use {@link #setProfile(Profile)} to change the profile in this Sone.
438          *
439          * @return A copy of the profile
440          */
441         public Profile getProfile() {
442                 return new Profile(profile);
443         }
444
445         /**
446          * Sets the profile of this Sone. A copy of the given profile is stored so
447          * that subsequent modifications of the given profile are not reflected in
448          * this Sone!
449          *
450          * @param profile
451          *            The profile to set
452          */
453         public void setProfile(Profile profile) {
454                 this.profile = new Profile(profile);
455         }
456
457         /**
458          * Returns the client used by this Sone.
459          *
460          * @return The client used by this Sone, or {@code null}
461          */
462         public Client getClient() {
463                 return client;
464         }
465
466         /**
467          * Sets the client used by this Sone.
468          *
469          * @param client
470          *            The client used by this Sone, or {@code null}
471          * @return This Sone (for method chaining)
472          */
473         public Sone setClient(Client client) {
474                 this.client = client;
475                 return this;
476         }
477
478         /**
479          * Returns whether this Sone is known.
480          *
481          * @return {@code true} if this Sone is known, {@code false} otherwise
482          */
483         public boolean isKnown() {
484                 return known;
485         }
486
487         /**
488          * Sets whether this Sone is known.
489          *
490          * @param known
491          *            {@code true} if this Sone is known, {@code false} otherwise
492          * @return This Sone
493          */
494         public Sone setKnown(boolean known) {
495                 this.known = known;
496                 return this;
497         }
498
499         /**
500          * Returns all friend Sones of this Sone.
501          *
502          * @return The friend Sones of this Sone
503          */
504         public List<String> getFriends() {
505                 List<String> friends = new ArrayList<String>(friendSones);
506                 return friends;
507         }
508
509         /**
510          * Returns whether this Sone has the given Sone as a friend Sone.
511          *
512          * @param friendSoneId
513          *            The ID of the Sone to check for
514          * @return {@code true} if this Sone has the given Sone as a friend,
515          *         {@code false} otherwise
516          */
517         public boolean hasFriend(String friendSoneId) {
518                 return friendSones.contains(friendSoneId);
519         }
520
521         /**
522          * Adds the given Sone as a friend Sone.
523          *
524          * @param friendSone
525          *            The friend Sone to add
526          * @return This Sone (for method chaining)
527          */
528         public Sone addFriend(String friendSone) {
529                 if (!friendSone.equals(id)) {
530                         friendSones.add(friendSone);
531                 }
532                 return this;
533         }
534
535         /**
536          * Removes the given Sone as a friend Sone.
537          *
538          * @param friendSoneId
539          *            The ID of the friend Sone to remove
540          * @return This Sone (for method chaining)
541          */
542         public Sone removeFriend(String friendSoneId) {
543                 friendSones.remove(friendSoneId);
544                 return this;
545         }
546
547         /**
548          * Returns the list of posts of this Sone, sorted by time, newest first.
549          *
550          * @return All posts of this Sone
551          */
552         public List<Post> getPosts() {
553                 List<Post> sortedPosts;
554                 synchronized (this) {
555                         sortedPosts = new ArrayList<Post>(posts);
556                 }
557                 Collections.sort(sortedPosts, Post.TIME_COMPARATOR);
558                 return sortedPosts;
559         }
560
561         /**
562          * Sets all posts of this Sone at once.
563          *
564          * @param posts
565          *            The new (and only) posts of this Sone
566          * @return This Sone (for method chaining)
567          */
568         public Sone setPosts(Collection<Post> posts) {
569                 synchronized (this) {
570                         this.posts.clear();
571                         this.posts.addAll(posts);
572                 }
573                 return this;
574         }
575
576         /**
577          * Adds the given post to this Sone. The post will not be added if its
578          * {@link Post#getSone() Sone} is not this Sone.
579          *
580          * @param post
581          *            The post to add
582          */
583         public void addPost(Post post) {
584                 if (post.getSone().equals(this) && posts.add(post)) {
585                         logger.log(Level.FINEST, String.format("Adding %s to “%s”.", post, getName()));
586                 }
587         }
588
589         /**
590          * Removes the given post from this Sone.
591          *
592          * @param post
593          *            The post to remove
594          */
595         public void removePost(Post post) {
596                 if (post.getSone().equals(this)) {
597                         posts.remove(post);
598                 }
599         }
600
601         /**
602          * Returns all replies this Sone made.
603          *
604          * @return All replies this Sone made
605          */
606         public Set<PostReply> getReplies() {
607                 return Collections.unmodifiableSet(replies);
608         }
609
610         /**
611          * Sets all replies of this Sone at once.
612          *
613          * @param replies
614          *            The new (and only) replies of this Sone
615          * @return This Sone (for method chaining)
616          */
617         public Sone setReplies(Collection<PostReply> replies) {
618                 this.replies.clear();
619                 this.replies.addAll(replies);
620                 return this;
621         }
622
623         /**
624          * Adds a reply to this Sone. If the given reply was not made by this Sone,
625          * nothing is added to this Sone.
626          *
627          * @param reply
628          *            The reply to add
629          */
630         public void addReply(PostReply reply) {
631                 if (reply.getSone().equals(this)) {
632                         replies.add(reply);
633                 }
634         }
635
636         /**
637          * Removes a reply from this Sone.
638          *
639          * @param reply
640          *            The reply to remove
641          */
642         public void removeReply(PostReply reply) {
643                 if (reply.getSone().equals(this)) {
644                         replies.remove(reply);
645                 }
646         }
647
648         /**
649          * Returns the IDs of all liked posts.
650          *
651          * @return All liked posts’ IDs
652          */
653         public Set<String> getLikedPostIds() {
654                 return Collections.unmodifiableSet(likedPostIds);
655         }
656
657         /**
658          * Sets the IDs of all liked posts.
659          *
660          * @param likedPostIds
661          *            All liked posts’ IDs
662          * @return This Sone (for method chaining)
663          */
664         public Sone setLikePostIds(Set<String> likedPostIds) {
665                 this.likedPostIds.clear();
666                 this.likedPostIds.addAll(likedPostIds);
667                 return this;
668         }
669
670         /**
671          * Checks whether the given post ID is liked by this Sone.
672          *
673          * @param postId
674          *            The ID of the post
675          * @return {@code true} if this Sone likes the given post, {@code false}
676          *         otherwise
677          */
678         public boolean isLikedPostId(String postId) {
679                 return likedPostIds.contains(postId);
680         }
681
682         /**
683          * Adds the given post ID to the list of posts this Sone likes.
684          *
685          * @param postId
686          *            The ID of the post
687          * @return This Sone (for method chaining)
688          */
689         public Sone addLikedPostId(String postId) {
690                 likedPostIds.add(postId);
691                 return this;
692         }
693
694         /**
695          * Removes the given post ID from the list of posts this Sone likes.
696          *
697          * @param postId
698          *            The ID of the post
699          * @return This Sone (for method chaining)
700          */
701         public Sone removeLikedPostId(String postId) {
702                 likedPostIds.remove(postId);
703                 return this;
704         }
705
706         /**
707          * Returns the IDs of all liked replies.
708          *
709          * @return All liked replies’ IDs
710          */
711         public Set<String> getLikedReplyIds() {
712                 return Collections.unmodifiableSet(likedReplyIds);
713         }
714
715         /**
716          * Sets the IDs of all liked replies.
717          *
718          * @param likedReplyIds
719          *            All liked replies’ IDs
720          * @return This Sone (for method chaining)
721          */
722         public Sone setLikeReplyIds(Set<String> likedReplyIds) {
723                 this.likedReplyIds.clear();
724                 this.likedReplyIds.addAll(likedReplyIds);
725                 return this;
726         }
727
728         /**
729          * Checks whether the given reply ID is liked by this Sone.
730          *
731          * @param replyId
732          *            The ID of the reply
733          * @return {@code true} if this Sone likes the given reply, {@code false}
734          *         otherwise
735          */
736         public boolean isLikedReplyId(String replyId) {
737                 return likedReplyIds.contains(replyId);
738         }
739
740         /**
741          * Adds the given reply ID to the list of replies this Sone likes.
742          *
743          * @param replyId
744          *            The ID of the reply
745          * @return This Sone (for method chaining)
746          */
747         public Sone addLikedReplyId(String replyId) {
748                 likedReplyIds.add(replyId);
749                 return this;
750         }
751
752         /**
753          * Removes the given post ID from the list of replies this Sone likes.
754          *
755          * @param replyId
756          *            The ID of the reply
757          * @return This Sone (for method chaining)
758          */
759         public Sone removeLikedReplyId(String replyId) {
760                 likedReplyIds.remove(replyId);
761                 return this;
762         }
763
764         /**
765          * Returns the albums of this Sone.
766          *
767          * @return The albums of this Sone
768          */
769         public List<Album> getAlbums() {
770                 return Collections.unmodifiableList(albums);
771         }
772
773         /**
774          * Returns all images of a Sone. Images of a album are inserted into this
775          * list before images of all child albums.
776          *
777          * @return The list of all images
778          */
779         public List<Image> getAllImages() {
780                 List<Image> allImages = new ArrayList<Image>();
781                 for (Album album : FluentIterable.from(getAlbums()).transformAndConcat(Album.FLATTENER).toList()) {
782                         allImages.addAll(album.getImages());
783                 }
784                 return allImages;
785         }
786
787         /**
788          * Adds an album to this Sone.
789          *
790          * @param album
791          *            The album to add
792          */
793         public void addAlbum(Album album) {
794                 checkNotNull(album, "album must not be null");
795                 checkArgument(album.getSone().equals(this), "album must belong to this Sone");
796                 if (!albums.contains(album)) {
797                         albums.add(album);
798                 }
799         }
800
801         /**
802          * Sets the albums of this Sone.
803          *
804          * @param albums
805          *            The albums of this Sone
806          */
807         public void setAlbums(Collection<? extends Album> albums) {
808                 checkNotNull(albums, "albums must not be null");
809                 this.albums.clear();
810                 for (Album album : albums) {
811                         addAlbum(album);
812                 }
813         }
814
815         /**
816          * Removes an album from this Sone.
817          *
818          * @param album
819          *            The album to remove
820          */
821         public void removeAlbum(Album album) {
822                 checkNotNull(album, "album must not be null");
823                 checkArgument(album.getSone().equals(this), "album must belong to this Sone");
824                 albums.remove(album);
825         }
826
827         /**
828          * Moves the given album up in this album’s albums. If the album is already
829          * the first album, nothing happens.
830          *
831          * @param album
832          *            The album to move up
833          * @return The album that the given album swapped the place with, or
834          *         <code>null</code> if the album did not change its place
835          */
836         public Album moveAlbumUp(Album album) {
837                 checkNotNull(album, "album must not be null");
838                 checkArgument(album.getSone().equals(this), "album must belong to this Sone");
839                 checkArgument(album.getParent() == null, "album must not have a parent");
840                 int oldIndex = albums.indexOf(album);
841                 if (oldIndex <= 0) {
842                         return null;
843                 }
844                 albums.remove(oldIndex);
845                 albums.add(oldIndex - 1, album);
846                 return albums.get(oldIndex);
847         }
848
849         /**
850          * Moves the given album down in this album’s albums. If the album is
851          * already the last album, nothing happens.
852          *
853          * @param album
854          *            The album to move down
855          * @return The album that the given album swapped the place with, or
856          *         <code>null</code> if the album did not change its place
857          */
858         public Album moveAlbumDown(Album album) {
859                 checkNotNull(album, "album must not be null");
860                 checkArgument(album.getSone().equals(this), "album must belong to this Sone");
861                 checkArgument(album.getParent() == null, "album must not have a parent");
862                 int oldIndex = albums.indexOf(album);
863                 if ((oldIndex < 0) || (oldIndex >= (albums.size() - 1))) {
864                         return null;
865                 }
866                 albums.remove(oldIndex);
867                 albums.add(oldIndex + 1, album);
868                 return albums.get(oldIndex);
869         }
870
871         /**
872          * Returns Sone-specific options.
873          *
874          * @return The options of this Sone
875          */
876         public Options getOptions() {
877                 return options;
878         }
879
880         /**
881          * Sets the options of this Sone.
882          *
883          * @param options
884          *            The options of this Sone
885          */
886         /* TODO - remove this method again, maybe add an option provider */
887         public void setOptions(Options options) {
888                 this.options = options;
889         }
890
891         //
892         // FINGERPRINTABLE METHODS
893         //
894
895         /**
896          * {@inheritDoc}
897          */
898         @Override
899         public synchronized String getFingerprint() {
900                 Hasher hash = Hashing.sha256().newHasher();
901                 hash.putString(profile.getFingerprint());
902
903                 hash.putString("Posts(");
904                 for (Post post : getPosts()) {
905                         hash.putString("Post(").putString(post.getId()).putString(")");
906                 }
907                 hash.putString(")");
908
909                 List<PostReply> replies = new ArrayList<PostReply>(getReplies());
910                 Collections.sort(replies, Reply.TIME_COMPARATOR);
911                 hash.putString("Replies(");
912                 for (PostReply reply : replies) {
913                         hash.putString("Reply(").putString(reply.getId()).putString(")");
914                 }
915                 hash.putString(")");
916
917                 List<String> likedPostIds = new ArrayList<String>(getLikedPostIds());
918                 Collections.sort(likedPostIds);
919                 hash.putString("LikedPosts(");
920                 for (String likedPostId : likedPostIds) {
921                         hash.putString("Post(").putString(likedPostId).putString(")");
922                 }
923                 hash.putString(")");
924
925                 List<String> likedReplyIds = new ArrayList<String>(getLikedReplyIds());
926                 Collections.sort(likedReplyIds);
927                 hash.putString("LikedReplies(");
928                 for (String likedReplyId : likedReplyIds) {
929                         hash.putString("Reply(").putString(likedReplyId).putString(")");
930                 }
931                 hash.putString(")");
932
933                 hash.putString("Albums(");
934                 for (Album album : albums) {
935                         hash.putString(album.getFingerprint());
936                 }
937                 hash.putString(")");
938
939                 return hash.hash().toString();
940         }
941
942         //
943         // INTERFACE Comparable<Sone>
944         //
945
946         /**
947          * {@inheritDoc}
948          */
949         @Override
950         public int compareTo(Sone sone) {
951                 return NICE_NAME_COMPARATOR.compare(this, sone);
952         }
953
954         //
955         // OBJECT METHODS
956         //
957
958         /**
959          * {@inheritDoc}
960          */
961         @Override
962         public int hashCode() {
963                 return id.hashCode();
964         }
965
966         /**
967          * {@inheritDoc}
968          */
969         @Override
970         public boolean equals(Object object) {
971                 if (!(object instanceof Sone)) {
972                         return false;
973                 }
974                 return ((Sone) object).id.equals(id);
975         }
976
977         /**
978          * {@inheritDoc}
979          */
980         @Override
981         public String toString() {
982                 return getClass().getName() + "[identity=" + identity + ",requestUri=" + requestUri + ",insertUri(" + String.valueOf(insertUri).length() + "),friends(" + friendSones.size() + "),posts(" + posts.size() + "),replies(" + replies.size() + ")]";
983         }
984
985 }