X-Git-Url: https://git.pterodactylus.net/?p=Sone.git;a=blobdiff_plain;f=src%2Fmain%2Fjava%2Fnet%2Fpterodactylus%2Fsone%2Fdata%2FSone.java;h=22dc593d3215deb12ffbf812c7fc854dbf275be9;hp=2bb5a3e204af7e475c7af9ec2117eabc46adc8d2;hb=c2e868714435ac7c75d77d1911d0dfb00393d051;hpb=62ebf3da00001abb841c92128c4fdb57d0795ee2 diff --git a/src/main/java/net/pterodactylus/sone/data/Sone.java b/src/main/java/net/pterodactylus/sone/data/Sone.java index 2bb5a3e..22dc593 100644 --- a/src/main/java/net/pterodactylus/sone/data/Sone.java +++ b/src/main/java/net/pterodactylus/sone/data/Sone.java @@ -1,5 +1,5 @@ /* - * FreenetSone - Sone.java - Copyright © 2010 David Roden + * Sone - Sone.java - Copyright © 2010–2013 David Roden * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -17,30 +17,88 @@ package net.pterodactylus.sone.data; +import static com.google.common.base.Preconditions.*; +import static com.google.common.collect.FluentIterable.from; +import static java.util.Arrays.asList; +import static net.pterodactylus.sone.data.Album.FLATTENER; +import static net.pterodactylus.sone.data.Album.IMAGES; + import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; -import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; import java.util.logging.Level; import java.util.logging.Logger; +import net.pterodactylus.sone.core.Options; import net.pterodactylus.sone.freenet.wot.Identity; +import net.pterodactylus.sone.freenet.wot.OwnIdentity; import net.pterodactylus.sone.template.SoneAccessor; import net.pterodactylus.util.logging.Logging; + import freenet.keys.FreenetURI; +import com.google.common.base.Predicate; +import com.google.common.hash.Hasher; +import com.google.common.hash.Hashing; +import com.google.common.primitives.Ints; + /** * A Sone defines everything about a user: her profile, her status updates, her * replies, her likes and dislikes, etc. - *

+ *

* Operations that modify the Sone need to synchronize on the Sone in question. * * @author David ‘Bombe’ Roden */ -public class Sone { +public class Sone implements Fingerprintable, Comparable { + + /** + * Enumeration for the possible states of a {@link Sone}. + * + * @author David ‘Bombe’ Roden + */ + public enum SoneStatus { + + /** The Sone is unknown, i.e. not yet downloaded. */ + unknown, + + /** The Sone is idle, i.e. not being downloaded or inserted. */ + idle, + + /** The Sone is currently being inserted. */ + inserting, + + /** The Sone is currently being downloaded. */ + downloading, + } + + /** + * The possible values for the “show custom avatars” option. + * + * @author David ‘Bombe’ Roden + */ + public static enum ShowCustomAvatars { + + /** Never show custom avatars. */ + NEVER, + + /** Only show custom avatars of followed Sones. */ + FOLLOWED, + + /** Only show custom avatars of Sones you manually trust. */ + MANUALLY_TRUSTED, + + /** Only show custom avatars of automatically trusted Sones. */ + TRUSTED, + + /** Always show custom avatars. */ + ALWAYS, + + } /** comparator that sorts Sones by their nice name. */ public static final Comparator NICE_NAME_COMPARATOR = new Comparator() { @@ -56,12 +114,79 @@ public class Sone { }; + /** Comparator that sorts Sones by last activity (least recent active first). */ + public static final Comparator LAST_ACTIVITY_COMPARATOR = new Comparator() { + + @Override + public int compare(Sone firstSone, Sone secondSone) { + return (int) Math.min(Integer.MAX_VALUE, Math.max(Integer.MIN_VALUE, secondSone.getTime() - firstSone.getTime())); + } + }; + + /** Comparator that sorts Sones by numbers of posts (descending). */ + public static final Comparator POST_COUNT_COMPARATOR = new Comparator() { + + /** + * {@inheritDoc} + */ + @Override + public int compare(Sone leftSone, Sone rightSone) { + return (leftSone.getPosts().size() != rightSone.getPosts().size()) ? (rightSone.getPosts().size() - leftSone.getPosts().size()) : (rightSone.getReplies().size() - leftSone.getReplies().size()); + } + }; + + /** Comparator that sorts Sones by number of images (descending). */ + public static final Comparator IMAGE_COUNT_COMPARATOR = new Comparator() { + + /** + * {@inheritDoc} + */ + @Override + public int compare(Sone leftSone, Sone rightSone) { + int rightSoneImageCount = from(asList(rightSone.getRootAlbum())).transformAndConcat(FLATTENER).transformAndConcat(IMAGES).size(); + int leftSoneImageCount = from(asList(leftSone.getRootAlbum())).transformAndConcat(FLATTENER).transformAndConcat(IMAGES).size(); + /* sort descending. */ + return Ints.compare(rightSoneImageCount, leftSoneImageCount); + } + }; + + /** Filter to remove Sones that have not been downloaded. */ + public static final Predicate EMPTY_SONE_FILTER = new Predicate() { + + @Override + public boolean apply(Sone sone) { + return sone.getTime() != 0; + } + }; + + /** Filter that matches all {@link Sone#isLocal() local Sones}. */ + public static final Predicate LOCAL_SONE_FILTER = new Predicate() { + + @Override + public boolean apply(Sone sone) { + return sone.getIdentity() instanceof OwnIdentity; + } + + }; + + /** Filter that matches Sones that have at least one album. */ + public static final Predicate HAS_ALBUM_FILTER = new Predicate() { + + @Override + public boolean apply(Sone sone) { + return !sone.getRootAlbum().getAlbums().isEmpty(); + } + }; + /** The logger. */ private static final Logger logger = Logging.getLogger(Sone.class); /** The ID of this Sone. */ private final String id; + /** Whether the Sone is local. */ + private final boolean local; + /** The identity of this Sone. */ private Identity identity; @@ -78,35 +203,50 @@ public class Sone { /** The time of the last inserted update. */ private volatile long time; + /** The status of this Sone. */ + private volatile SoneStatus status = SoneStatus.unknown; + /** The profile of this Sone. */ - private volatile Profile profile; + private volatile Profile profile = new Profile(this); + + /** The client used by the Sone. */ + private volatile Client client; + + /** Whether this Sone is known. */ + private volatile boolean known; /** All friend Sones. */ - private final Set friendSones = Collections.synchronizedSet(new HashSet()); + private final Set friendSones = new CopyOnWriteArraySet(); /** All posts. */ - private final Set posts = Collections.synchronizedSet(new HashSet()); + private final Set posts = new CopyOnWriteArraySet(); /** All replies. */ - private final Set replies = Collections.synchronizedSet(new HashSet()); + private final Set replies = new CopyOnWriteArraySet(); /** The IDs of all liked posts. */ - private final Set likedPostIds = Collections.synchronizedSet(new HashSet()); + private final Set likedPostIds = new CopyOnWriteArraySet(); /** The IDs of all liked replies. */ - private final Set likedReplyIds = Collections.synchronizedSet(new HashSet()); + private final Set likedReplyIds = new CopyOnWriteArraySet(); - /** Modification count. */ - private volatile long modificationCounter = 0; + /** The root album containing all albums. */ + private final Album rootAlbum = new Album().setSone(this); + + /** Sone-specific options. */ + private Options options = new Options(); /** * Creates a new Sone. * * @param id - * The ID of the Sone + * The ID of the Sone + * @param local + * {@code true} if the Sone is a local Sone, {@code false} otherwise */ - public Sone(String id) { + public Sone(String id, boolean local) { this.id = id; + this.local = local; } // @@ -136,10 +276,10 @@ public class Sone { * identity has to match this Sone’s {@link #getId()}. * * @param identity - * The identity of this Sone + * The identity of this Sone * @return This Sone (for method chaining) * @throws IllegalArgumentException - * if the ID of the identity does not match this Sone’s ID + * if the ID of the identity does not match this Sone’s ID */ public Sone setIdentity(Identity identity) throws IllegalArgumentException { if (!identity.getId().equals(id)) { @@ -159,6 +299,15 @@ public class Sone { } /** + * Returns whether this Sone is a local Sone. + * + * @return {@code true} if this Sone is a local Sone, {@code false} otherwise + */ + public boolean isLocal() { + return local; + } + + /** * Returns the request URI of this Sone. * * @return The request URI of this Sone @@ -171,16 +320,16 @@ public class Sone { * Sets the request URI of this Sone. * * @param requestUri - * The request URI of this Sone + * The request URI of this Sone * @return This Sone (for method chaining) */ public Sone setRequestUri(FreenetURI requestUri) { if (this.requestUri == null) { - this.requestUri = requestUri.setDocName("Sone").setMetaString(new String[0]); + this.requestUri = requestUri.setKeyType("USK").setDocName("Sone").setMetaString(new String[0]); return this; } if (!this.requestUri.equalsKeypair(requestUri)) { - logger.log(Level.WARNING, "Request URI %s tried to overwrite %s!", new Object[] { requestUri, this.requestUri }); + logger.log(Level.WARNING, String.format("Request URI %s tried to overwrite %s!", requestUri, this.requestUri)); return this; } return this; @@ -199,16 +348,16 @@ public class Sone { * Sets the insert URI of this Sone. * * @param insertUri - * The insert URI of this Sone + * The insert URI of this Sone * @return This Sone (for method chaining) */ public Sone setInsertUri(FreenetURI insertUri) { if (this.insertUri == null) { - this.insertUri = insertUri.setDocName("Sone").setMetaString(new String[0]); + this.insertUri = insertUri.setKeyType("USK").setDocName("Sone").setMetaString(new String[0]); return this; } if (!this.insertUri.equalsKeypair(insertUri)) { - logger.log(Level.WARNING, "Request URI %s tried to overwrite %s!", new Object[] { insertUri, this.insertUri }); + logger.log(Level.WARNING, String.format("Request URI %s tried to overwrite %s!", insertUri, this.insertUri)); return this; } return this; @@ -225,15 +374,15 @@ public class Sone { /** * Sets the latest edition of this Sone. If the given latest edition is not - * greater than the current latest edition, the latest edition of this Sone - * is not changed. + * greater than the current latest edition, the latest edition of this Sone is + * not changed. * * @param latestEdition - * The latest edition of this Sone + * The latest edition of this Sone */ public void setLatestEdition(long latestEdition) { if (!(latestEdition > this.latestEdition)) { - logger.log(Level.INFO, "New latest edition %d is not greater than current latest edition %d!", new Object[] { latestEdition, this.latestEdition }); + logger.log(Level.FINE, String.format("New latest edition %d is not greater than current latest edition %d!", latestEdition, this.latestEdition)); return; } this.latestEdition = latestEdition; @@ -252,7 +401,7 @@ public class Sone { * Sets the time of the last inserted update of this Sone. * * @param time - * The time of the update (in milliseconds since Jan 1, 1970 UTC) + * The time of the update (in milliseconds since Jan 1, 1970 UTC) * @return This Sone (for method chaining) */ public Sone setTime(long time) { @@ -261,9 +410,32 @@ public class Sone { } /** - * Returns a copy of the profile. If you want to update values in the - * profile of this Sone, update the values in the returned {@link Profile} - * and use {@link #setProfile(Profile)} to change the profile in this Sone. + * Returns the status of this Sone. + * + * @return The status of this Sone + */ + public SoneStatus getStatus() { + return status; + } + + /** + * Sets the new status of this Sone. + * + * @param status + * The new status of this Sone + * @return This Sone + * @throws IllegalArgumentException + * if {@code status} is {@code null} + */ + public Sone setStatus(SoneStatus status) { + this.status = checkNotNull(status, "status must not be null"); + return this; + } + + /** + * Returns a copy of the profile. If you want to update values in the profile + * of this Sone, update the values in the returned {@link Profile} and use + * {@link #setProfile(Profile)} to change the profile in this Sone. * * @return A copy of the profile */ @@ -272,58 +444,86 @@ public class Sone { } /** - * Sets the profile of this Sone. A copy of the given profile is stored so - * that subsequent modifications of the given profile are not reflected in - * this Sone! + * Sets the profile of this Sone. A copy of the given profile is stored so that + * subsequent modifications of the given profile are not reflected in this + * Sone! * * @param profile - * The profile to set + * The profile to set */ - public synchronized void setProfile(Profile profile) { + public void setProfile(Profile profile) { this.profile = new Profile(profile); - modificationCounter++; } /** - * Returns all friend Sones of this Sone. + * Returns the client used by this Sone. * - * @return The friend Sones of this Sone + * @return The client used by this Sone, or {@code null} */ - public List getFriends() { - List friends = new ArrayList(friendSones); - return friends; + public Client getClient() { + return client; } /** - * Sets all friends of this Sone at once. + * Sets the client used by this Sone. * - * @param friends - * The new (and only) friends of this Sone + * @param client + * The client used by this Sone, or {@code null} * @return This Sone (for method chaining) */ - public Sone setFriends(Collection friends) { - friendSones.clear(); - friendSones.addAll(friends); + public Sone setClient(Client client) { + this.client = client; return this; } /** + * Returns whether this Sone is known. + * + * @return {@code true} if this Sone is known, {@code false} otherwise + */ + public boolean isKnown() { + return known; + } + + /** + * Sets whether this Sone is known. + * + * @param known + * {@code true} if this Sone is known, {@code false} otherwise + * @return This Sone + */ + public Sone setKnown(boolean known) { + this.known = known; + return this; + } + + /** + * Returns all friend Sones of this Sone. + * + * @return The friend Sones of this Sone + */ + public List getFriends() { + List friends = new ArrayList(friendSones); + return friends; + } + + /** * Returns whether this Sone has the given Sone as a friend Sone. * - * @param friendSone - * The friend Sone to check for - * @return {@code true} if this Sone has the given Sone as a friend, - * {@code false} otherwise + * @param friendSoneId + * The ID of the Sone to check for + * @return {@code true} if this Sone has the given Sone as a friend, {@code + * false} otherwise */ - public boolean hasFriend(Sone friendSone) { - return friendSones.contains(friendSone); + public boolean hasFriend(String friendSoneId) { + return friendSones.contains(friendSoneId); } /** * Adds the given Sone as a friend Sone. * * @param friendSone - * The friend Sone to add + * The friend Sone to add * @return This Sone (for method chaining) */ public Sone addFriend(String friendSone) { @@ -336,12 +536,12 @@ public class Sone { /** * Removes the given Sone as a friend Sone. * - * @param friendSone - * The friend Sone to remove + * @param friendSoneId + * The ID of the friend Sone to remove * @return This Sone (for method chaining) */ - public Sone removeFriend(Sone friendSone) { - friendSones.remove(friendSone); + public Sone removeFriend(String friendSoneId) { + friendSones.remove(friendSoneId); return this; } @@ -363,27 +563,27 @@ public class Sone { * Sets all posts of this Sone at once. * * @param posts - * The new (and only) posts of this Sone + * The new (and only) posts of this Sone * @return This Sone (for method chaining) */ - public synchronized Sone setPosts(Collection posts) { - this.posts.clear(); - this.posts.addAll(posts); - modificationCounter++; + public Sone setPosts(Collection posts) { + synchronized (this) { + this.posts.clear(); + this.posts.addAll(posts); + } return this; } /** - * Adds the given post to this Sone. The post will not be added if its - * {@link Post#getSone() Sone} is not this Sone. + * Adds the given post to this Sone. The post will not be added if its {@link + * Post#getSone() Sone} is not this Sone. * * @param post - * The post to add + * The post to add */ - public synchronized void addPost(Post post) { + public void addPost(Post post) { if (post.getSone().equals(this) && posts.add(post)) { - logger.log(Level.FINEST, "Adding %s to “%s”.", new Object[] { post, getName() }); - modificationCounter++; + logger.log(Level.FINEST, String.format("Adding %s to “%s”.", post, getName())); } } @@ -391,11 +591,11 @@ public class Sone { * Removes the given post from this Sone. * * @param post - * The post to remove + * The post to remove */ - public synchronized void removePost(Post post) { - if (post.getSone().equals(this) && posts.remove(post)) { - modificationCounter++; + public void removePost(Post post) { + if (post.getSone().equals(this)) { + posts.remove(post); } } @@ -404,7 +604,7 @@ public class Sone { * * @return All replies this Sone made */ - public synchronized Set getReplies() { + public Set getReplies() { return Collections.unmodifiableSet(replies); } @@ -412,13 +612,12 @@ public class Sone { * Sets all replies of this Sone at once. * * @param replies - * The new (and only) replies of this Sone + * The new (and only) replies of this Sone * @return This Sone (for method chaining) */ - public synchronized Sone setReplies(Collection replies) { + public Sone setReplies(Collection replies) { this.replies.clear(); this.replies.addAll(replies); - modificationCounter++; return this; } @@ -427,11 +626,11 @@ public class Sone { * nothing is added to this Sone. * * @param reply - * The reply to add + * The reply to add */ - public synchronized void addReply(Reply reply) { - if (reply.getSone().equals(this) && replies.add(reply)) { - modificationCounter++; + public void addReply(PostReply reply) { + if (reply.getSone().equals(this)) { + replies.add(reply); } } @@ -439,11 +638,11 @@ public class Sone { * Removes a reply from this Sone. * * @param reply - * The reply to remove + * The reply to remove */ - public synchronized void removeReply(Reply reply) { - if (reply.getSone().equals(this) && replies.remove(reply)) { - modificationCounter++; + public void removeReply(PostReply reply) { + if (reply.getSone().equals(this)) { + replies.remove(reply); } } @@ -460,13 +659,12 @@ public class Sone { * Sets the IDs of all liked posts. * * @param likedPostIds - * All liked posts’ IDs + * All liked posts’ IDs * @return This Sone (for method chaining) */ - public synchronized Sone setLikePostIds(Set likedPostIds) { + public Sone setLikePostIds(Set likedPostIds) { this.likedPostIds.clear(); this.likedPostIds.addAll(likedPostIds); - modificationCounter++; return this; } @@ -474,7 +672,7 @@ public class Sone { * Checks whether the given post ID is liked by this Sone. * * @param postId - * The ID of the post + * The ID of the post * @return {@code true} if this Sone likes the given post, {@code false} * otherwise */ @@ -486,13 +684,11 @@ public class Sone { * Adds the given post ID to the list of posts this Sone likes. * * @param postId - * The ID of the post + * The ID of the post * @return This Sone (for method chaining) */ - public synchronized Sone addLikedPostId(String postId) { - if (likedPostIds.add(postId)) { - modificationCounter++; - } + public Sone addLikedPostId(String postId) { + likedPostIds.add(postId); return this; } @@ -500,13 +696,11 @@ public class Sone { * Removes the given post ID from the list of posts this Sone likes. * * @param postId - * The ID of the post + * The ID of the post * @return This Sone (for method chaining) */ - public synchronized Sone removeLikedPostId(String postId) { - if (likedPostIds.remove(postId)) { - modificationCounter++; - } + public Sone removeLikedPostId(String postId) { + likedPostIds.remove(postId); return this; } @@ -523,13 +717,12 @@ public class Sone { * Sets the IDs of all liked replies. * * @param likedReplyIds - * All liked replies’ IDs + * All liked replies’ IDs * @return This Sone (for method chaining) */ - public synchronized Sone setLikeReplyIds(Set likedReplyIds) { + public Sone setLikeReplyIds(Set likedReplyIds) { this.likedReplyIds.clear(); this.likedReplyIds.addAll(likedReplyIds); - modificationCounter++; return this; } @@ -537,7 +730,7 @@ public class Sone { * Checks whether the given reply ID is liked by this Sone. * * @param replyId - * The ID of the reply + * The ID of the reply * @return {@code true} if this Sone likes the given reply, {@code false} * otherwise */ @@ -549,13 +742,11 @@ public class Sone { * Adds the given reply ID to the list of replies this Sone likes. * * @param replyId - * The ID of the reply + * The ID of the reply * @return This Sone (for method chaining) */ - public synchronized Sone addLikedReplyId(String replyId) { - if (likedReplyIds.add(replyId)) { - modificationCounter++; - } + public Sone addLikedReplyId(String replyId) { + likedReplyIds.add(replyId); return this; } @@ -563,50 +754,113 @@ public class Sone { * Removes the given post ID from the list of replies this Sone likes. * * @param replyId - * The ID of the reply + * The ID of the reply * @return This Sone (for method chaining) */ - public synchronized Sone removeLikedReplyId(String replyId) { - if (likedReplyIds.remove(replyId)) { - modificationCounter++; - } + public Sone removeLikedReplyId(String replyId) { + likedReplyIds.remove(replyId); return this; } /** - * Returns the modification counter. + * Returns the root album that contains all visible albums of this Sone. * - * @return The modification counter + * @return The root album of this Sone */ - public synchronized long getModificationCounter() { - return modificationCounter; + public Album getRootAlbum() { + return rootAlbum; } /** - * Sets the modification counter. + * Returns Sone-specific options. * - * @param modificationCounter - * The new modification counter + * @return The options of this Sone */ - public synchronized void setModificationCounter(long modificationCounter) { - this.modificationCounter = modificationCounter; + public Options getOptions() { + return options; + } + + /** + * Sets the options of this Sone. + * + * @param options + * The options of this Sone + */ + /* TODO - remove this method again, maybe add an option provider */ + public void setOptions(Options options) { + this.options = options; + } + + // + // FINGERPRINTABLE METHODS + // + + /** {@inheritDoc} */ + @Override + public synchronized String getFingerprint() { + Hasher hash = Hashing.sha256().newHasher(); + hash.putString(profile.getFingerprint()); + + hash.putString("Posts("); + for (Post post : getPosts()) { + hash.putString("Post(").putString(post.getId()).putString(")"); + } + hash.putString(")"); + + List replies = new ArrayList(getReplies()); + Collections.sort(replies, Reply.TIME_COMPARATOR); + hash.putString("Replies("); + for (PostReply reply : replies) { + hash.putString("Reply(").putString(reply.getId()).putString(")"); + } + hash.putString(")"); + + List likedPostIds = new ArrayList(getLikedPostIds()); + Collections.sort(likedPostIds); + hash.putString("LikedPosts("); + for (String likedPostId : likedPostIds) { + hash.putString("Post(").putString(likedPostId).putString(")"); + } + hash.putString(")"); + + List likedReplyIds = new ArrayList(getLikedReplyIds()); + Collections.sort(likedReplyIds); + hash.putString("LikedReplies("); + for (String likedReplyId : likedReplyIds) { + hash.putString("Reply(").putString(likedReplyId).putString(")"); + } + hash.putString(")"); + + hash.putString("Albums("); + for (Album album : albums) { + hash.putString(album.getFingerprint()); + } + hash.putString(")"); + + return hash.hash().toString(); + } + + // + // INTERFACE Comparable + // + + /** {@inheritDoc} */ + @Override + public int compareTo(Sone sone) { + return NICE_NAME_COMPARATOR.compare(this, sone); } // // OBJECT METHODS // - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ @Override public int hashCode() { return id.hashCode(); } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ @Override public boolean equals(Object object) { if (!(object instanceof Sone)) { @@ -615,9 +869,7 @@ public class Sone { return ((Sone) object).id.equals(id); } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ @Override public String toString() { return getClass().getName() + "[identity=" + identity + ",requestUri=" + requestUri + ",insertUri(" + String.valueOf(insertUri).length() + "),friends(" + friendSones.size() + "),posts(" + posts.size() + "),replies(" + replies.size() + ")]";