Fix ALL the logging!
[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         /** Whether this Sone is known. */
204         private volatile boolean known;
205
206         /** All friend Sones. */
207         private final Set<String> friendSones = new CopyOnWriteArraySet<String>();
208
209         /** All posts. */
210         private final Set<Post> posts = new CopyOnWriteArraySet<Post>();
211
212         /** All replies. */
213         private final Set<PostReply> replies = new CopyOnWriteArraySet<PostReply>();
214
215         /** The IDs of all liked posts. */
216         private final Set<String> likedPostIds = new CopyOnWriteArraySet<String>();
217
218         /** The IDs of all liked replies. */
219         private final Set<String> likedReplyIds = new CopyOnWriteArraySet<String>();
220
221         /** The albums of this Sone. */
222         private final List<Album> albums = new CopyOnWriteArrayList<Album>();
223
224         /** Sone-specific options. */
225         private final Options options = new Options();
226
227         /**
228          * Creates a new Sone.
229          *
230          * @param id
231          *            The ID of the Sone
232          */
233         public Sone(String id) {
234                 this.id = id;
235         }
236
237         //
238         // ACCESSORS
239         //
240
241         /**
242          * Returns the identity of this Sone.
243          *
244          * @return The identity of this Sone
245          */
246         public String getId() {
247                 return id;
248         }
249
250         /**
251          * Returns the identity of this Sone.
252          *
253          * @return The identity of this Sone
254          */
255         public Identity getIdentity() {
256                 return identity;
257         }
258
259         /**
260          * Sets the identity of this Sone. The {@link Identity#getId() ID} of the
261          * identity has to match this Sone’s {@link #getId()}.
262          *
263          * @param identity
264          *            The identity of this Sone
265          * @return This Sone (for method chaining)
266          * @throws IllegalArgumentException
267          *             if the ID of the identity does not match this Sone’s ID
268          */
269         public Sone setIdentity(Identity identity) throws IllegalArgumentException {
270                 if (!identity.getId().equals(id)) {
271                         throw new IllegalArgumentException("Identity’s ID does not match Sone’s ID!");
272                 }
273                 this.identity = identity;
274                 return this;
275         }
276
277         /**
278          * Returns the name of this Sone.
279          *
280          * @return The name of this Sone
281          */
282         public String getName() {
283                 return (identity != null) ? identity.getNickname() : null;
284         }
285
286         /**
287          * Returns the request URI of this Sone.
288          *
289          * @return The request URI of this Sone
290          */
291         public FreenetURI getRequestUri() {
292                 return (requestUri != null) ? requestUri.setSuggestedEdition(latestEdition) : null;
293         }
294
295         /**
296          * Sets the request URI of this Sone.
297          *
298          * @param requestUri
299          *            The request URI of this Sone
300          * @return This Sone (for method chaining)
301          */
302         public Sone setRequestUri(FreenetURI requestUri) {
303                 if (this.requestUri == null) {
304                         this.requestUri = requestUri.setKeyType("USK").setDocName("Sone").setMetaString(new String[0]);
305                         return this;
306                 }
307                 if (!this.requestUri.equalsKeypair(requestUri)) {
308                         logger.log(Level.WARNING, String.format("Request URI %s tried to overwrite %s!", requestUri, this.requestUri));
309                         return this;
310                 }
311                 return this;
312         }
313
314         /**
315          * Returns the insert URI of this Sone.
316          *
317          * @return The insert URI of this Sone
318          */
319         public FreenetURI getInsertUri() {
320                 return (insertUri != null) ? insertUri.setSuggestedEdition(latestEdition) : null;
321         }
322
323         /**
324          * Sets the insert URI of this Sone.
325          *
326          * @param insertUri
327          *            The insert URI of this Sone
328          * @return This Sone (for method chaining)
329          */
330         public Sone setInsertUri(FreenetURI insertUri) {
331                 if (this.insertUri == null) {
332                         this.insertUri = insertUri.setKeyType("USK").setDocName("Sone").setMetaString(new String[0]);
333                         return this;
334                 }
335                 if (!this.insertUri.equalsKeypair(insertUri)) {
336                         logger.log(Level.WARNING, String.format("Request URI %s tried to overwrite %s!", insertUri, this.insertUri));
337                         return this;
338                 }
339                 return this;
340         }
341
342         /**
343          * Returns the latest edition of this Sone.
344          *
345          * @return The latest edition of this Sone
346          */
347         public long getLatestEdition() {
348                 return latestEdition;
349         }
350
351         /**
352          * Sets the latest edition of this Sone. If the given latest edition is not
353          * greater than the current latest edition, the latest edition of this Sone
354          * is not changed.
355          *
356          * @param latestEdition
357          *            The latest edition of this Sone
358          */
359         public void setLatestEdition(long latestEdition) {
360                 if (!(latestEdition > this.latestEdition)) {
361                         logger.log(Level.FINE, String.format("New latest edition %d is not greater than current latest edition %d!", latestEdition, this.latestEdition));
362                         return;
363                 }
364                 this.latestEdition = latestEdition;
365         }
366
367         /**
368          * Return the time of the last inserted update of this Sone.
369          *
370          * @return The time of the update (in milliseconds since Jan 1, 1970 UTC)
371          */
372         public long getTime() {
373                 return time;
374         }
375
376         /**
377          * Sets the time of the last inserted update of this Sone.
378          *
379          * @param time
380          *            The time of the update (in milliseconds since Jan 1, 1970 UTC)
381          * @return This Sone (for method chaining)
382          */
383         public Sone setTime(long time) {
384                 this.time = time;
385                 return this;
386         }
387
388         /**
389          * Returns the status of this Sone.
390          *
391          * @return The status of this Sone
392          */
393         public SoneStatus getStatus() {
394                 return status;
395         }
396
397         /**
398          * Sets the new status of this Sone.
399          *
400          * @param status
401          *            The new status of this Sone
402          * @return This Sone
403          * @throws IllegalArgumentException
404          *             if {@code status} is {@code null}
405          */
406         public Sone setStatus(SoneStatus status) {
407                 Validation.begin().isNotNull("Sone Status", status).check();
408                 this.status = status;
409                 return this;
410         }
411
412         /**
413          * Returns a copy of the profile. If you want to update values in the
414          * profile of this Sone, update the values in the returned {@link Profile}
415          * and use {@link #setProfile(Profile)} to change the profile in this Sone.
416          *
417          * @return A copy of the profile
418          */
419         public Profile getProfile() {
420                 return new Profile(profile);
421         }
422
423         /**
424          * Sets the profile of this Sone. A copy of the given profile is stored so
425          * that subsequent modifications of the given profile are not reflected in
426          * this Sone!
427          *
428          * @param profile
429          *            The profile to set
430          */
431         public void setProfile(Profile profile) {
432                 this.profile = new Profile(profile);
433         }
434
435         /**
436          * Returns the client used by this Sone.
437          *
438          * @return The client used by this Sone, or {@code null}
439          */
440         public Client getClient() {
441                 return client;
442         }
443
444         /**
445          * Sets the client used by this Sone.
446          *
447          * @param client
448          *            The client used by this Sone, or {@code null}
449          * @return This Sone (for method chaining)
450          */
451         public Sone setClient(Client client) {
452                 this.client = client;
453                 return this;
454         }
455
456         /**
457          * Returns whether this Sone is known.
458          *
459          * @return {@code true} if this Sone is known, {@code false} otherwise
460          */
461         public boolean isKnown() {
462                 return known;
463         }
464
465         /**
466          * Sets whether this Sone is known.
467          *
468          * @param known
469          *            {@code true} if this Sone is known, {@code false} otherwise
470          * @return This Sone
471          */
472         public Sone setKnown(boolean known) {
473                 this.known = known;
474                 return this;
475         }
476
477         /**
478          * Returns all friend Sones of this Sone.
479          *
480          * @return The friend Sones of this Sone
481          */
482         public List<String> getFriends() {
483                 List<String> friends = new ArrayList<String>(friendSones);
484                 return friends;
485         }
486
487         /**
488          * Returns whether this Sone has the given Sone as a friend Sone.
489          *
490          * @param friendSoneId
491          *            The ID of the Sone to check for
492          * @return {@code true} if this Sone has the given Sone as a friend,
493          *         {@code false} otherwise
494          */
495         public boolean hasFriend(String friendSoneId) {
496                 return friendSones.contains(friendSoneId);
497         }
498
499         /**
500          * Adds the given Sone as a friend Sone.
501          *
502          * @param friendSone
503          *            The friend Sone to add
504          * @return This Sone (for method chaining)
505          */
506         public Sone addFriend(String friendSone) {
507                 if (!friendSone.equals(id)) {
508                         friendSones.add(friendSone);
509                 }
510                 return this;
511         }
512
513         /**
514          * Removes the given Sone as a friend Sone.
515          *
516          * @param friendSoneId
517          *            The ID of the friend Sone to remove
518          * @return This Sone (for method chaining)
519          */
520         public Sone removeFriend(String friendSoneId) {
521                 friendSones.remove(friendSoneId);
522                 return this;
523         }
524
525         /**
526          * Returns the list of posts of this Sone, sorted by time, newest first.
527          *
528          * @return All posts of this Sone
529          */
530         public List<Post> getPosts() {
531                 List<Post> sortedPosts;
532                 synchronized (this) {
533                         sortedPosts = new ArrayList<Post>(posts);
534                 }
535                 Collections.sort(sortedPosts, Post.TIME_COMPARATOR);
536                 return sortedPosts;
537         }
538
539         /**
540          * Sets all posts of this Sone at once.
541          *
542          * @param posts
543          *            The new (and only) posts of this Sone
544          * @return This Sone (for method chaining)
545          */
546         public Sone setPosts(Collection<Post> posts) {
547                 synchronized (this) {
548                         this.posts.clear();
549                         this.posts.addAll(posts);
550                 }
551                 return this;
552         }
553
554         /**
555          * Adds the given post to this Sone. The post will not be added if its
556          * {@link Post#getSone() Sone} is not this Sone.
557          *
558          * @param post
559          *            The post to add
560          */
561         public void addPost(Post post) {
562                 if (post.getSone().equals(this) && posts.add(post)) {
563                         logger.log(Level.FINEST, String.format("Adding %s to “%s”.", post, getName()));
564                 }
565         }
566
567         /**
568          * Removes the given post from this Sone.
569          *
570          * @param post
571          *            The post to remove
572          */
573         public void removePost(Post post) {
574                 if (post.getSone().equals(this)) {
575                         posts.remove(post);
576                 }
577         }
578
579         /**
580          * Returns all replies this Sone made.
581          *
582          * @return All replies this Sone made
583          */
584         public Set<PostReply> getReplies() {
585                 return Collections.unmodifiableSet(replies);
586         }
587
588         /**
589          * Sets all replies of this Sone at once.
590          *
591          * @param replies
592          *            The new (and only) replies of this Sone
593          * @return This Sone (for method chaining)
594          */
595         public Sone setReplies(Collection<PostReply> replies) {
596                 this.replies.clear();
597                 this.replies.addAll(replies);
598                 return this;
599         }
600
601         /**
602          * Adds a reply to this Sone. If the given reply was not made by this Sone,
603          * nothing is added to this Sone.
604          *
605          * @param reply
606          *            The reply to add
607          */
608         public void addReply(PostReply reply) {
609                 if (reply.getSone().equals(this)) {
610                         replies.add(reply);
611                 }
612         }
613
614         /**
615          * Removes a reply from this Sone.
616          *
617          * @param reply
618          *            The reply to remove
619          */
620         public void removeReply(PostReply reply) {
621                 if (reply.getSone().equals(this)) {
622                         replies.remove(reply);
623                 }
624         }
625
626         /**
627          * Returns the IDs of all liked posts.
628          *
629          * @return All liked posts’ IDs
630          */
631         public Set<String> getLikedPostIds() {
632                 return Collections.unmodifiableSet(likedPostIds);
633         }
634
635         /**
636          * Sets the IDs of all liked posts.
637          *
638          * @param likedPostIds
639          *            All liked posts’ IDs
640          * @return This Sone (for method chaining)
641          */
642         public Sone setLikePostIds(Set<String> likedPostIds) {
643                 this.likedPostIds.clear();
644                 this.likedPostIds.addAll(likedPostIds);
645                 return this;
646         }
647
648         /**
649          * Checks whether the given post ID is liked by this Sone.
650          *
651          * @param postId
652          *            The ID of the post
653          * @return {@code true} if this Sone likes the given post, {@code false}
654          *         otherwise
655          */
656         public boolean isLikedPostId(String postId) {
657                 return likedPostIds.contains(postId);
658         }
659
660         /**
661          * Adds the given post ID to the list of posts this Sone likes.
662          *
663          * @param postId
664          *            The ID of the post
665          * @return This Sone (for method chaining)
666          */
667         public Sone addLikedPostId(String postId) {
668                 likedPostIds.add(postId);
669                 return this;
670         }
671
672         /**
673          * Removes the given post ID from the list of posts this Sone likes.
674          *
675          * @param postId
676          *            The ID of the post
677          * @return This Sone (for method chaining)
678          */
679         public Sone removeLikedPostId(String postId) {
680                 likedPostIds.remove(postId);
681                 return this;
682         }
683
684         /**
685          * Returns the IDs of all liked replies.
686          *
687          * @return All liked replies’ IDs
688          */
689         public Set<String> getLikedReplyIds() {
690                 return Collections.unmodifiableSet(likedReplyIds);
691         }
692
693         /**
694          * Sets the IDs of all liked replies.
695          *
696          * @param likedReplyIds
697          *            All liked replies’ IDs
698          * @return This Sone (for method chaining)
699          */
700         public Sone setLikeReplyIds(Set<String> likedReplyIds) {
701                 this.likedReplyIds.clear();
702                 this.likedReplyIds.addAll(likedReplyIds);
703                 return this;
704         }
705
706         /**
707          * Checks whether the given reply ID is liked by this Sone.
708          *
709          * @param replyId
710          *            The ID of the reply
711          * @return {@code true} if this Sone likes the given reply, {@code false}
712          *         otherwise
713          */
714         public boolean isLikedReplyId(String replyId) {
715                 return likedReplyIds.contains(replyId);
716         }
717
718         /**
719          * Adds the given reply ID to the list of replies this Sone likes.
720          *
721          * @param replyId
722          *            The ID of the reply
723          * @return This Sone (for method chaining)
724          */
725         public Sone addLikedReplyId(String replyId) {
726                 likedReplyIds.add(replyId);
727                 return this;
728         }
729
730         /**
731          * Removes the given post ID from the list of replies this Sone likes.
732          *
733          * @param replyId
734          *            The ID of the reply
735          * @return This Sone (for method chaining)
736          */
737         public Sone removeLikedReplyId(String replyId) {
738                 likedReplyIds.remove(replyId);
739                 return this;
740         }
741
742         /**
743          * Returns the albums of this Sone.
744          *
745          * @return The albums of this Sone
746          */
747         public List<Album> getAlbums() {
748                 return Collections.unmodifiableList(albums);
749         }
750
751         /**
752          * Returns a flattened list of all albums of this Sone. The resulting list
753          * contains parent albums before child albums so that the resulting list can
754          * be parsed in a single pass.
755          *
756          * @return The flattened albums
757          */
758         public List<Album> getAllAlbums() {
759                 List<Album> flatAlbums = new ArrayList<Album>();
760                 flatAlbums.addAll(albums);
761                 int lastAlbumIndex = 0;
762                 while (lastAlbumIndex < flatAlbums.size()) {
763                         int previousAlbumCount = flatAlbums.size();
764                         for (Album album : new ArrayList<Album>(flatAlbums.subList(lastAlbumIndex, flatAlbums.size()))) {
765                                 flatAlbums.addAll(album.getAlbums());
766                         }
767                         lastAlbumIndex = previousAlbumCount;
768                 }
769                 return flatAlbums;
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 : getAllAlbums()) {
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                 Validation.begin().isNotNull("Album", album).check().isEqual("Album Owner", album.getSone(), this).check();
794                 albums.add(album);
795         }
796
797         /**
798          * Sets the albums of this Sone.
799          *
800          * @param albums
801          *            The albums of this Sone
802          */
803         public void setAlbums(Collection<? extends Album> albums) {
804                 Validation.begin().isNotNull("Albums", albums).check();
805                 this.albums.clear();
806                 for (Album album : albums) {
807                         addAlbum(album);
808                 }
809         }
810
811         /**
812          * Removes an album from this Sone.
813          *
814          * @param album
815          *            The album to remove
816          */
817         public void removeAlbum(Album album) {
818                 Validation.begin().isNotNull("Album", album).check().isEqual("Album Owner", album.getSone(), this).check();
819                 albums.remove(album);
820         }
821
822         /**
823          * Moves the given album up in this album’s albums. If the album is already
824          * the first album, nothing happens.
825          *
826          * @param album
827          *            The album to move up
828          * @return The album that the given album swapped the place with, or
829          *         <code>null</code> if the album did not change its place
830          */
831         public Album moveAlbumUp(Album album) {
832                 Validation.begin().isNotNull("Album", album).check().isEqual("Album Owner", album.getSone(), this).isNull("Album Parent", album.getParent()).check();
833                 int oldIndex = albums.indexOf(album);
834                 if (oldIndex <= 0) {
835                         return null;
836                 }
837                 albums.remove(oldIndex);
838                 albums.add(oldIndex - 1, album);
839                 return albums.get(oldIndex);
840         }
841
842         /**
843          * Moves the given album down in this album’s albums. If the album is
844          * already the last album, nothing happens.
845          *
846          * @param album
847          *            The album to move down
848          * @return The album that the given album swapped the place with, or
849          *         <code>null</code> if the album did not change its place
850          */
851         public Album moveAlbumDown(Album album) {
852                 Validation.begin().isNotNull("Album", album).check().isEqual("Album Owner", album.getSone(), this).isNull("Album Parent", album.getParent()).check();
853                 int oldIndex = albums.indexOf(album);
854                 if ((oldIndex < 0) || (oldIndex >= (albums.size() - 1))) {
855                         return null;
856                 }
857                 albums.remove(oldIndex);
858                 albums.add(oldIndex + 1, album);
859                 return albums.get(oldIndex);
860         }
861
862         /**
863          * Returns Sone-specific options.
864          *
865          * @return The options of this Sone
866          */
867         public Options getOptions() {
868                 return options;
869         }
870
871         //
872         // FINGERPRINTABLE METHODS
873         //
874
875         /**
876          * {@inheritDoc}
877          */
878         @Override
879         public synchronized String getFingerprint() {
880                 StringBuilder fingerprint = new StringBuilder();
881                 fingerprint.append(profile.getFingerprint());
882
883                 fingerprint.append("Posts(");
884                 for (Post post : getPosts()) {
885                         fingerprint.append("Post(").append(post.getId()).append(')');
886                 }
887                 fingerprint.append(")");
888
889                 List<PostReply> replies = new ArrayList<PostReply>(getReplies());
890                 Collections.sort(replies, Reply.TIME_COMPARATOR);
891                 fingerprint.append("Replies(");
892                 for (PostReply reply : replies) {
893                         fingerprint.append("Reply(").append(reply.getId()).append(')');
894                 }
895                 fingerprint.append(')');
896
897                 List<String> likedPostIds = new ArrayList<String>(getLikedPostIds());
898                 Collections.sort(likedPostIds);
899                 fingerprint.append("LikedPosts(");
900                 for (String likedPostId : likedPostIds) {
901                         fingerprint.append("Post(").append(likedPostId).append(')');
902                 }
903                 fingerprint.append(')');
904
905                 List<String> likedReplyIds = new ArrayList<String>(getLikedReplyIds());
906                 Collections.sort(likedReplyIds);
907                 fingerprint.append("LikedReplies(");
908                 for (String likedReplyId : likedReplyIds) {
909                         fingerprint.append("Reply(").append(likedReplyId).append(')');
910                 }
911                 fingerprint.append(')');
912
913                 fingerprint.append("Albums(");
914                 for (Album album : albums) {
915                         fingerprint.append(album.getFingerprint());
916                 }
917                 fingerprint.append(')');
918
919                 return fingerprint.toString();
920         }
921
922         //
923         // INTERFACE Comparable<Sone>
924         //
925
926         /**
927          * {@inheritDoc}
928          */
929         @Override
930         public int compareTo(Sone sone) {
931                 return NICE_NAME_COMPARATOR.compare(this, sone);
932         }
933
934         //
935         // OBJECT METHODS
936         //
937
938         /**
939          * {@inheritDoc}
940          */
941         @Override
942         public int hashCode() {
943                 return id.hashCode();
944         }
945
946         /**
947          * {@inheritDoc}
948          */
949         @Override
950         public boolean equals(Object object) {
951                 if (!(object instanceof Sone)) {
952                         return false;
953                 }
954                 return ((Sone) object).id.equals(id);
955         }
956
957         /**
958          * {@inheritDoc}
959          */
960         @Override
961         public String toString() {
962                 return getClass().getName() + "[identity=" + identity + ",requestUri=" + requestUri + ",insertUri(" + String.valueOf(insertUri).length() + "),friends(" + friendSones.size() + "),posts(" + posts.size() + "),replies(" + replies.size() + ")]";
963         }
964
965 }