/*
* Sone - Sone.java - Copyright © 2010 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 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.logging.Level;
import java.util.logging.Logger;
import net.pterodactylus.sone.core.Core;
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.filter.Filter;
import net.pterodactylus.util.logging.Logging;
import net.pterodactylus.util.validation.Validation;
import freenet.keys.FreenetURI;
/**
* 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 Fingerprintable, Comparable {
/** comparator that sorts Sones by their nice name. */
public static final Comparator NICE_NAME_COMPARATOR = new Comparator() {
@Override
public int compare(Sone leftSone, Sone rightSone) {
int diff = SoneAccessor.getNiceName(leftSone).compareToIgnoreCase(SoneAccessor.getNiceName(rightSone));
if (diff != 0) {
return diff;
}
return leftSone.getId().compareToIgnoreCase(rightSone.getId());
}
};
/**
* 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) {
return rightSone.getAllImages().size() - leftSone.getAllImages().size();
}
};
/** Filter to remove Sones that have not been downloaded. */
public static final Filter EMPTY_SONE_FILTER = new Filter() {
@Override
public boolean filterObject(Sone sone) {
return sone.getTime() != 0;
}
};
/** Filter that matches all {@link Core#isLocalSone(Sone) local Sones}. */
public static final Filter LOCAL_SONE_FILTER = new Filter() {
@Override
public boolean filterObject(Sone sone) {
return sone.getIdentity() instanceof OwnIdentity;
}
};
/** Filter that matches Sones that have at least one album. */
public static final Filter HAS_ALBUM_FILTER = new Filter() {
@Override
public boolean filterObject(Sone sone) {
return !sone.getAlbums().isEmpty();
}
};
/** The logger. */
private static final Logger logger = Logging.getLogger(Sone.class);
/** The ID of this Sone. */
private final String id;
/** 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 profile of this Sone. */
private volatile Profile profile = new Profile();
/** The client used by the Sone. */
private volatile Client client;
/** All friend Sones. */
private final Set friendSones = Collections.synchronizedSet(new HashSet());
/** All posts. */
private final Set posts = Collections.synchronizedSet(new HashSet());
/** All replies. */
private final Set replies = Collections.synchronizedSet(new HashSet());
/** The IDs of all liked posts. */
private final Set likedPostIds = Collections.synchronizedSet(new HashSet());
/** The IDs of all liked replies. */
private final Set likedReplyIds = Collections.synchronizedSet(new HashSet());
/** The albums of this Sone. */
private final List albums = Collections.synchronizedList(new ArrayList());
/** Sone-specific options. */
private final Options options = new Options();
/** The avatar of this Sone. */
private volatile String avatar;
/**
* Creates a new Sone.
*
* @param id
* The ID of the Sone
*/
public Sone(String id) {
this.id = id;
}
//
// 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 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;
}
/**
* Returns the name of this Sone.
*
* @return The name of this Sone
*/
public String getName() {
return (identity != null) ? identity.getNickname() : null;
}
/**
* 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, "Request URI %s tried to overwrite %s!", new Object[] { 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, "Request URI %s tried to overwrite %s!", new Object[] { 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, "New latest edition %d is not greater than current latest edition %d!", new Object[] { 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 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 synchronized 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 synchronized 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 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 synchronized 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 synchronized void addPost(Post post) {
if (post.getSone().equals(this) && posts.add(post)) {
logger.log(Level.FINEST, "Adding %s to “%s”.", new Object[] { post, getName() });
}
}
/**
* Removes the given post from this Sone.
*
* @param post
* The post to remove
*/
public synchronized 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 synchronized 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 synchronized 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 synchronized 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 synchronized 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 synchronized 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 synchronized 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 synchronized 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 synchronized 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 synchronized 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 synchronized Sone removeLikedReplyId(String replyId) {
likedReplyIds.remove(replyId);
return this;
}
/**
* Returns the albums of this Sone.
*
* @return The albums of this Sone
*/
public List getAlbums() {
return Collections.unmodifiableList(albums);
}
/**
* Returns a flattened list of all albums of this Sone. The resulting list
* contains parent albums before child albums so that the resulting list can
* be parsed in a single pass.
*
* @return The flattened albums
*/
public List getAllAlbums() {
List flatAlbums = new ArrayList();
flatAlbums.addAll(albums);
int lastAlbumIndex = 0;
while (lastAlbumIndex < flatAlbums.size()) {
int previousAlbumCount = flatAlbums.size();
for (Album album : new ArrayList(flatAlbums.subList(lastAlbumIndex, flatAlbums.size()))) {
flatAlbums.addAll(album.getAlbums());
}
lastAlbumIndex = previousAlbumCount;
}
return flatAlbums;
}
/**
* Returns all images of a Sone. Images of a album are inserted into this
* list before images of all child albums.
*
* @return The list of all images
*/
public List getAllImages() {
List allImages = new ArrayList();
for (Album album : getAllAlbums()) {
allImages.addAll(album.getImages());
}
return allImages;
}
/**
* Adds an album to this Sone.
*
* @param album
* The album to add
*/
public synchronized void addAlbum(Album album) {
Validation.begin().isNotNull("Album", album).check().isEqual("Album Owner", album.getSone(), this).check();
albums.add(album);
}
/**
* Sets the albums of this Sone.
*
* @param albums
* The albums of this Sone
*/
public synchronized void setAlbums(Collection extends Album> albums) {
Validation.begin().isNotNull("Albums", albums).check();
this.albums.clear();
for (Album album : albums) {
addAlbum(album);
}
}
/**
* Removes an album from this Sone.
*
* @param album
* The album to remove
*/
public synchronized void removeAlbum(Album album) {
Validation.begin().isNotNull("Album", album).check().isEqual("Album Owner", album.getSone(), this).check();
albums.remove(album);
}
/**
* Moves the given album up in this album’s albums. If the album is already
* the first album, nothing happens.
*
* @param album
* The album to move up
* @return The album that the given album swapped the place with, or
* null
if the album did not change its place
*/
public Album moveAlbumUp(Album album) {
Validation.begin().isNotNull("Album", album).check().isEqual("Album Owner", album.getSone(), this).isNull("Album Parent", album.getParent()).check();
int oldIndex = albums.indexOf(album);
if (oldIndex <= 0) {
return null;
}
albums.remove(oldIndex);
albums.add(oldIndex - 1, album);
return albums.get(oldIndex);
}
/**
* Moves the given album down in this album’s albums. If the album is
* already the last album, nothing happens.
*
* @param album
* The album to move down
* @return The album that the given album swapped the place with, or
* null
if the album did not change its place
*/
public Album moveAlbumDown(Album album) {
Validation.begin().isNotNull("Album", album).check().isEqual("Album Owner", album.getSone(), this).isNull("Album Parent", album.getParent()).check();
int oldIndex = albums.indexOf(album);
if ((oldIndex < 0) || (oldIndex >= (albums.size() - 1))) {
return null;
}
albums.remove(oldIndex);
albums.add(oldIndex + 1, album);
return albums.get(oldIndex);
}
/**
* Returns the ID of the currently selected avatar image.
*
* @return The ID of the currently selected avatar image, or {@code null} if
* no avatar is selected.
*/
public String getAvatar() {
return avatar;
}
/**
* Sets the avatar image.
*
* @param avatar
* The new avatar image, or {@code null} to not select an avatar
* image.
* @return This Sone
*/
public Sone setAvatar(Image avatar) {
if (avatar == null) {
this.avatar = null;
return this;
}
Validation.begin().isEqual("Image Owner", avatar.getSone(), this).check();
this.avatar = avatar.getId();
return this;
}
/**
* Returns Sone-specific options.
*
* @return The options of this Sone
*/
public Options getOptions() {
return options;
}
//
// FINGERPRINTABLE METHODS
//
/**
* {@inheritDoc}
*/
@Override
public synchronized String getFingerprint() {
StringBuilder fingerprint = new StringBuilder();
fingerprint.append(profile.getFingerprint());
fingerprint.append("Posts(");
for (Post post : getPosts()) {
fingerprint.append("Post(").append(post.getId()).append(')');
}
fingerprint.append(")");
List replies = new ArrayList(getReplies());
Collections.sort(replies, Reply.TIME_COMPARATOR);
fingerprint.append("Replies(");
for (PostReply reply : replies) {
fingerprint.append("Reply(").append(reply.getId()).append(')');
}
fingerprint.append(')');
List likedPostIds = new ArrayList(getLikedPostIds());
Collections.sort(likedPostIds);
fingerprint.append("LikedPosts(");
for (String likedPostId : likedPostIds) {
fingerprint.append("Post(").append(likedPostId).append(')');
}
fingerprint.append(')');
List likedReplyIds = new ArrayList(getLikedReplyIds());
Collections.sort(likedReplyIds);
fingerprint.append("LikedReplies(");
for (String likedReplyId : likedReplyIds) {
fingerprint.append("Reply(").append(likedReplyId).append(')');
}
fingerprint.append(')');
fingerprint.append("Albums(");
for (Album album : albums) {
fingerprint.append(album.getFingerprint());
}
fingerprint.append(')');
fingerprint.append("Avatar(").append(avatar).append(')');
return fingerprint.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() + ")]";
}
}