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