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