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