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