package models; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Set; import models.helpers.ICleanup; import models.helpers.Tools; /** * An {@link Item} which has a content, can be commented and can be voted up and * down. * * @author Simon Marti * @author Mirco Kocher */ public abstract class Entry extends Item implements Comparable<Entry>, ICleanup<Item> { private final String content; private String contentText, contentHtml; private final HashMap<Integer, Comment> comments; private final HashMap<User, Vote> votes; private final Set<Notification> notifications; private boolean possiblySpam; private int cachedRating; /** * Create an <code>Entry</code> with Markdown or HTML content. The content * will be sanitized before being used, the returned HTML should be safe to * include without further processing in any web page. * * @param owner * the {@link User} who owns the <code>Entry</code> * @param content * the content of the <code>Entry</code> (Markdown/HTML) */ public Entry(User owner, String content) { super(owner); if (content == null) { content = ""; } this.content = content; this.comments = new HashMap<Integer, Comment>(); this.votes = new HashMap<User, Vote>(); this.notifications = new HashSet<Notification>(); this.cachedRating = 0; this.possiblySpam = false; } /* * (non-Javadoc) * * @see models.Item#delete() */ @Override public void delete() { for (Comment comment : new ArrayList<Comment>(this.comments.values())) { comment.delete(); } for (Vote vote : new ArrayList<Vote>(this.votes.values())) { vote.delete(); } for (Notification notification : new ArrayList<Notification>( this.notifications)) { notification.delete(); } super.delete(); } /** * Called by the various <code>Item</code>s associated with this * <code>Entry</code> when they're deleted so that the references to them * kept by this <code>Entry</code> can be removed. * * @see models.helpers.ICleanup#cleanUp(java.lang.Object) */ public void cleanUp(Item item) { if (item instanceof Comment) { this.comments.remove(item.id()); } else if (item instanceof Vote) { this.votes.remove(item.owner()); this.cachedRating -= ((Vote) item).up() ? 1 : -1; } else if (item instanceof Notification) { this.notifications.remove(item); } } /** * Gets the content of an <code>Entry</code> as cleaned up HTML. * * @return the HTML-content of the <code>Entry</code> */ public String content() { if (this.contentHtml == null) this.contentHtml = Tools.markdownToHtml(this.content); return this.contentHtml; } /** * Gets this <code>Entry</code>'s content stripped of all HTML tags. Use * this e.g. for searching. * * @return the text extracted from this <code>Entry</code>'s content */ public String getContentText() { if (this.contentText == null) this.contentText = Tools.htmlToText(this.content()); return this.contentText; } /** * This is a comment-Factory method that creates a new {@link Comment} to * this <code>Entry</code> and adds it to the <code>Entry</code>'s list of * comments. To remove the {@link Comment}, call its delete method. The * comment's content can be either Markdown or HTML, both of which will be * converted to a safe subset of HTML. * * @param user * the {@link User} posting the {@link Comment} * @param content * the comment's content as Markdown or HTML * @return the created {@link Comment} */ public Comment comment(User user, String content) { Comment comment = new Comment(user, this, content); this.comments.put(comment.id(), comment); return comment; } /** * Checks if a {@link Comment} belongs to an <code>Entry</code>. * * @param comment * the {@link Comment} to check * @return true if the {@link Comment} belongs to the <code>Entry</code> */ public boolean hasComment(Comment comment) { return this.comments.containsValue(comment); } /** * Get all {@link Comment}s to an <code>Entry</code> sorted by age (oldest * first). * * @return {@link Collection} of {@link Comments} */ public List<Comment> comments() { List<Comment> list = new ArrayList<Comment>(this.comments.values()); Collections.sort(list); return list; } /** * Get a specific {@link Comment} to an <code>Entry</code>. * * @param id * of the <code>Comment</code> * @return {@link Comment} or null */ public Comment getComment(int id) { return this.comments.get(id); } /** * Count all positive {@link Vote}s on an <code>Entry</code>. * * @return number of positive {@link Vote}s */ public int upVotes() { return this.countVotes(true); } /** * Count all negative {@link Vote}s on an <code>Entry</code>. * * @return number of negative {@link Vote}s */ public int downVotes() { return this.countVotes(false); } /** * Get the current rating of the <code>Entry</code>. * * @return rating as an <code>Integer</code> */ public int rating() { return this.cachedRating; } public void registerNotification(Notification notification) { this.notifications.add(notification); } /** * Compares this <code>Entry</code> with another one with respect to their * ratings (or their age, if they've got identical ratings). * * @return comparison result (-1 = this Entry has more upVotes) */ public int compareTo(Entry e) { int diff = e.rating() - this.rating(); if (diff == 0) // compare by ID instead of - potentially identical - timestamp // for a guaranteed stable sorting (makes testing easier) return this.id() - e.id(); return diff; } /** * Counts the number of <code>Votes</code> of an <code>Entry</code>. * * @param up * boolean whether there is a <code>Vote</code> to this * <code>Entry</code> or not * @return counter number of <code>Votes</code> */ private int countVotes(boolean up) { int counter = 0; for (Vote vote : this.votes.values()) if (vote.up() == up) { counter++; } return counter; } /** * Vote an <code>Entry</code> up. * * @param user * the {@link User} who voted * @return the {@link Vote} */ public Vote voteUp(User user) { return this.vote(user, true); } /** * Vote an <code>Entry</code> down. * * @param user * the {@link User} who voted * @return the {@link Vote} */ public Vote voteDown(User user) { return this.vote(user, false); } /** * Cancel a vote for an <code>Entry</code> (if there was one). * * @param user * the {@link User} who voted * @return the {@link Vote} that was removed (or <code>null</code>) */ public Vote voteCancel(User user) { if (this.hasVote(user)) { Vote oldVote = this.votes.get(user); oldVote.delete(); } return this.votes.remove(user); } /** * Checks for an up-vote for a specific user * * @param user * the {@link User} to check for * @return true, if the given user has indeed voted for this * <code>Entry</code> */ public boolean hasUpVote(User user) { return this.hasVote(user) && this.votes.get(user).up(); } /** * Checks for a down-vote for a specific user * * @param user * the {@link User} to check for * @return true, if the given user has indeed voted for this * <code>Entry</code> */ public boolean hasDownVote(User user) { return this.hasVote(user) && !this.votes.get(user).up(); } /** * Checks for vote for a specific user * * @param user * the {@link User} to check for * @return true, if the given user has indeed voted for this * <code>Entry</code> */ private boolean hasVote(User user) { return this.votes.containsKey(user); } /** * Let an <code>User</code> vote for an <code>Entry</code>. * * @param user * who is voting * @return vote of the <code>User</code> */ private Vote vote(User user, boolean up) { if (user == this.owner()) return null; this.voteCancel(user); Vote vote = new Vote(user, this, up); this.votes.put(user, vote); this.cachedRating += up ? 1 : -1; return vote; } /** * Turns this Entry into an anonymous (user-less) one. */ public void anonymize() { this.unregisterUser(); } /** * Produces a one-line summary of an Entry: the first 75 to 85 characters, * if possible cut off at a word boundary, and an ellipsis, if the content * is longer. * * @return a one-line summary of an <code>Entry</code>. * */ public String summary() { return this.getContentText().replaceAll("\\s+", " ") .replaceFirst("^(.{75}\\S{0,9} ?).{5,}", "$1..."); } /** * Declare this post to be probably spam. Optionally sends a notice to the * moderating staff to check if this really is spam. * * @param moderatorMailbox * an optional reference to a mailbox to dump the spam * notification in */ public void markSpam(IMailbox moderatorMailbox) { if (this.owner().isSpammer()) { this.confirmSpam(); } else if (!this.possiblySpam) { if (moderatorMailbox != null) { moderatorMailbox.notify(null, this); } this.possiblySpam = true; } } /** * A moderator declares this to be definitively spam. It deletes the post * and blocks the user that posted it in the first place. */ public void confirmSpam() { this.owner().setIsSpammer(true); this.delete(); } /** * Return whether this <code>Entry</code> has been marked as possibly being * spam by a user but this status has not yet been confirmed by a moderator * (in which case this <code>Entry</code> would already have been deleted). * * @return true, if the <code>Entry</code> has been marked as being spam by * any user */ public boolean isPossiblySpam() { return this.possiblySpam; } @Override public String toString() { String className = this.getClass().getName(); className = className.substring(className.lastIndexOf(".") + 1); return className + "(" + this.summary() + ")"; } }