2 * Sone - Sone.java - Copyright © 2010–2012 David Roden
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.
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.
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/>.
18 package net.pterodactylus.sone.data;
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;
26 import java.util.concurrent.CopyOnWriteArrayList;
27 import java.util.concurrent.CopyOnWriteArraySet;
28 import java.util.logging.Level;
29 import java.util.logging.Logger;
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.logging.Logging;
36 import net.pterodactylus.util.validation.Validation;
38 import com.google.common.base.Predicate;
40 import freenet.keys.FreenetURI;
43 * A Sone defines everything about a user: her profile, her status updates, her
44 * replies, her likes and dislikes, etc.
46 * Operations that modify the Sone need to synchronize on the Sone in question.
48 * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
50 public class Sone implements Fingerprintable, Comparable<Sone> {
53 * Enumeration for the possible states of a {@link Sone}.
55 * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
57 public enum SoneStatus {
59 /** The Sone is unknown, i.e. not yet downloaded. */
62 /** The Sone is idle, i.e. not being downloaded or inserted. */
65 /** The Sone is currently being inserted. */
68 /** The Sone is currently being downloaded. */
73 * The possible values for the “show custom avatars” option.
75 * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
77 public static enum ShowCustomAvatars {
79 /** Never show custom avatars. */
82 /** Only show custom avatars of followed Sones. */
85 /** Only show custom avatars of Sones you manually trust. */
88 /** Only show custom avatars of automatically trusted Sones. */
91 /** Always show custom avatars. */
96 /** comparator that sorts Sones by their nice name. */
97 public static final Comparator<Sone> NICE_NAME_COMPARATOR = new Comparator<Sone>() {
100 public int compare(Sone leftSone, Sone rightSone) {
101 int diff = SoneAccessor.getNiceName(leftSone).compareToIgnoreCase(SoneAccessor.getNiceName(rightSone));
105 return leftSone.getId().compareToIgnoreCase(rightSone.getId());
111 * Comparator that sorts Sones by last activity (least recent active first).
113 public static final Comparator<Sone> LAST_ACTIVITY_COMPARATOR = new Comparator<Sone>() {
116 public int compare(Sone firstSone, Sone secondSone) {
117 return (int) Math.min(Integer.MAX_VALUE, Math.max(Integer.MIN_VALUE, secondSone.getTime() - firstSone.getTime()));
121 /** Comparator that sorts Sones by numbers of posts (descending). */
122 public static final Comparator<Sone> POST_COUNT_COMPARATOR = new Comparator<Sone>() {
128 public int compare(Sone leftSone, Sone rightSone) {
129 return (leftSone.getPosts().size() != rightSone.getPosts().size()) ? (rightSone.getPosts().size() - leftSone.getPosts().size()) : (rightSone.getReplies().size() - leftSone.getReplies().size());
133 /** Comparator that sorts Sones by number of images (descending). */
134 public static final Comparator<Sone> IMAGE_COUNT_COMPARATOR = new Comparator<Sone>() {
140 public int compare(Sone leftSone, Sone rightSone) {
141 return rightSone.getAllImages().size() - leftSone.getAllImages().size();
145 /** Filter to remove Sones that have not been downloaded. */
146 public static final Predicate<Sone> EMPTY_SONE_FILTER = new Predicate<Sone>() {
149 public boolean apply(Sone sone) {
150 return sone.getTime() != 0;
154 /** Filter that matches all {@link Sone#isLocal() local Sones}. */
155 public static final Predicate<Sone> LOCAL_SONE_FILTER = new Predicate<Sone>() {
158 public boolean apply(Sone sone) {
159 return sone.getIdentity() instanceof OwnIdentity;
164 /** Filter that matches Sones that have at least one album. */
165 public static final Predicate<Sone> HAS_ALBUM_FILTER = new Predicate<Sone>() {
168 public boolean apply(Sone sone) {
169 return !sone.getAlbums().isEmpty();
174 private static final Logger logger = Logging.getLogger(Sone.class);
176 /** The ID of this Sone. */
177 private final String id;
179 /** Whether the Sone is local. */
180 private final boolean local;
182 /** The identity of this Sone. */
183 private Identity identity;
185 /** The URI under which the Sone is stored in Freenet. */
186 private volatile FreenetURI requestUri;
188 /** The URI used to insert a new version of this Sone. */
189 /* This will be null for remote Sones! */
190 private volatile FreenetURI insertUri;
192 /** The latest edition of the Sone. */
193 private volatile long latestEdition;
195 /** The time of the last inserted update. */
196 private volatile long time;
198 /** The status of this Sone. */
199 private volatile SoneStatus status = SoneStatus.unknown;
201 /** The profile of this Sone. */
202 private volatile Profile profile = new Profile(this);
204 /** The client used by the Sone. */
205 private volatile Client client;
207 /** Whether this Sone is known. */
208 private volatile boolean known;
210 /** All friend Sones. */
211 private final Set<String> friendSones = new CopyOnWriteArraySet<String>();
214 private final Set<Post> posts = new CopyOnWriteArraySet<Post>();
217 private final Set<PostReply> replies = new CopyOnWriteArraySet<PostReply>();
219 /** The IDs of all liked posts. */
220 private final Set<String> likedPostIds = new CopyOnWriteArraySet<String>();
222 /** The IDs of all liked replies. */
223 private final Set<String> likedReplyIds = new CopyOnWriteArraySet<String>();
225 /** The albums of this Sone. */
226 private final List<Album> albums = new CopyOnWriteArrayList<Album>();
228 /** Sone-specific options. */
229 private final Options options = new Options();
232 * Creates a new Sone.
237 * {@code true} if the Sone is a local Sone, {@code false}
240 public Sone(String id, boolean local) {
250 * Returns the identity of this Sone.
252 * @return The identity of this Sone
254 public String getId() {
259 * Returns the identity of this Sone.
261 * @return The identity of this Sone
263 public Identity getIdentity() {
268 * Sets the identity of this Sone. The {@link Identity#getId() ID} of the
269 * identity has to match this Sone’s {@link #getId()}.
272 * The identity of this Sone
273 * @return This Sone (for method chaining)
274 * @throws IllegalArgumentException
275 * if the ID of the identity does not match this Sone’s ID
277 public Sone setIdentity(Identity identity) throws IllegalArgumentException {
278 if (!identity.getId().equals(id)) {
279 throw new IllegalArgumentException("Identity’s ID does not match Sone’s ID!");
281 this.identity = identity;
286 * Returns the name of this Sone.
288 * @return The name of this Sone
290 public String getName() {
291 return (identity != null) ? identity.getNickname() : null;
295 * Returns whether this Sone is a local Sone.
297 * @return {@code true} if this Sone is a local Sone, {@code false}
300 public boolean isLocal() {
305 * Returns the request URI of this Sone.
307 * @return The request URI of this Sone
309 public FreenetURI getRequestUri() {
310 return (requestUri != null) ? requestUri.setSuggestedEdition(latestEdition) : null;
314 * Sets the request URI of this Sone.
317 * The request URI of this Sone
318 * @return This Sone (for method chaining)
320 public Sone setRequestUri(FreenetURI requestUri) {
321 if (this.requestUri == null) {
322 this.requestUri = requestUri.setKeyType("USK").setDocName("Sone").setMetaString(new String[0]);
325 if (!this.requestUri.equalsKeypair(requestUri)) {
326 logger.log(Level.WARNING, String.format("Request URI %s tried to overwrite %s!", requestUri, this.requestUri));
333 * Returns the insert URI of this Sone.
335 * @return The insert URI of this Sone
337 public FreenetURI getInsertUri() {
338 return (insertUri != null) ? insertUri.setSuggestedEdition(latestEdition) : null;
342 * Sets the insert URI of this Sone.
345 * The insert URI of this Sone
346 * @return This Sone (for method chaining)
348 public Sone setInsertUri(FreenetURI insertUri) {
349 if (this.insertUri == null) {
350 this.insertUri = insertUri.setKeyType("USK").setDocName("Sone").setMetaString(new String[0]);
353 if (!this.insertUri.equalsKeypair(insertUri)) {
354 logger.log(Level.WARNING, String.format("Request URI %s tried to overwrite %s!", insertUri, this.insertUri));
361 * Returns the latest edition of this Sone.
363 * @return The latest edition of this Sone
365 public long getLatestEdition() {
366 return latestEdition;
370 * Sets the latest edition of this Sone. If the given latest edition is not
371 * greater than the current latest edition, the latest edition of this Sone
374 * @param latestEdition
375 * The latest edition of this Sone
377 public void setLatestEdition(long latestEdition) {
378 if (!(latestEdition > this.latestEdition)) {
379 logger.log(Level.FINE, String.format("New latest edition %d is not greater than current latest edition %d!", latestEdition, this.latestEdition));
382 this.latestEdition = latestEdition;
386 * Return the time of the last inserted update of this Sone.
388 * @return The time of the update (in milliseconds since Jan 1, 1970 UTC)
390 public long getTime() {
395 * Sets the time of the last inserted update of this Sone.
398 * The time of the update (in milliseconds since Jan 1, 1970 UTC)
399 * @return This Sone (for method chaining)
401 public Sone setTime(long time) {
407 * Returns the status of this Sone.
409 * @return The status of this Sone
411 public SoneStatus getStatus() {
416 * Sets the new status of this Sone.
419 * The new status of this Sone
421 * @throws IllegalArgumentException
422 * if {@code status} is {@code null}
424 public Sone setStatus(SoneStatus status) {
425 Validation.begin().isNotNull("Sone Status", status).check();
426 this.status = status;
431 * Returns a copy of the profile. If you want to update values in the
432 * profile of this Sone, update the values in the returned {@link Profile}
433 * and use {@link #setProfile(Profile)} to change the profile in this Sone.
435 * @return A copy of the profile
437 public Profile getProfile() {
438 return new Profile(profile);
442 * Sets the profile of this Sone. A copy of the given profile is stored so
443 * that subsequent modifications of the given profile are not reflected in
449 public void setProfile(Profile profile) {
450 this.profile = new Profile(profile);
454 * Returns the client used by this Sone.
456 * @return The client used by this Sone, or {@code null}
458 public Client getClient() {
463 * Sets the client used by this Sone.
466 * The client used by this Sone, or {@code null}
467 * @return This Sone (for method chaining)
469 public Sone setClient(Client client) {
470 this.client = client;
475 * Returns whether this Sone is known.
477 * @return {@code true} if this Sone is known, {@code false} otherwise
479 public boolean isKnown() {
484 * Sets whether this Sone is known.
487 * {@code true} if this Sone is known, {@code false} otherwise
490 public Sone setKnown(boolean known) {
496 * Returns all friend Sones of this Sone.
498 * @return The friend Sones of this Sone
500 public List<String> getFriends() {
501 List<String> friends = new ArrayList<String>(friendSones);
506 * Returns whether this Sone has the given Sone as a friend Sone.
508 * @param friendSoneId
509 * The ID of the Sone to check for
510 * @return {@code true} if this Sone has the given Sone as a friend,
511 * {@code false} otherwise
513 public boolean hasFriend(String friendSoneId) {
514 return friendSones.contains(friendSoneId);
518 * Adds the given Sone as a friend Sone.
521 * The friend Sone to add
522 * @return This Sone (for method chaining)
524 public Sone addFriend(String friendSone) {
525 if (!friendSone.equals(id)) {
526 friendSones.add(friendSone);
532 * Removes the given Sone as a friend Sone.
534 * @param friendSoneId
535 * The ID of the friend Sone to remove
536 * @return This Sone (for method chaining)
538 public Sone removeFriend(String friendSoneId) {
539 friendSones.remove(friendSoneId);
544 * Returns the list of posts of this Sone, sorted by time, newest first.
546 * @return All posts of this Sone
548 public List<Post> getPosts() {
549 List<Post> sortedPosts;
550 synchronized (this) {
551 sortedPosts = new ArrayList<Post>(posts);
553 Collections.sort(sortedPosts, Post.TIME_COMPARATOR);
558 * Sets all posts of this Sone at once.
561 * The new (and only) posts of this Sone
562 * @return This Sone (for method chaining)
564 public Sone setPosts(Collection<Post> posts) {
565 synchronized (this) {
567 this.posts.addAll(posts);
573 * Adds the given post to this Sone. The post will not be added if its
574 * {@link Post#getSone() Sone} is not this Sone.
579 public void addPost(Post post) {
580 if (post.getSone().equals(this) && posts.add(post)) {
581 logger.log(Level.FINEST, String.format("Adding %s to “%s”.", post, getName()));
586 * Removes the given post from this Sone.
591 public void removePost(Post post) {
592 if (post.getSone().equals(this)) {
598 * Returns all replies this Sone made.
600 * @return All replies this Sone made
602 public Set<PostReply> getReplies() {
603 return Collections.unmodifiableSet(replies);
607 * Sets all replies of this Sone at once.
610 * The new (and only) replies of this Sone
611 * @return This Sone (for method chaining)
613 public Sone setReplies(Collection<PostReply> replies) {
614 this.replies.clear();
615 this.replies.addAll(replies);
620 * Adds a reply to this Sone. If the given reply was not made by this Sone,
621 * nothing is added to this Sone.
626 public void addReply(PostReply reply) {
627 if (reply.getSone().equals(this)) {
633 * Removes a reply from this Sone.
636 * The reply to remove
638 public void removeReply(PostReply reply) {
639 if (reply.getSone().equals(this)) {
640 replies.remove(reply);
645 * Returns the IDs of all liked posts.
647 * @return All liked posts’ IDs
649 public Set<String> getLikedPostIds() {
650 return Collections.unmodifiableSet(likedPostIds);
654 * Sets the IDs of all liked posts.
656 * @param likedPostIds
657 * All liked posts’ IDs
658 * @return This Sone (for method chaining)
660 public Sone setLikePostIds(Set<String> likedPostIds) {
661 this.likedPostIds.clear();
662 this.likedPostIds.addAll(likedPostIds);
667 * Checks whether the given post ID is liked by this Sone.
671 * @return {@code true} if this Sone likes the given post, {@code false}
674 public boolean isLikedPostId(String postId) {
675 return likedPostIds.contains(postId);
679 * Adds the given post ID to the list of posts this Sone likes.
683 * @return This Sone (for method chaining)
685 public Sone addLikedPostId(String postId) {
686 likedPostIds.add(postId);
691 * Removes the given post ID from the list of posts this Sone likes.
695 * @return This Sone (for method chaining)
697 public Sone removeLikedPostId(String postId) {
698 likedPostIds.remove(postId);
703 * Returns the IDs of all liked replies.
705 * @return All liked replies’ IDs
707 public Set<String> getLikedReplyIds() {
708 return Collections.unmodifiableSet(likedReplyIds);
712 * Sets the IDs of all liked replies.
714 * @param likedReplyIds
715 * All liked replies’ IDs
716 * @return This Sone (for method chaining)
718 public Sone setLikeReplyIds(Set<String> likedReplyIds) {
719 this.likedReplyIds.clear();
720 this.likedReplyIds.addAll(likedReplyIds);
725 * Checks whether the given reply ID is liked by this Sone.
728 * The ID of the reply
729 * @return {@code true} if this Sone likes the given reply, {@code false}
732 public boolean isLikedReplyId(String replyId) {
733 return likedReplyIds.contains(replyId);
737 * Adds the given reply ID to the list of replies this Sone likes.
740 * The ID of the reply
741 * @return This Sone (for method chaining)
743 public Sone addLikedReplyId(String replyId) {
744 likedReplyIds.add(replyId);
749 * Removes the given post ID from the list of replies this Sone likes.
752 * The ID of the reply
753 * @return This Sone (for method chaining)
755 public Sone removeLikedReplyId(String replyId) {
756 likedReplyIds.remove(replyId);
761 * Returns the albums of this Sone.
763 * @return The albums of this Sone
765 public List<Album> getAlbums() {
766 return Collections.unmodifiableList(albums);
770 * Returns a flattened list of all albums of this Sone. The resulting list
771 * contains parent albums before child albums so that the resulting list can
772 * be parsed in a single pass.
774 * @return The flattened albums
776 public List<Album> getAllAlbums() {
777 List<Album> flatAlbums = new ArrayList<Album>();
778 flatAlbums.addAll(albums);
779 int lastAlbumIndex = 0;
780 while (lastAlbumIndex < flatAlbums.size()) {
781 int previousAlbumCount = flatAlbums.size();
782 for (Album album : new ArrayList<Album>(flatAlbums.subList(lastAlbumIndex, flatAlbums.size()))) {
783 flatAlbums.addAll(album.getAlbums());
785 lastAlbumIndex = previousAlbumCount;
791 * Returns all images of a Sone. Images of a album are inserted into this
792 * list before images of all child albums.
794 * @return The list of all images
796 public List<Image> getAllImages() {
797 List<Image> allImages = new ArrayList<Image>();
798 for (Album album : getAllAlbums()) {
799 allImages.addAll(album.getImages());
805 * Adds an album to this Sone.
810 public void addAlbum(Album album) {
811 Validation.begin().isNotNull("Album", album).check().isEqual("Album Owner", album.getSone(), this).check();
812 if (!albums.contains(album)) {
818 * Sets the albums of this Sone.
821 * The albums of this Sone
823 public void setAlbums(Collection<? extends Album> albums) {
824 Validation.begin().isNotNull("Albums", albums).check();
826 for (Album album : albums) {
832 * Removes an album from this Sone.
835 * The album to remove
837 public void removeAlbum(Album album) {
838 Validation.begin().isNotNull("Album", album).check().isEqual("Album Owner", album.getSone(), this).check();
839 albums.remove(album);
843 * Moves the given album up in this album’s albums. If the album is already
844 * the first album, nothing happens.
847 * The album to move up
848 * @return The album that the given album swapped the place with, or
849 * <code>null</code> if the album did not change its place
851 public Album moveAlbumUp(Album album) {
852 Validation.begin().isNotNull("Album", album).check().isEqual("Album Owner", album.getSone(), this).isNull("Album Parent", album.getParent()).check();
853 int oldIndex = albums.indexOf(album);
857 albums.remove(oldIndex);
858 albums.add(oldIndex - 1, album);
859 return albums.get(oldIndex);
863 * Moves the given album down in this album’s albums. If the album is
864 * already the last album, nothing happens.
867 * The album to move down
868 * @return The album that the given album swapped the place with, or
869 * <code>null</code> if the album did not change its place
871 public Album moveAlbumDown(Album album) {
872 Validation.begin().isNotNull("Album", album).check().isEqual("Album Owner", album.getSone(), this).isNull("Album Parent", album.getParent()).check();
873 int oldIndex = albums.indexOf(album);
874 if ((oldIndex < 0) || (oldIndex >= (albums.size() - 1))) {
877 albums.remove(oldIndex);
878 albums.add(oldIndex + 1, album);
879 return albums.get(oldIndex);
883 * Returns Sone-specific options.
885 * @return The options of this Sone
887 public Options getOptions() {
892 // FINGERPRINTABLE METHODS
899 public synchronized String getFingerprint() {
900 StringBuilder fingerprint = new StringBuilder();
901 fingerprint.append(profile.getFingerprint());
903 fingerprint.append("Posts(");
904 for (Post post : getPosts()) {
905 fingerprint.append("Post(").append(post.getId()).append(')');
907 fingerprint.append(")");
909 List<PostReply> replies = new ArrayList<PostReply>(getReplies());
910 Collections.sort(replies, Reply.TIME_COMPARATOR);
911 fingerprint.append("Replies(");
912 for (PostReply reply : replies) {
913 fingerprint.append("Reply(").append(reply.getId()).append(')');
915 fingerprint.append(')');
917 List<String> likedPostIds = new ArrayList<String>(getLikedPostIds());
918 Collections.sort(likedPostIds);
919 fingerprint.append("LikedPosts(");
920 for (String likedPostId : likedPostIds) {
921 fingerprint.append("Post(").append(likedPostId).append(')');
923 fingerprint.append(')');
925 List<String> likedReplyIds = new ArrayList<String>(getLikedReplyIds());
926 Collections.sort(likedReplyIds);
927 fingerprint.append("LikedReplies(");
928 for (String likedReplyId : likedReplyIds) {
929 fingerprint.append("Reply(").append(likedReplyId).append(')');
931 fingerprint.append(')');
933 fingerprint.append("Albums(");
934 for (Album album : albums) {
935 fingerprint.append(album.getFingerprint());
937 fingerprint.append(')');
939 return fingerprint.toString();
943 // INTERFACE Comparable<Sone>
950 public int compareTo(Sone sone) {
951 return NICE_NAME_COMPARATOR.compare(this, sone);
962 public int hashCode() {
963 return id.hashCode();
970 public boolean equals(Object object) {
971 if (!(object instanceof Sone)) {
974 return ((Sone) object).id.equals(id);
981 public String toString() {
982 return getClass().getName() + "[identity=" + identity + ",requestUri=" + requestUri + ",insertUri(" + String.valueOf(insertUri).length() + "),friends(" + friendSones.size() + "),posts(" + posts.size() + "),replies(" + replies.size() + ")]";