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