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