package models.database.HotDatabase;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import models.Answer;
import models.Question;
import models.SearchFilter;
import models.Tag;
import models.User;
import models.database.IQuestionDatabase;
import models.database.ITagDatabase;
import models.helpers.ICleanup;
import models.helpers.Mapper;
public class HotQuestionDatabase implements IQuestionDatabase,
ICleanup<Question> {
private final HashMap<Integer, Question> questions = new HashMap<Integer, Question>();
private final ITagDatabase tagDB;
/**
* Creates a new in-memory database for managing questions.
*
* @param tagDB
* the database which is to store the tags associated with the
* questions stored in the newly created database
*/
public HotQuestionDatabase(ITagDatabase tagDB) {
this.tagDB = tagDB;
}
/**
* Searches through all questions, answers and usernames for the given
* search terms.
*
* @param term
* the list of strings that must appear somewhere in a question
* or its answers. Only letters and numbers are retained. In
* order to search for questions having a specific tag, use the
* "tag:<em>tagname</em>" syntax.
* @return a list of all the questions that match <em>all</em> the search
* criteria.
*
*/
public List<Question> searchFor(String term) {
Set<String> terms = new HashSet();
Set<Tag> tags = new HashSet<Tag>();
for (String s : term.toLowerCase().split("\\s+")) {
if (s.startsWith("tag:") && s.length() > 4) {
// search for tag only
terms.add(s);
tags.add(this.tagDB.get(s.substring(4)));
} else {
// search for this term anywhere, so ignore all non-alphanumeric
// characters
terms.addAll(Arrays.asList(s.split("\\W+")));
tags.add(this.tagDB.get(s));
}
}
return Mapper.sort(this.questions.values(),
new SearchFilter(terms, tags));
}
/**
* Get the <code>Question</code> with the given id.
*
* @param id
* @return a <code>Question</code> or null if the given id doesn't exist.
*/
public Question get(int id) {
return this.questions.get(id);
}
/**
* Get a <@link Collection} of all <code>Questions</code>.
*
* @return all <code>Questions</code>
*/
public List<Question> all() {
List<Question> list = new ArrayList<Question>(this.questions.values());
Collections.sort(list);
return list;
}
public Question add(User owner, String content) {
Question question = new Question(owner, content, this.tagDB, this);
this.questions.put(question.id(), question);
return question;
}
public int count() {
return this.questions.size();
}
public int countBestRatedAnswers() {
int count = 0;
for (Question q : this.questions.values())
if (q.hasBestAnswer()) {
count++;
}
return count;
}
public int countAllAnswers() {
int count = 0;
for (Question q : this.questions.values()) {
count += q.answers().size();
}
return count;
}
public int countHighRatedAnswers() {
int count = 0;
for (Question q : this.questions.values()) {
for (Answer a : q.answers()) {
if (a.isHighRated()) {
count += 1;
}
}
}
return count;
}
public List<Question> findSimilar(Question q) {
List<Question> result = Mapper.sort(this.questions.values(),
new SearchFilter(null, new HashSet<Tag>(q.getTags())));
result.remove(q); // don't find the question itself!
return result;
}
public List<Question> suggestQuestions(User user) {
List<Question> suggestedQuestions = new ArrayList<Question>();
List<Question> sortedAnsweredQuestions = user
.getSortedAnsweredQuestions();
/*
* Don't list questions that have many answers or already have a best
* answer or are just plain old (older than 120 days). The user should
* not be the owner of the suggested question. Remove duplicates.
*/
for (Question q : sortedAnsweredQuestions) {
for (Question similarQ : this.findSimilar(q)) {
if (!suggestedQuestions.contains(similarQ)
&& !sortedAnsweredQuestions.contains(similarQ)
&& similarQ.owner() != user
&& similarQ.getAgeInDays() <= 120
&& similarQ.answers().size() < 10
&& !similarQ.hasBestAnswer()) {
suggestedQuestions.add(similarQ);
}
}
}
if (suggestedQuestions.size() > 6)
return suggestedQuestions.subList(0, 6);
return suggestedQuestions;
}
/**
* Having given a best answer gives the equivalent of an additional
* BEST_ANSWER_BONUS votes.
*/
private final int BEST_ANSWER_BONUS = 5;
/**
* Collects for all tags the vote counts for all the users that have
* answered a question labeled with that tag.
*
* @return a statistics map allowing to either determine the experts for a
* given tag or the tags this user is an expert for
*/
public Map<Tag, Map<User, Integer>> collectExpertiseStatistics() {
Map<Tag, Map<User, Integer>> stats = new HashMap();
// only check each question (and answer) once
for (Question question : this.all()) {
List<Tag> tags = question.getTags();
// skip untagged questions
if (tags.isEmpty())
continue;
for (Answer answer : question.answers()) {
User user = answer.owner();
// don't consider answers by the question's author and by
// anonymous users
if (user == question.owner() || user == null)
continue;
for (Tag tag : tags) {
// get the statistics for a given tag (initialize them at
// the first pass)
Map<User, Integer> tagStats = stats.get(tag);
if (tagStats == null) {
tagStats = new HashMap();
stats.put(tag, tagStats);
}
// update the vote count for this answer's owner
Integer count = tagStats.get(user);
if (count == null)
count = 0;
// a best answer count as 5 additional up-votes
if (answer.isBestAnswer())
count += BEST_ANSWER_BONUS;
tagStats.put(user, count + answer.rating());
}
}
}
return stats;
}
/**
* Having less than MINIMAL_EXPERTISE_THRESHOLD votes on a topic prevents a
* user from being an expert.
*/
private final int MINIMAL_EXPERTISE_THRESHOLD = 2;
/**
* What percentage of most proficient users are considered experts on a
* topic.
*/
private final int EXPERTISE_PERCENTILE = 20;
public List<Tag> getExpertise(User user) {
Map<Tag, Map<User, Integer>> stats = this.collectExpertiseStatistics();
List<Tag> expertise = new ArrayList();
for (Tag tag : stats.keySet()) {
// ignore tags this user knows nothing about
if (!stats.get(tag).containsKey(user)) {
continue;
}
// ignore tags this user knows hardly anything about
if (stats.get(tag).get(user) < this.MINIMAL_EXPERTISE_THRESHOLD) {
continue;
}
Map<User, Integer> tagStats = stats.get(tag);
List<User> experts = Mapper.sortByValue(tagStats);
int threshold = (100 - this.EXPERTISE_PERCENTILE) * experts.size()
/ 100;
if (tagStats.get(user) >= tagStats.get(experts.get(threshold))) {
expertise.add(tag);
}
}
return expertise;
}
public void clear() {
this.questions.clear();
}
public List<Question> getWatchList(User user) {
List<Question> watchList = new ArrayList();
for (Question question : this.questions.values()) {
if (question.hasObserver(user)) {
watchList.add(question);
}
}
return watchList;
}
/**
* Remove all references to the <code>Question</code> when it's being
* deleted (Callback method).
*
* @see models.helpers.ICleanup#cleanUp(java.lang.Object)
*/
public void cleanUp(Question question) {
this.questions.remove(question.id());
}
}