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