package models;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import models.database.ITagDatabase;
import models.helpers.ICleanup;
import models.helpers.IObservable;
import models.helpers.IObserver;
/**
* A {@link Entry} containing a question as <code>content</code>, and
* {@link Answer}s (comments and votes are tracked by the superclass).
*
* @author Simon Marti
* @author Mirco Kocher
*/
public class Question extends Entry implements IObservable {
private final HashMap<Integer, Answer> answers;
private boolean isLocked = false;
private Answer bestAnswer;
private Date settingOfBestAnswer;
private final ArrayList<Tag> tags = new ArrayList<Tag>();
private final ITagDatabase tagDB;
private final ICleanup<Question> cleaner;
protected HashSet<IObserver> observers;
/**
* Create a Question, store it in the database and have its owner notified
* about all answers (opt-out).
*
* @param owner
* the {@link User} who posted the <code>Question</code>
* @param content
* the question
* @param tagDB
* an optional tag database in which to store tags associated
* with this question
* @param cleaner
* an optional clean-up object that wants to be notified when
* this question is no longer needed
*/
public Question(User owner, String content, ITagDatabase tagDB,
ICleanup<Question> cleaner) {
super(owner, content);
this.answers = new HashMap<Integer, Answer>();
this.observers = new HashSet<IObserver>();
this.tagDB = tagDB;
this.cleaner = cleaner;
// all users watch their own questions by default
if (owner != null)
owner.startObserving(this);
}
/**
* Constructor for questions not registered in any kind of database (for
* testing only).
*
* @param owner
* the {@link User} who posted the <code>Question</code>
* @param content
* the question
*/
public Question(User owner, String content) {
this(owner, content, null, null);
}
/**
* Unregisters all {@link Answer}s, {@link Tag}s and itself.
*/
@Override
public void delete() {
for (Answer answer : new ArrayList<Answer>(this.answers.values())) {
answer.delete();
}
this.observers.clear();
setTagString("");
if (this.cleaner != null) {
this.cleaner.cleanUp(this);
}
super.delete();
}
/**
* This is a callback method for removing all references to an
* <code>Item</code> such as an answer, comment, vote, etc. kept by this
* <code>Question</code> when the <code>Item</code> is being deleted.
*
* @see models.Entry#cleanUp(models.Item)
*/
@Override
public void cleanUp(Item item) {
if (item instanceof Answer) {
this.answers.remove(item.id());
}
super.cleanUp(item);
}
/**
* Factory method that creates a new {@link Answer} to this
* <code>Question</code>, stores the answer in the question's list of
* answers and notifies all the users observing this question about the new
* answer.
*
* @param user
* the {@link User} posting the {@link Answer}
* @param content
* the answer (Markdown/HTML)
* @return an {@link Answer}
*/
public Answer answer(User user, String content) {
Answer answer = new Answer(user, this, content);
this.answers.put(answer.id(), answer);
// make users aware of this new answer
this.notifyObservers(answer);
return answer;
}
/**
* Checks if a {@link Answer} belongs to a <code>Question</code>.
*
* @param answer
* the {@link Answer} to check
* @return true if the {@link Answer} belongs to the <code>Question</code>
*/
public boolean hasAnswer(Answer answer) {
return this.answers.containsValue(answer);
}
/**
* Get all {@link Answer}s to a <code>Question</code> sorted by their rating
* (best rated ones first).
*
* @return {@link List} of {@link Answers}
*/
public List<Answer> answers() {
List<Answer> list = new ArrayList<Answer>(this.answers.values());
Collections.sort(list);
return list;
}
/**
* Get a specific {@link Answer} to a <code>Question</code>.
*
* @param id
* of the <code>Answer</code>
* @return {@link Answer} or null
*/
public Answer getAnswer(int id) {
return this.answers.get(id);
}
/**
* How many milliseconds may pass between a best answer has been chosen and
* the point where that decision becomes permanent.
*/
private final int BEST_ANSWER_DECISION_TIME_IN_MS = 30 * 60 * 1000;
/**
* Checks if for this answer a best answer can still be chosen. This is the
* case when either this question doesn't have a best answer yet or when the
* current best answer has been chosen less than 30 minutes ago. After that
* 30 minute window, the decision becomes permanent and can no longer be
* changed.
*
* @return true, if is best answer settable
*/
public boolean isBestAnswerSettable() {
long thirtyMinutesAgo = SysInfo.now().getTime()
- this.BEST_ANSWER_DECISION_TIME_IN_MS;
return this.settingOfBestAnswer == null
|| thirtyMinutesAgo <= this.settingOfBestAnswer.getTime();
}
/**
* Sets the best answer. This answer can not be changed after 30min. This
* Method enforces this and fails if it can not be set.
*
* @param bestAnswer
* the answer the user chose to be the best for this question.
* @return true if setting of best answer was allowed.
*/
public boolean setBestAnswer(Answer bestAnswer) {
if (!isBestAnswerSettable()) {
return false;
}
this.bestAnswer = bestAnswer;
this.settingOfBestAnswer = SysInfo.now();
return true;
}
/**
* Checks whether this question has a best answer set. The returned answer
* is guaranteed to remain the best answer after 30 minutes have past (and
* the answerer doesn't delete it).
*
* @return true, if a best answer has been set already.
*/
public boolean hasBestAnswer() {
return this.bestAnswer != null;
}
/**
* Gets the best <code>Answer</code> to this <code>Question</code>. The
* returned answer has <code>answer.isBestAnswer() == true</code>.
*
* @return the answer that's currently best.
*/
public Answer getBestAnswer() {
return this.bestAnswer;
}
/**
* Returns whether a <code>Question</code> is locked or not. Locked
* questions cannot be answered or commented.
*
* @return whether the <code>Question</code> is locked or not
*/
public boolean isLocked() {
return this.isLocked;
}
/**
* Sets a <code>Question</code> to the locked status. Locked questions
* cannot be answered or commented.
*/
public void lock() {
this.isLocked = true;
}
/**
* Unlocks a <code>Question</code> so that it can be answered or commented
* again.
*/
public void unlock() {
this.isLocked = false;
}
/**
* Changes this question's tags to the passed in list, removing all the tags
* that aren't in the passed in tag list. Tag names must be separated by
* either commas or whitespace. Tags are converted to lowercase before being
* added and overlong tags are truncated to 32 characters.
*
* @param tags
* a comma- or whitespace-separated list of tags to be associated
* with this question
*/
public void setTagString(String tags) {
for (Tag tag : this.tags) {
tag.removeQuestion(this);
}
this.tags.clear();
if (tags == null || tags.equals(""))
return;
String bits[] = tags.split("[\\s,]+");
for (String bit : bits) {
// make the tag conform to Tag.tagRegex
bit = bit.toLowerCase();
if (bit.length() > 32) {
bit = bit.substring(0, 32);
}
Tag tag = this.tagDB.get(bit);
if (tag != null && !this.tags.contains(tag)) {
this.tags.add(tag);
tag.addQuestion(this);
}
}
Collections.sort(this.tags);
}
/*
* Get a List of all tags for a <code>Question</code>.
*
* @return List of tags
*/
public List<Tag> getTags() {
return (List<Tag>) this.tags.clone();
}
/**
* @see models.helpers.IObservable#addObserver(models.IObserver)
*/
public void addObserver(IObserver o) {
if (o == null)
throw new IllegalArgumentException();
this.observers.add(o);
}
/**
* @see models.helpers.IObservable#hasObserver(models.IObserver)
*/
public boolean hasObserver(IObserver o) {
return this.observers.contains(o);
}
/**
* @see models.helpers.IObservable#removeObserver(models.IObserver)
*/
public void removeObserver(IObserver o) {
this.observers.remove(o);
}
/**
* @see models.helpers.IObservable#notifyObservers(java.lang.Object)
*/
public void notifyObservers(Object arg) {
for (IObserver o : this.observers) {
o.observe(this, arg);
}
}
/**
* Calculates the age of this question in days.
*
* @return this question's age in days
*/
public long getAgeInDays() {
return (SysInfo.now().getTime() - this.timestamp().getTime())
/ (1000 * 60 * 60 * 24);
}
}