package models;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import models.helpers.ICleanup;
import models.helpers.IFilter;
import models.helpers.IObservable;
import models.helpers.IObserver;
import models.helpers.Mapper;
import models.helpers.Tools;
/**
* A user with a name. Can contain {@link Item}s i.e. {@link Question}s,
* {@link Answer}s, {@link Comment}s and {@link Vote}s. When deleted, the
* <code>User</code> requests all his {@link Item}s to delete themselves.
*
* Furthermore, they have their own profile with informations about them,
* an email address to contact them and some more attributes and methods that
* represent the user of the system and their actions.
*
* @author Simon Marti
* @author Mirco Kocher
*
*/
public class User implements IObserver, ICleanup<Item> {
private final String name;
private String password;
private String email;
private final HashSet<Item> items;
private String fullname;
protected Date dateOfBirth;
private String website;
private String profession;
private String employer;
private String biography;
private final String confirmKey;
private final Date registrationTimestamp;
private String statustext = "";
private boolean isBlocked = false;
private boolean isModerator = false;
private boolean isConfirmed = false;
private long lastSearch = 0;
private String lastSearchTerm = "";
private long lastPost = 0;
private final Mailbox mainMailbox;
private IMailbox moderatorMailbox;
private boolean isSpammer;
private final ICleanup<User> cleaner;
/**
* Creates a <code>User</code> with a given name.
*
* @param name
* the name of the <code>User</code>
* @param password
* the user's password
* @param email
* the user's valid(!) email address
* @param cleaner
* an optional clean-up object that wants to be notified when
* this user object is no longer needed
*/
public User(String name, String password, String email,
ICleanup<User> cleaner) {
this.name = name;
if (password != null) {
// null passwords may happen during testing, as hashing a password
// can be quite slow in comparison
this.password = Tools.encrypt(password);
}
this.email = email;
this.confirmKey = Tools.randomStringGenerator(35);
this.registrationTimestamp = SysInfo.now();
this.items = new HashSet<Item>();
this.mainMailbox = new Mailbox(name);
this.isSpammer = false;
this.cleaner = cleaner;
}
/**
* Only for tests:
* Creates a <code>User</code> with a given name.
*
* @param name
* the name of the <code>User</code>
*/
public User(String name) {
this(name, null, null, null);
}
public boolean canEdit(Entry entry) {
return entry.owner() == this && !this.isBlocked() || this.isModerator();
}
/**
* Gets the name of the <code>User</code>.
*
* @return name of the <code>User</code>
*/
public String getName() {
return this.name;
}
/**
* Encrypt the password and check if it is the same as the stored one.
*
* @param passwort
* @return true if the password is right
*/
public boolean checkPW(String password) {
return this.password.equals(Tools.encrypt(password));
}
/**
* Registers an {@link Item} which should be deleted in case the
* <code>User</code> gets deleted and, if the item is an {@link Entry},
* remembers the time of the user's last post for spamming prevention.
*
* @param item
* the {@link Item} to register
*/
public void registerItem(Item item) {
this.items.add(item);
this.updateCheaterStatus();
if (item instanceof Entry) {
this.setLastPostTime(SysInfo.now());
}
}
/**
* Causes the <code>User</code> to delete all his {@link Item}s.
*/
public void delete() {
// operate on a clone to prevent a ConcurrentModificationException
HashSet<Item> clone = (HashSet<Item>) this.items.clone();
for (Item item : clone) {
item.delete();
}
this.items.clear();
this.mainMailbox.delete();
if (this.cleaner != null) {
this.cleaner.cleanUp(this);
}
}
/*
* (non-Javadoc)
*
* @see models.helpers.ICleanup#cleanUp(java.lang.Object)
*/
public void cleanUp(Item item) {
this.items.remove(item);
}
/**
* Checks if an {@link Item} is registered and therefore owned by a
* <code>User</code>.
*
* @param item
* the {@link Item}to check
* @return true if the {@link Item} is registered
*/
public boolean hasItem(Item item) {
return this.items.contains(item);
}
/**
* The amount of Comments, Answers and Questions the <code>User</code> has
* posted in the last 60 Minutes.
*
* @return The amount of Comments, Answers and Questions for this
* <code>User</code> in this Hour.
*/
public int howManyItemsPerHour() {
long now = SysInfo.now().getTime();
int i = 0;
for (Item item : this.items) {
if (now - item.timestamp().getTime() <= 60 * 60 * 1000) {
i++;
}
}
return i;
}
/**
* The <code>User</code> is a Cheater if over 50% of his votes is for the
* same <code>User</code>.
*
* @return True if the <code>User</code> is supporting somebody.
*/
public boolean isMaybeCheater() {
if (SysInfo.isInTestMode())
return false;
int voteCount = 0;
HashMap<User, Integer> votesForUser = new HashMap<User, Integer>();
for (Item item : this.items) {
if (item instanceof Vote && ((Vote) item).up()) {
Vote vote = (Vote) item;
Integer count = votesForUser.get(vote.getEntry().owner());
if (count == null) {
count = 0;
}
votesForUser.put(vote.getEntry().owner(), count + 1);
voteCount++;
}
}
if (votesForUser.isEmpty())
return false;
Integer maxCount = Collections.max(votesForUser.values());
return maxCount > 3 && maxCount > 0.5 * voteCount;
}
/**
* Anonymizes all questions, answers and comments by this user.
*
* @param keepOnlyQuestions
* whether to anonymize this user's answers and comments as well
* or whether to just keep his/her questions
*/
public void anonymize(boolean keepOnlyQuestions) {
// operate on a clone to prevent a ConcurrentModificationException
HashSet<Item> clone = (HashSet<Item>) this.items.clone();
for (Item item : clone) {
if (item instanceof Question || keepOnlyQuestions
&& item instanceof Entry) {
((Entry) item).anonymize();
this.items.remove(item);
}
}
}
/**
* The <code>User</code> is a Spammer if he posts more than 60 comments,
* answers or questions in the last hour.
*
* @return True if the <code>User</code> is a Spammer.
*/
public boolean isSpammer() {
if (SysInfo.isInTestMode())
return false;
if (this.isSpammer)
return true;
int number = this.howManyItemsPerHour();
if (number >= 60)
return true;
return false;
}
/**
* A <code>User</code> is a Cheater when he spams the Site or supports
* somebody.
*
* @return true if <code>User</code> is a Spammer or supports somebody.
*
*/
public boolean isCheating() {
return this.isSpammer() || this.isMaybeCheater();
}
/**
* Blocks the User if he is a cheater or unblocks him if he is not cheating.
* The Cheater gets the appropriate status message.
*
* This method is supposed to be called after each new post of this user, as
* we will remember here the time of the user's last post for future
* spamming prevention.
*/
private void updateCheaterStatus() {
if (this.isSpammer()) {
this.block("User is a Spammer");
} else if (this.isMaybeCheater()) {
this.block("User voted up somebody");
}
}
/**
* Calculates the age of the <code>User</code> in years.
*
* @return age of the <code>User</code>
*/
private int age() {
if (this.dateOfBirth != null) {
long age = SysInfo.now().getTime() - this.dateOfBirth.getTime();
return (int) (age / ((long) 1000 * 3600 * 24 * 365));
} else
return 0;
}
/* Getter and Setter for profile data */
public void setEmail(String email) {
this.email = email;
}
public String getEmail() {
return this.email;
}
public void setFullname(String fullname) {
this.fullname = fullname;
}
public String getFullname() {
return this.fullname;
}
public void setDateOfBirth(String birthday) throws ParseException {
this.dateOfBirth = Tools.stringToDate(birthday);
}
public String getDateOfBirth() {
return Tools.dateToString(this.dateOfBirth);
}
public int getAge() {
return this.age();
}
public void setWebsite(String website) {
this.website = website;
}
public String getWebsite() {
return this.website;
}
public void setProfession(String profession) {
this.profession = profession;
}
public String getProfession() {
return this.profession;
}
public void setEmployer(String employer) {
this.employer = employer;
}
public String getEmployer() {
return this.employer;
}
public void setBiography(String biography) {
this.biography = biography;
}
public String getBiography() {
return this.biography;
}
/**
* @return the user's biography as sanitized HTML. The raw biography content
* may contain Markdown and/or HTML.
*/
public String getBiographyHTML() {
if (this.biography == null)
return null;
return Tools.markdownToHtml(this.biography);
}
/**
* @return the SHA-1 hash of the user's password
*/
public String getSHA1Password() {
return this.password;
}
/**
* Sets the user's password by storing only its SHA-1 digest.
*
* @param password
* the password to digest and store
*/
public void setSHA1Password(String password) {
this.password = Tools.encrypt(password);
}
/**
* A newly registered user must provide this randomly generated key in order
* to verify the validity of his/her e-mail address.
*
* @return the confirmation key a user must possess in order to enable
* his/her account
*/
public String getConfirmKey() {
return this.confirmKey;
}
/**
* Returns the time in milliseconds that the user has left before his/her
* unconfirmed account might be deleted so that the username can be reused.
* The confirmation window will last at least one hour. Older unconfirmed
* users will be deleted by {@link CleanUpJobs}.
*
* @return the confirmation limit in milliseconds
*/
public long getConfirmationLimit() {
return this.registrationTimestamp.getTime() + 60 * 60 * 1000
- SysInfo.now().getTime();
}
/**
* Get the reason for why the user is blocked.
*
* @return the reason
*/
public String getStatusMessage() {
return this.statustext;
}
/**
* Blocks a <code>User</code> and gives him the reason.
*
* @param block
* , true if the user has to be blocked
* @param reason
* , why the users is getting blocked.
*/
public void block(String reason) {
this.isBlocked = true;
this.statustext = reason;
}
/**
* Cleans the name of the User. They are no longer accused of any spamming
* or any other thing causing them to be blocked.
*/
public void unblock() {
this.isBlocked = false;
this.isSpammer = false;
this.statustext = "";
}
/**
* Get the current status of the user whether he is blocked or not.
*
* @return true, if the user is blocked
*/
public boolean isBlocked() {
return this.isBlocked;
}
/**
* Get the status of the user whether he is a moderator or not.
*
* @return true, if the user is moderator
*/
public boolean isModerator() {
return this.isModerator;
}
/**
* Get the status of the user if he is confirmed or not.
*
* @return true, if the user is confirmed
*/
public boolean isConfirmed() {
return this.isConfirmed;
}
/**
* Set the status of the user whether he is a moderator or not.
*
* @param mod
* whether the user is to become a moderator or not
* @param mailbox
* an optional moderator mailbox, through which the user will be
* notified about spam reports, etc.
*/
public void setModerator(boolean mod, IMailbox mailbox) {
this.isModerator = mod;
this.moderatorMailbox = mod ? mailbox : null;
}
/**
* Set the status of the user on confirmed
*/
public void confirm() {
this.isConfirmed = true;
}
/**
* Start observing changes for an entry (e.g. new answers to a question).
*
* @param what
* the entry to watch
*/
public void startObserving(IObservable what) {
what.addObserver(this);
}
/**
* Checks if a specific entry is being observed for changes.
*
* @param what
* the entry to check
*/
public boolean isObserving(IObservable what) {
return what.hasObserver(this);
}
/**
* Stop observing changes for an entry (e.g. new answers to a question).
*
* @param what
* the entry to unwatch
*/
public void stopObserving(IObservable what) {
what.removeObserver(this);
}
/**
* Observe new answers added to questions this user is observing (obviously
* ignore answers given by this very user, though).
*
* @see models.IObserver#observe(models.IObservable, java.lang.Object)
*/
public void observe(IObservable o, Object arg) {
if (o instanceof Question && arg instanceof Answer
&& ((Answer) arg).owner() != this) {
this.mainMailbox.notify(this, (Answer) arg);
}
}
/**
* Get a List of the last three <code>Question</code>s of this
* <code>User</code>. Registers a new <code>User</code> to the database.
*
* @param username
* @param password
* of the <code>User</code>
* @return user
*/
public List<Question> getRecentQuestions() {
return this.getRecentItemsByType(Question.class);
}
/**
* Get a list of all Questions the user has answered sorted by how high the
* answers are rated.
*
* @return List<Question>
*/
public List<Question> getSortedAnsweredQuestions() {
List<Question> sortedAnsweredQuestions = new ArrayList<Question>();
/*
* Get all questions the user has answered. Ignore duplicates. Don't add
* those questions belonging to negative rated answers.
*/
// getAnswers already sorts all answers - best first
for (Answer a : this.getAnswers()) {
Question q = a.getQuestion();
if (!sortedAnsweredQuestions.contains(q) && a.rating() >= 0) {
sortedAnsweredQuestions.add(q);
}
}
return sortedAnsweredQuestions;
}
/**
* Get a List of the last three <code>Answer</code>s of this
* <code>User</code>.
*
* @return List<Answer> The last three <code>Answer</code>s of this
* <code>User</code>
*/
public List<Answer> getRecentAnswers() {
return this.getRecentItemsByType(Answer.class);
}
/**
* Get a List of the last three <code>Comment</code>s of this
* <code>User</code>.
*
* @return List<Comment> The last three <code>Comment</code>s of this
* <code>User</code>
*/
public List<Comment> getRecentComments() {
return this.getRecentItemsByType(Comment.class);
}
/**
* Get a List of the last three <code>Items</code>s of type T of this
* <code>User</code>.
*
* @return List<Item> The last three <code>Item</code>s of this
* <code>User</code>
*/
protected List getRecentItemsByType(Class type) {
List recentItems = this.getItemsByType(type);
Collections.sort(recentItems, new Comparator<Item>() {
public int compare(Item i1, Item i2) {
return i2.timestamp().compareTo(i1.timestamp());
}
});
if (recentItems.size() > 3)
return recentItems.subList(0, 3);
return recentItems;
}
/**
* Get a sorted ArrayList of all <code>Questions</code>s of this
* <code>User</code>.
*
* @return ArrayList<Question> All questions of this <code>User</code>
*/
public List<Question> getQuestions() {
return this.getItemsByType(Question.class);
}
/**
* Get a sorted ArrayList of all <code>Answer</code>s of this
* <code>User</code>.
*
* @return ArrayList<Answer> All <code>Answer</code>s of this
* <code>User</code>
*/
public List<Answer> getAnswers() {
return this.getItemsByType(Answer.class);
}
/**
* Get a sorted ArrayList of all <code>Comment</code>s of this
* <code>User</code>
*
* @return ArrayList<Comment> All <code>Comments</code>s of this
* <code>User</code>
*/
public List<Comment> getComments() {
return this.getItemsByType(Comment.class);
}
/**
* Get a List of all best rated answers
*
* @return List<Answer> All best rated answers
*/
public List<Answer> bestAnswers() {
return Mapper.filter(this.getAnswers(), new IFilter<Answer, Boolean>() {
public Boolean visit(Answer a) {
return a.isBestAnswer();
}
});
}
/**
* Get a List of all highRated answers
*
* @return List<Answer> All high rated answers
*/
public List<Answer> highRatedAnswers() {
return Mapper.filter(this.getAnswers(), new IFilter<Answer, Boolean>() {
public Boolean visit(Answer a) {
return a.isHighRated();
}
});
}
/**
* Returns all notifications blonging to this user.
*
* @return the full list of notifications (new and old, read and unread)
*/
public List<Notification> getNotifications() {
List<Notification> all = new LinkedList();
for (IMailbox mailbox : this.getAllMailboxes()) {
all.addAll(mailbox.getAllNotifications());
}
return all;
}
/**
* Gets the most recent unread notification, if there is any very recent one
*
* @return a very recent notification (or null, if there isn't any)
*/
public Notification getVeryRecentNewNotification() {
for (Notification n : this.mainMailbox.getNewNotifications())
if (n.isVeryRecent())
return n;
return null;
}
/**
* Gets a notification by its id value.
*
* NOTE: slightly hacky since we don't track notifications in a separate
* HashMap but in this.items like everything else - this should get fixed
* once we migrate to using a real DB.
*
* @param id
* the notification's id
* @return a notification with the given id
*/
public Notification getNotification(int id) {
for (Notification n : this.getNotifications())
if (n.id() == id)
return n;
return null;
}
/**
* Get an ArrayList of all items of this user being an instance of a
* specific type.
*
* @param type
* the type
* @return ArrayList All type-items of this user
*/
protected List getItemsByType(Class type) {
List items = new ArrayList();
for (Item item : this.items)
if (type.isInstance(item)) {
items.add(item);
}
Collections.sort(items);
return items;
}
public void setDateOfBirth(Date time) {
this.dateOfBirth = time;
}
@Override
public String toString() {
return "U[" + this.name + "]";
}
/**
* Remembers the term a user last searched for and the time of the search so
* that we can specifically permit the user to continue a specific search
* (e.g. display the next batch of search results) while still preventing
* the user from starting a new search too soon after the last one.
*
* @param term
* the term a user has last searched for
* @param time
* the date/time of the last search
*/
public void setLastSearch(String term) {
this.lastSearchTerm = term;
this.lastSearch = SysInfo.now().getTime();
}
/**
* Checks if the user can use the search for a specific term. There must be
* at least 15 seconds between his last search and now, if it's a different
* search.
*
* @return true if the user can search
*/
public boolean canSearchFor(String term) {
return SysInfo.isInTestMode()
|| term.equals(this.lastSearchTerm) || this.timeToSearch() <= 0;
}
/**
* Calculates the remaining time until the user can make a new search.
* Counting down from 15.
*
* @return an Integer that equals the remaining seconds.
*/
public int timeToSearch() {
return (int) (15 - (SysInfo.now().getTime() - this.lastSearch) / 1000);
}
/**
* Set the time of the Users last Post to a specific one.
*
* @param Time
* of the last post
*/
public void setLastPostTime(Date time) {
this.lastPost = time.getTime();
}
/**
* Checks if the user can ask, answer or comment a post. There must be at
* least 30 seconds between his last post to make a new one and he must not
* be blocked.
*
* @return true if the user can post
*/
public boolean canPost() {
return SysInfo.isInTestMode() || !this.isBlocked()
&& this.timeToPost() <= 0;
}
/**
* Calculates the remaining time until he can make a new post. Counting down
* from 30.
*
* @return an Integer that equals the remaining seconds.
*/
public int timeToPost() {
return (int) (30 - (SysInfo.now().getTime() - this.lastPost) / 1000);
}
/**
* Returns all the mailboxes this user has access to. This is usually the
* user's personal mailbox for watch-list notifications and, if the user is
* a moderator, also the global spam report mailbox.
*
* @return a list of all mailboxes
*/
public List<IMailbox> getAllMailboxes() {
List<IMailbox> mailboxes = new ArrayList();
mailboxes.add(this.mainMailbox);
if (this.isModerator())
mailboxes.add(this.moderatorMailbox);
return mailboxes;
}
/**
* @return this user's unread notifications.
*/
public List<Notification> getNewNotifications() {
List<Notification> allNew = new LinkedList();
for (IMailbox mailbox : this.getAllMailboxes()) {
allNew.addAll(mailbox.getNewNotifications());
}
return allNew;
}
/**
* @return this user's recent notifications (arrived within the last 5
* minutes).
*/
public List<Notification> getRecentNotifications() {
List<Notification> allRecent = new LinkedList();
for (IMailbox mailbox : this.getAllMailboxes()) {
allRecent.addAll(mailbox.getRecentNotifications());
}
return allRecent;
}
/**
* Sets this user's current spammer status. If the user is set to be a
* spammer and hasn't been blocked yet, he/she will be blocked in the
* process.
*
* @param isSpammer
* whether this user is a spammer or not
*/
public void setIsSpammer(boolean isSpammer) {
if (isSpammer && !this.isBlocked) {
this.block("Declared Spammer");
}
this.isSpammer = isSpammer;
}
}