From 3d888e800617335535c1a4b2cbac3e02acfdde34 Mon Sep 17 00:00:00 2001 From: =?utf8?q?David=20=E2=80=98Bombe=E2=80=99=20Roden?= Date: Thu, 10 Oct 2013 21:36:46 +0200 Subject: [PATCH] Extract an interface out of Sone. --- .../java/net/pterodactylus/sone/core/Core.java | 7 +- .../pterodactylus/sone/core/SoneDownloader.java | 3 +- .../java/net/pterodactylus/sone/data/Sone.java | 424 ++---------- .../java/net/pterodactylus/sone/data/SoneImpl.java | 749 +++++++++++++++++++++ .../pterodactylus/sone/text/SoneTextParser.java | 3 +- .../sone/text/SoneTextParserTest.java | 3 +- 6 files changed, 805 insertions(+), 384 deletions(-) create mode 100644 src/main/java/net/pterodactylus/sone/data/SoneImpl.java diff --git a/src/main/java/net/pterodactylus/sone/core/Core.java b/src/main/java/net/pterodactylus/sone/core/Core.java index 0b9b08f..7e93410 100644 --- a/src/main/java/net/pterodactylus/sone/core/Core.java +++ b/src/main/java/net/pterodactylus/sone/core/Core.java @@ -62,6 +62,7 @@ import net.pterodactylus.sone.data.Reply; import net.pterodactylus.sone.data.Sone; import net.pterodactylus.sone.data.Sone.ShowCustomAvatars; import net.pterodactylus.sone.data.Sone.SoneStatus; +import net.pterodactylus.sone.data.SoneImpl; import net.pterodactylus.sone.data.TemporaryImage; import net.pterodactylus.sone.database.Database; import net.pterodactylus.sone.database.DatabaseException; @@ -378,11 +379,11 @@ public class Core extends AbstractService implements SoneProvider, PostProvider, synchronized (sones) { Sone sone = sones.get(id); if ((sone == null) && create) { - sone = new Sone(id, true); + sone = new SoneImpl(id, true); sones.put(id, sone); } if ((sone != null) && !sone.isLocal()) { - sone = new Sone(id, true); + sone = new SoneImpl(id, true); sones.put(id, sone); } return sone; @@ -419,7 +420,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider, synchronized (sones) { Sone sone = sones.get(id); if ((sone == null) && create && (id != null) && (id.length() == 43)) { - sone = new Sone(id, false); + sone = new SoneImpl(id, false); sones.put(id, sone); } return sone; diff --git a/src/main/java/net/pterodactylus/sone/core/SoneDownloader.java b/src/main/java/net/pterodactylus/sone/core/SoneDownloader.java index 56ae917..cffc8d9 100644 --- a/src/main/java/net/pterodactylus/sone/core/SoneDownloader.java +++ b/src/main/java/net/pterodactylus/sone/core/SoneDownloader.java @@ -35,6 +35,7 @@ import net.pterodactylus.sone.data.PostReply; import net.pterodactylus.sone.data.Profile; import net.pterodactylus.sone.data.Sone; import net.pterodactylus.sone.data.Sone.SoneStatus; +import net.pterodactylus.sone.data.SoneImpl; import net.pterodactylus.sone.database.PostBuilder; import net.pterodactylus.sone.database.PostReplyBuilder; import net.pterodactylus.util.io.Closer; @@ -239,7 +240,7 @@ public class SoneDownloader extends AbstractService { return null; } - Sone sone = new Sone(originalSone.getId(), originalSone.isLocal()).setIdentity(originalSone.getIdentity()); + Sone sone = new SoneImpl(originalSone.getId(), originalSone.isLocal()).setIdentity(originalSone.getIdentity()); SimpleXML soneXml; try { diff --git a/src/main/java/net/pterodactylus/sone/data/Sone.java b/src/main/java/net/pterodactylus/sone/data/Sone.java index 1279f75..d7a7dee 100644 --- a/src/main/java/net/pterodactylus/sone/data/Sone.java +++ b/src/main/java/net/pterodactylus/sone/data/Sone.java @@ -17,44 +17,33 @@ package net.pterodactylus.sone.data; -import static com.google.common.base.Preconditions.checkNotNull; 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.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 implements Identified, Fingerprintable, Comparable { +public interface Sone extends Identified, Fingerprintable, Comparable { /** * Enumeration for the possible states of a {@link Sone}. @@ -178,98 +167,12 @@ public class Sone implements Identified, Fingerprintable, Comparable { } }; - /** 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; - - /** The URI under which the Sone is stored in Freenet. */ - private volatile FreenetURI requestUri; - - /** The URI used to insert a new version of this Sone. */ - /* This will be null for remote Sones! */ - private volatile FreenetURI insertUri; - - /** The latest edition of the Sone. */ - private volatile long latestEdition; - - /** 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 = 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 = new CopyOnWriteArraySet(); - - /** All posts. */ - private final Set posts = new CopyOnWriteArraySet(); - - /** All replies. */ - private final Set replies = new CopyOnWriteArraySet(); - - /** The IDs of all liked posts. */ - private final Set likedPostIds = new CopyOnWriteArraySet(); - - /** The IDs of all liked replies. */ - private final Set likedReplyIds = new CopyOnWriteArraySet(); - - /** The root album containing all albums. */ - private final Album rootAlbum = new AlbumImpl().setSone(this); - - /** Sone-specific options. */ - private Options options = new Options(); - - /** - * Creates a new Sone. - * - * @param id - * The ID of the Sone - * @param local - * {@code true} if the Sone is a local Sone, {@code false} otherwise - */ - public Sone(String id, boolean local) { - this.id = id; - this.local = local; - } - - // - // ACCESSORS - // - - /** - * Returns the identity of this Sone. - * - * @return The identity of this Sone - */ - public String getId() { - return id; - } - /** * Returns the identity of this Sone. * * @return The identity of this Sone */ - public Identity getIdentity() { - return identity; - } + Identity getIdentity(); /** * Sets the identity of this Sone. The {@link Identity#getId() ID} of the @@ -281,40 +184,28 @@ public class Sone implements Identified, Fingerprintable, Comparable { * @throws IllegalArgumentException * 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)) { - throw new IllegalArgumentException("Identity’s ID does not match Sone’s ID!"); - } - this.identity = identity; - return this; - } + Sone setIdentity(Identity identity) throws IllegalArgumentException; /** * Returns the name of this Sone. * * @return The name of this Sone */ - public String getName() { - return (identity != null) ? identity.getNickname() : null; - } + String getName(); /** * 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; - } + boolean isLocal(); /** * Returns the request URI of this Sone. * * @return The request URI of this Sone */ - public FreenetURI getRequestUri() { - return (requestUri != null) ? requestUri.setSuggestedEdition(latestEdition) : null; - } + FreenetURI getRequestUri(); /** * Sets the request URI of this Sone. @@ -323,26 +214,14 @@ public class Sone implements Identified, Fingerprintable, Comparable { * The request URI of this Sone * @return This Sone (for method chaining) */ - public Sone setRequestUri(FreenetURI requestUri) { - if (this.requestUri == null) { - this.requestUri = requestUri.setKeyType("USK").setDocName("Sone").setMetaString(new String[0]); - return this; - } - if (!this.requestUri.equalsKeypair(requestUri)) { - logger.log(Level.WARNING, String.format("Request URI %s tried to overwrite %s!", requestUri, this.requestUri)); - return this; - } - return this; - } + Sone setRequestUri(FreenetURI requestUri); /** * Returns the insert URI of this Sone. * * @return The insert URI of this Sone */ - public FreenetURI getInsertUri() { - return (insertUri != null) ? insertUri.setSuggestedEdition(latestEdition) : null; - } + FreenetURI getInsertUri(); /** * Sets the insert URI of this Sone. @@ -351,26 +230,14 @@ public class Sone implements Identified, Fingerprintable, Comparable { * The insert URI of this Sone * @return This Sone (for method chaining) */ - public Sone setInsertUri(FreenetURI insertUri) { - if (this.insertUri == null) { - this.insertUri = insertUri.setKeyType("USK").setDocName("Sone").setMetaString(new String[0]); - return this; - } - if (!this.insertUri.equalsKeypair(insertUri)) { - logger.log(Level.WARNING, String.format("Request URI %s tried to overwrite %s!", insertUri, this.insertUri)); - return this; - } - return this; - } + Sone setInsertUri(FreenetURI insertUri); /** * Returns the latest edition of this Sone. * * @return The latest edition of this Sone */ - public long getLatestEdition() { - return latestEdition; - } + long getLatestEdition(); /** * Sets the latest edition of this Sone. If the given latest edition is not @@ -380,22 +247,14 @@ public class Sone implements Identified, Fingerprintable, Comparable { * @param latestEdition * The latest edition of this Sone */ - public void setLatestEdition(long latestEdition) { - if (!(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; - } + void setLatestEdition(long latestEdition); /** * Return the time of the last inserted update of this Sone. * * @return The time of the update (in milliseconds since Jan 1, 1970 UTC) */ - public long getTime() { - return time; - } + long getTime(); /** * Sets the time of the last inserted update of this Sone. @@ -404,19 +263,14 @@ public class Sone implements Identified, Fingerprintable, Comparable { * The time of the update (in milliseconds since Jan 1, 1970 UTC) * @return This Sone (for method chaining) */ - public Sone setTime(long time) { - this.time = time; - return this; - } + Sone setTime(long time); /** * Returns the status of this Sone. * * @return The status of this Sone */ - public SoneStatus getStatus() { - return status; - } + SoneStatus getStatus(); /** * Sets the new status of this Sone. @@ -427,10 +281,7 @@ public class Sone implements Identified, Fingerprintable, Comparable { * @throws IllegalArgumentException * if {@code status} is {@code null} */ - public Sone setStatus(SoneStatus status) { - this.status = checkNotNull(status, "status must not be null"); - return this; - } + Sone setStatus(SoneStatus status); /** * Returns a copy of the profile. If you want to update values in the profile @@ -439,9 +290,7 @@ public class Sone implements Identified, Fingerprintable, Comparable { * * @return A copy of the profile */ - public Profile getProfile() { - return new Profile(profile); - } + Profile getProfile(); /** * Sets the profile of this Sone. A copy of the given profile is stored so that @@ -451,18 +300,14 @@ public class Sone implements Identified, Fingerprintable, Comparable { * @param profile * The profile to set */ - public void setProfile(Profile profile) { - this.profile = new Profile(profile); - } + void setProfile(Profile profile); /** * Returns the client used by this Sone. * * @return The client used by this Sone, or {@code null} */ - public Client getClient() { - return client; - } + Client getClient(); /** * Sets the client used by this Sone. @@ -471,19 +316,14 @@ public class Sone implements Identified, Fingerprintable, Comparable { * The client used by this Sone, or {@code null} * @return This Sone (for method chaining) */ - public Sone setClient(Client client) { - this.client = client; - return this; - } + Sone setClient(Client client); /** * Returns whether this Sone is known. * * @return {@code true} if this Sone is known, {@code false} otherwise */ - public boolean isKnown() { - return known; - } + boolean isKnown(); /** * Sets whether this Sone is known. @@ -492,20 +332,14 @@ public class Sone implements Identified, Fingerprintable, Comparable { * {@code true} if this Sone is known, {@code false} otherwise * @return This Sone */ - public Sone setKnown(boolean known) { - this.known = known; - return this; - } + Sone setKnown(boolean known); /** * Returns all friend Sones of this Sone. * * @return The friend Sones of this Sone */ - public List getFriends() { - List friends = new ArrayList(friendSones); - return friends; - } + List getFriends(); /** * Returns whether this Sone has the given Sone as a friend Sone. @@ -515,9 +349,7 @@ public class Sone implements Identified, Fingerprintable, Comparable { * @return {@code true} if this Sone has the given Sone as a friend, {@code * false} otherwise */ - public boolean hasFriend(String friendSoneId) { - return friendSones.contains(friendSoneId); - } + boolean hasFriend(String friendSoneId); /** * Adds the given Sone as a friend Sone. @@ -526,12 +358,7 @@ public class Sone implements Identified, Fingerprintable, Comparable { * The friend Sone to add * @return This Sone (for method chaining) */ - public Sone addFriend(String friendSone) { - if (!friendSone.equals(id)) { - friendSones.add(friendSone); - } - return this; - } + Sone addFriend(String friendSone); /** * Removes the given Sone as a friend Sone. @@ -540,24 +367,14 @@ public class Sone implements Identified, Fingerprintable, Comparable { * The ID of the friend Sone to remove * @return This Sone (for method chaining) */ - public Sone removeFriend(String friendSoneId) { - friendSones.remove(friendSoneId); - return this; - } + Sone removeFriend(String friendSoneId); /** * Returns the list of posts of this Sone, sorted by time, newest first. * * @return All posts of this Sone */ - public List getPosts() { - List sortedPosts; - synchronized (this) { - sortedPosts = new ArrayList(posts); - } - Collections.sort(sortedPosts, Post.TIME_COMPARATOR); - return sortedPosts; - } + List getPosts(); /** * Sets all posts of this Sone at once. @@ -566,13 +383,7 @@ public class Sone implements Identified, Fingerprintable, Comparable { * The new (and only) posts of this Sone * @return This Sone (for method chaining) */ - public Sone setPosts(Collection posts) { - synchronized (this) { - this.posts.clear(); - this.posts.addAll(posts); - } - return this; - } + Sone setPosts(Collection posts); /** * Adds the given post to this Sone. The post will not be added if its {@link @@ -581,11 +392,7 @@ public class Sone implements Identified, Fingerprintable, Comparable { * @param post * The post to add */ - public void addPost(Post post) { - if (post.getSone().equals(this) && posts.add(post)) { - logger.log(Level.FINEST, String.format("Adding %s to “%s”.", post, getName())); - } - } + void addPost(Post post); /** * Removes the given post from this Sone. @@ -593,20 +400,14 @@ public class Sone implements Identified, Fingerprintable, Comparable { * @param post * The post to remove */ - public void removePost(Post post) { - if (post.getSone().equals(this)) { - posts.remove(post); - } - } + void removePost(Post post); /** * Returns all replies this Sone made. * * @return All replies this Sone made */ - public Set getReplies() { - return Collections.unmodifiableSet(replies); - } + Set getReplies(); /** * Sets all replies of this Sone at once. @@ -615,11 +416,7 @@ public class Sone implements Identified, Fingerprintable, Comparable { * The new (and only) replies of this Sone * @return This Sone (for method chaining) */ - public Sone setReplies(Collection replies) { - this.replies.clear(); - this.replies.addAll(replies); - return this; - } + Sone setReplies(Collection replies); /** * Adds a reply to this Sone. If the given reply was not made by this Sone, @@ -628,11 +425,7 @@ public class Sone implements Identified, Fingerprintable, Comparable { * @param reply * The reply to add */ - public void addReply(PostReply reply) { - if (reply.getSone().equals(this)) { - replies.add(reply); - } - } + void addReply(PostReply reply); /** * Removes a reply from this Sone. @@ -640,20 +433,14 @@ public class Sone implements Identified, Fingerprintable, Comparable { * @param reply * The reply to remove */ - public void removeReply(PostReply reply) { - if (reply.getSone().equals(this)) { - replies.remove(reply); - } - } + void removeReply(PostReply reply); /** * Returns the IDs of all liked posts. * * @return All liked posts’ IDs */ - public Set getLikedPostIds() { - return Collections.unmodifiableSet(likedPostIds); - } + Set getLikedPostIds(); /** * Sets the IDs of all liked posts. @@ -662,11 +449,7 @@ public class Sone implements Identified, Fingerprintable, Comparable { * All liked posts’ IDs * @return This Sone (for method chaining) */ - public Sone setLikePostIds(Set likedPostIds) { - this.likedPostIds.clear(); - this.likedPostIds.addAll(likedPostIds); - return this; - } + Sone setLikePostIds(Set likedPostIds); /** * Checks whether the given post ID is liked by this Sone. @@ -676,9 +459,7 @@ public class Sone implements Identified, Fingerprintable, Comparable { * @return {@code true} if this Sone likes the given post, {@code false} * otherwise */ - public boolean isLikedPostId(String postId) { - return likedPostIds.contains(postId); - } + boolean isLikedPostId(String postId); /** * Adds the given post ID to the list of posts this Sone likes. @@ -687,10 +468,7 @@ public class Sone implements Identified, Fingerprintable, Comparable { * The ID of the post * @return This Sone (for method chaining) */ - public Sone addLikedPostId(String postId) { - likedPostIds.add(postId); - return this; - } + Sone addLikedPostId(String postId); /** * Removes the given post ID from the list of posts this Sone likes. @@ -699,19 +477,14 @@ public class Sone implements Identified, Fingerprintable, Comparable { * The ID of the post * @return This Sone (for method chaining) */ - public Sone removeLikedPostId(String postId) { - likedPostIds.remove(postId); - return this; - } + Sone removeLikedPostId(String postId); /** * Returns the IDs of all liked replies. * * @return All liked replies’ IDs */ - public Set getLikedReplyIds() { - return Collections.unmodifiableSet(likedReplyIds); - } + Set getLikedReplyIds(); /** * Sets the IDs of all liked replies. @@ -720,11 +493,7 @@ public class Sone implements Identified, Fingerprintable, Comparable { * All liked replies’ IDs * @return This Sone (for method chaining) */ - public Sone setLikeReplyIds(Set likedReplyIds) { - this.likedReplyIds.clear(); - this.likedReplyIds.addAll(likedReplyIds); - return this; - } + Sone setLikeReplyIds(Set likedReplyIds); /** * Checks whether the given reply ID is liked by this Sone. @@ -734,9 +503,7 @@ public class Sone implements Identified, Fingerprintable, Comparable { * @return {@code true} if this Sone likes the given reply, {@code false} * otherwise */ - public boolean isLikedReplyId(String replyId) { - return likedReplyIds.contains(replyId); - } + boolean isLikedReplyId(String replyId); /** * Adds the given reply ID to the list of replies this Sone likes. @@ -745,10 +512,7 @@ public class Sone implements Identified, Fingerprintable, Comparable { * The ID of the reply * @return This Sone (for method chaining) */ - public Sone addLikedReplyId(String replyId) { - likedReplyIds.add(replyId); - return this; - } + Sone addLikedReplyId(String replyId); /** * Removes the given post ID from the list of replies this Sone likes. @@ -757,28 +521,21 @@ public class Sone implements Identified, Fingerprintable, Comparable { * The ID of the reply * @return This Sone (for method chaining) */ - public Sone removeLikedReplyId(String replyId) { - likedReplyIds.remove(replyId); - return this; - } + Sone removeLikedReplyId(String replyId); /** * Returns the root album that contains all visible albums of this Sone. * * @return The root album of this Sone */ - public Album getRootAlbum() { - return rootAlbum; - } + Album getRootAlbum(); /** * Returns Sone-specific options. * * @return The options of this Sone */ - public Options getOptions() { - return options; - } + Options getOptions(); /** * Sets the options of this Sone. @@ -787,95 +544,6 @@ public class Sone implements Identified, Fingerprintable, Comparable { * 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 : rootAlbum.getAlbums()) { - if (!Album.NOT_EMPTY.apply(album)) { - continue; - } - 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} */ - @Override - public int hashCode() { - return id.hashCode(); - } - - /** {@inheritDoc} */ - @Override - public boolean equals(Object object) { - if (!(object instanceof Sone)) { - return false; - } - return ((Sone) object).id.equals(id); - } - - /** {@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() + "),albums(" + getRootAlbum().getAlbums().size() + ")]"; - } + void setOptions(Options options); } diff --git a/src/main/java/net/pterodactylus/sone/data/SoneImpl.java b/src/main/java/net/pterodactylus/sone/data/SoneImpl.java new file mode 100644 index 0000000..880eb95 --- /dev/null +++ b/src/main/java/net/pterodactylus/sone/data/SoneImpl.java @@ -0,0 +1,749 @@ +/* + * Sone - SoneImpl.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 + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.pterodactylus.sone.data; + +import static com.google.common.base.Preconditions.checkNotNull; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +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.util.logging.Logging; + +import freenet.keys.FreenetURI; + +import com.google.common.hash.Hasher; +import com.google.common.hash.Hashing; + +/** + * {@link Sone} implementation. + *

+ * Operations that modify the Sone need to synchronize on the Sone in question. + * + * @author David ‘Bombe’ Roden + */ +public class SoneImpl implements Sone { + + /** The logger. */ + private static final Logger logger = Logging.getLogger(SoneImpl.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; + + /** The URI under which the Sone is stored in Freenet. */ + private volatile FreenetURI requestUri; + + /** The URI used to insert a new version of this Sone. */ + /* This will be null for remote Sones! */ + private volatile FreenetURI insertUri; + + /** The latest edition of the Sone. */ + private volatile long latestEdition; + + /** 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 = 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 = new CopyOnWriteArraySet(); + + /** All posts. */ + private final Set posts = new CopyOnWriteArraySet(); + + /** All replies. */ + private final Set replies = new CopyOnWriteArraySet(); + + /** The IDs of all liked posts. */ + private final Set likedPostIds = new CopyOnWriteArraySet(); + + /** The IDs of all liked replies. */ + private final Set likedReplyIds = new CopyOnWriteArraySet(); + + /** The root album containing all albums. */ + private final Album rootAlbum = new AlbumImpl().setSone(this); + + /** Sone-specific options. */ + private Options options = new Options(); + + /** + * Creates a new Sone. + * + * @param id + * The ID of the Sone + * @param local + * {@code true} if the Sone is a local Sone, {@code false} otherwise + */ + public SoneImpl(String id, boolean local) { + this.id = id; + this.local = local; + } + + // + // ACCESSORS + // + + /** + * Returns the identity of this Sone. + * + * @return The identity of this Sone + */ + public String getId() { + return id; + } + + /** + * Returns the identity of this Sone. + * + * @return The identity of this Sone + */ + public Identity getIdentity() { + return identity; + } + + /** + * Sets the identity of this Sone. The {@link Identity#getId() ID} of the + * identity has to match this Sone’s {@link #getId()}. + * + * @param identity + * 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 + */ + public SoneImpl setIdentity(Identity identity) throws IllegalArgumentException { + if (!identity.getId().equals(id)) { + throw new IllegalArgumentException("Identity’s ID does not match Sone’s ID!"); + } + this.identity = identity; + return this; + } + + /** + * Returns the name of this Sone. + * + * @return The name of this Sone + */ + public String getName() { + return (identity != null) ? identity.getNickname() : null; + } + + /** + * 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 + */ + public FreenetURI getRequestUri() { + return (requestUri != null) ? requestUri.setSuggestedEdition(latestEdition) : null; + } + + /** + * Sets the request URI of this Sone. + * + * @param requestUri + * The request URI of this Sone + * @return This Sone (for method chaining) + */ + public Sone setRequestUri(FreenetURI requestUri) { + if (this.requestUri == null) { + this.requestUri = requestUri.setKeyType("USK").setDocName("Sone").setMetaString(new String[0]); + return this; + } + if (!this.requestUri.equalsKeypair(requestUri)) { + logger.log(Level.WARNING, String.format("Request URI %s tried to overwrite %s!", requestUri, this.requestUri)); + return this; + } + return this; + } + + /** + * Returns the insert URI of this Sone. + * + * @return The insert URI of this Sone + */ + public FreenetURI getInsertUri() { + return (insertUri != null) ? insertUri.setSuggestedEdition(latestEdition) : null; + } + + /** + * Sets the insert URI of this Sone. + * + * @param insertUri + * The insert URI of this Sone + * @return This Sone (for method chaining) + */ + public Sone setInsertUri(FreenetURI insertUri) { + if (this.insertUri == null) { + this.insertUri = insertUri.setKeyType("USK").setDocName("Sone").setMetaString(new String[0]); + return this; + } + if (!this.insertUri.equalsKeypair(insertUri)) { + logger.log(Level.WARNING, String.format("Request URI %s tried to overwrite %s!", insertUri, this.insertUri)); + return this; + } + return this; + } + + /** + * Returns the latest edition of this Sone. + * + * @return The latest edition of this Sone + */ + public long getLatestEdition() { + return latestEdition; + } + + /** + * 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. + * + * @param latestEdition + * The latest edition of this Sone + */ + public void setLatestEdition(long latestEdition) { + if (!(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; + } + + /** + * Return the time of the last inserted update of this Sone. + * + * @return The time of the update (in milliseconds since Jan 1, 1970 UTC) + */ + public long getTime() { + return time; + } + + /** + * 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) + * @return This Sone (for method chaining) + */ + public Sone setTime(long time) { + this.time = time; + return this; + } + + /** + * 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 + */ + public Profile getProfile() { + return new Profile(profile); + } + + /** + * 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 + */ + public void setProfile(Profile profile) { + this.profile = new Profile(profile); + } + + /** + * Returns the client used by this Sone. + * + * @return The client used by this Sone, or {@code null} + */ + public Client getClient() { + return client; + } + + /** + * Sets the client used by this Sone. + * + * @param client + * The client used by this Sone, or {@code null} + * @return This Sone (for method chaining) + */ + 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 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(String friendSoneId) { + return friendSones.contains(friendSoneId); + } + + /** + * Adds the given Sone as a friend Sone. + * + * @param friendSone + * The friend Sone to add + * @return This Sone (for method chaining) + */ + public Sone addFriend(String friendSone) { + if (!friendSone.equals(id)) { + friendSones.add(friendSone); + } + return this; + } + + /** + * Removes the given Sone as a friend Sone. + * + * @param friendSoneId + * The ID of the friend Sone to remove + * @return This Sone (for method chaining) + */ + public Sone removeFriend(String friendSoneId) { + friendSones.remove(friendSoneId); + return this; + } + + /** + * Returns the list of posts of this Sone, sorted by time, newest first. + * + * @return All posts of this Sone + */ + public List getPosts() { + List sortedPosts; + synchronized (this) { + sortedPosts = new ArrayList(posts); + } + Collections.sort(sortedPosts, Post.TIME_COMPARATOR); + return sortedPosts; + } + + /** + * Sets all posts of this Sone at once. + * + * @param posts + * The new (and only) posts of this Sone + * @return This Sone (for method chaining) + */ + 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. + * + * @param post + * The post to add + */ + public void addPost(Post post) { + if (post.getSone().equals(this) && posts.add(post)) { + logger.log(Level.FINEST, String.format("Adding %s to “%s”.", post, getName())); + } + } + + /** + * Removes the given post from this Sone. + * + * @param post + * The post to remove + */ + public void removePost(Post post) { + if (post.getSone().equals(this)) { + posts.remove(post); + } + } + + /** + * Returns all replies this Sone made. + * + * @return All replies this Sone made + */ + public Set getReplies() { + return Collections.unmodifiableSet(replies); + } + + /** + * Sets all replies of this Sone at once. + * + * @param replies + * The new (and only) replies of this Sone + * @return This Sone (for method chaining) + */ + public Sone setReplies(Collection replies) { + this.replies.clear(); + this.replies.addAll(replies); + return this; + } + + /** + * Adds a reply to this Sone. If the given reply was not made by this Sone, + * nothing is added to this Sone. + * + * @param reply + * The reply to add + */ + public void addReply(PostReply reply) { + if (reply.getSone().equals(this)) { + replies.add(reply); + } + } + + /** + * Removes a reply from this Sone. + * + * @param reply + * The reply to remove + */ + public void removeReply(PostReply reply) { + if (reply.getSone().equals(this)) { + replies.remove(reply); + } + } + + /** + * Returns the IDs of all liked posts. + * + * @return All liked posts’ IDs + */ + public Set getLikedPostIds() { + return Collections.unmodifiableSet(likedPostIds); + } + + /** + * Sets the IDs of all liked posts. + * + * @param likedPostIds + * All liked posts’ IDs + * @return This Sone (for method chaining) + */ + public Sone setLikePostIds(Set likedPostIds) { + this.likedPostIds.clear(); + this.likedPostIds.addAll(likedPostIds); + return this; + } + + /** + * Checks whether the given post ID is liked by this Sone. + * + * @param postId + * The ID of the post + * @return {@code true} if this Sone likes the given post, {@code false} + * otherwise + */ + public boolean isLikedPostId(String postId) { + return likedPostIds.contains(postId); + } + + /** + * Adds the given post ID to the list of posts this Sone likes. + * + * @param postId + * The ID of the post + * @return This Sone (for method chaining) + */ + public Sone addLikedPostId(String postId) { + likedPostIds.add(postId); + return this; + } + + /** + * Removes the given post ID from the list of posts this Sone likes. + * + * @param postId + * The ID of the post + * @return This Sone (for method chaining) + */ + public Sone removeLikedPostId(String postId) { + likedPostIds.remove(postId); + return this; + } + + /** + * Returns the IDs of all liked replies. + * + * @return All liked replies’ IDs + */ + public Set getLikedReplyIds() { + return Collections.unmodifiableSet(likedReplyIds); + } + + /** + * Sets the IDs of all liked replies. + * + * @param likedReplyIds + * All liked replies’ IDs + * @return This Sone (for method chaining) + */ + public Sone setLikeReplyIds(Set likedReplyIds) { + this.likedReplyIds.clear(); + this.likedReplyIds.addAll(likedReplyIds); + return this; + } + + /** + * Checks whether the given reply ID is liked by this Sone. + * + * @param replyId + * The ID of the reply + * @return {@code true} if this Sone likes the given reply, {@code false} + * otherwise + */ + public boolean isLikedReplyId(String replyId) { + return likedReplyIds.contains(replyId); + } + + /** + * Adds the given reply ID to the list of replies this Sone likes. + * + * @param replyId + * The ID of the reply + * @return This Sone (for method chaining) + */ + public Sone addLikedReplyId(String replyId) { + likedReplyIds.add(replyId); + return this; + } + + /** + * Removes the given post ID from the list of replies this Sone likes. + * + * @param replyId + * The ID of the reply + * @return This Sone (for method chaining) + */ + public Sone removeLikedReplyId(String replyId) { + likedReplyIds.remove(replyId); + return this; + } + + /** + * Returns the root album that contains all visible albums of this Sone. + * + * @return The root album of this Sone + */ + public Album getRootAlbum() { + return rootAlbum; + } + + /** + * Returns Sone-specific options. + * + * @return The options of this Sone + */ + 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 : rootAlbum.getAlbums()) { + if (!Album.NOT_EMPTY.apply(album)) { + continue; + } + 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} */ + @Override + public int hashCode() { + return id.hashCode(); + } + + /** {@inheritDoc} */ + @Override + public boolean equals(Object object) { + if (!(object instanceof Sone)) { + return false; + } + return ((Sone) object).getId().equals(id); + } + + /** {@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() + "),albums(" + getRootAlbum().getAlbums().size() + ")]"; + } + +} diff --git a/src/main/java/net/pterodactylus/sone/text/SoneTextParser.java b/src/main/java/net/pterodactylus/sone/text/SoneTextParser.java index 6d81c0d..0b1585a 100644 --- a/src/main/java/net/pterodactylus/sone/text/SoneTextParser.java +++ b/src/main/java/net/pterodactylus/sone/text/SoneTextParser.java @@ -28,6 +28,7 @@ import java.util.regex.Pattern; import net.pterodactylus.sone.data.Post; import net.pterodactylus.sone.data.Sone; +import net.pterodactylus.sone.data.SoneImpl; import net.pterodactylus.sone.database.PostProvider; import net.pterodactylus.sone.database.SoneProvider; import net.pterodactylus.util.io.Closer; @@ -248,7 +249,7 @@ public class SoneTextParser implements Parser { * don’t use create=true above, we don’t want * the empty shell. */ - sone = Optional.fromNullable(new Sone(soneId, false)); + sone = Optional.of(new SoneImpl(soneId, false)); } parts.add(new SonePart(sone.get())); line = line.substring(50); diff --git a/src/test/java/net/pterodactylus/sone/text/SoneTextParserTest.java b/src/test/java/net/pterodactylus/sone/text/SoneTextParserTest.java index 7286001..2c04b23 100644 --- a/src/test/java/net/pterodactylus/sone/text/SoneTextParserTest.java +++ b/src/test/java/net/pterodactylus/sone/text/SoneTextParserTest.java @@ -26,6 +26,7 @@ import com.google.common.base.Optional; import junit.framework.TestCase; import net.pterodactylus.sone.data.Sone; +import net.pterodactylus.sone.data.SoneImpl; import net.pterodactylus.sone.database.SoneProvider; /** @@ -185,7 +186,7 @@ public class SoneTextParserTest extends TestCase { */ @Override public Optional getSone(final String soneId) { - return Optional. fromNullable(new Sone(soneId, false) { + return Optional.of(new SoneImpl(soneId, false) { /** * {@inheritDoc} -- 2.7.4