Newer
Older
* This file is part of ARSnova Backend.
* Copyright (C) 2012-2015 The ARSnova Team
* ARSnova Backend 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.
*
* ARSnova Backend 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 <http://www.gnu.org/licenses/>.
*/
package de.thm.arsnova.dao;
import java.io.IOException;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.ConcurrentLinkedQueue;
import net.sf.ezmorph.Morpher;
import net.sf.ezmorph.MorpherRegistry;
import net.sf.ezmorph.bean.BeanMorpher;
import net.sf.json.JSONArray;
import net.sf.json.JSONObject;
import net.sf.json.util.JSONUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.Caching;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;
import com.fourspaces.couchdb.Database;
import com.fourspaces.couchdb.Document;
import com.fourspaces.couchdb.View;
import com.fourspaces.couchdb.ViewResults;
import de.thm.arsnova.connector.model.Course;
import de.thm.arsnova.domain.CourseScore;
import de.thm.arsnova.entities.DbUser;
import de.thm.arsnova.entities.InterposedQuestion;
import de.thm.arsnova.entities.InterposedReadingCount;
Paul-Christian Volkmer
committed
import de.thm.arsnova.entities.LoggedIn;
import de.thm.arsnova.entities.PossibleAnswer;
import de.thm.arsnova.entities.Question;
import de.thm.arsnova.entities.Session;
import de.thm.arsnova.entities.SessionInfo;
import de.thm.arsnova.entities.User;
import de.thm.arsnova.entities.VisitedSession;
import de.thm.arsnova.entities.transport.ImportExportSession;
import de.thm.arsnova.entities.transport.ImportExportSession.ImportExportQuestion;
Paul-Christian Volkmer
committed
import de.thm.arsnova.exceptions.NotFoundException;
import de.thm.arsnova.services.ISessionService;
public class CouchDBDao implements IDatabaseDao {
private String databaseHost;
private int databasePort;
private String databaseName;
private Database database;
private Queue<AbstractMap.SimpleEntry<Document, Answer>> answerQueue = new ConcurrentLinkedQueue<AbstractMap.SimpleEntry<Document, Answer>>();
public static final Logger LOGGER = LoggerFactory.getLogger(CouchDBDao.class);
@Value("${couchdb.host}")
public final void setDatabaseHost(final String newDatabaseHost) {
Paul-Christian Volkmer
committed
databaseHost = newDatabaseHost;
@Value("${couchdb.port}")
public final void setDatabasePort(final String newDatabasePort) {
Paul-Christian Volkmer
committed
databasePort = Integer.parseInt(newDatabasePort);
@Value("${couchdb.name}")
public final void setDatabaseName(final String newDatabaseName) {
Paul-Christian Volkmer
committed
databaseName = newDatabaseName;
public final void setSessionService(final ISessionService service) {
Paul-Christian Volkmer
committed
sessionService = service;
/**
* @deprecated Use getSessionFromKeyword instead. The database should not throw HTTP exceptions.
* Additionally, the caching mechanism won't work here because it is calling inside the same class.
*/
@Deprecated
public final Session getSession(final String keyword) {
Paul-Christian Volkmer
committed
final Session result = getSessionFromKeyword(keyword);
if (result != null) {
Paul-Christian Volkmer
committed
throw new NotFoundException();
public final List<Session> getMySessions(final User user) {
Paul-Christian Volkmer
committed
final NovaView view = new NovaView("session/by_creator");
view.setStartKeyArray(user.getUsername());
view.setEndKeyArray(user.getUsername(), "{}");
Paul-Christian Volkmer
committed
final ViewResults sessions = getDatabase().view(view);
Paul-Christian Volkmer
committed
final List<Session> result = new ArrayList<Session>();
for (final Document d : sessions.getResults()) {
final Session session = (Session) JSONObject.toBean(
d.getJSONObject().getJSONObject("value"),
Session.class
session.setCreator(d.getJSONObject().getJSONArray("key").getString(0));
session.setName(d.getJSONObject().getJSONArray("key").getString(1));
session.set_id(d.getId());
result.add(session);
public final List<Session> getPublicPoolSessions() {
final NovaView view = new NovaView("session/public_pool_by_subject");
final ViewResults sessions = getDatabase().view(view);
final List<Session> result = new ArrayList<Session>();
for (final Document d : sessions.getResults()) {
final Session session = (Session) JSONObject.toBean(
d.getJSONObject().getJSONObject("value"),
Session.class
);
Felix Schmidt
committed
session.set_id(d.getId());
result.add(session);
return result;
Felix Schmidt
committed
@Override
public final List<SessionInfo> getPublicPoolSessionsInfo() {
final List<Session> sessions = this.getPublicPoolSessions();
return getInfosForSessions(sessions);
}
@Override
public final List<Session> getMyPublicPoolSessions(final User user) {
final NovaView view = new NovaView("session/public_pool_by_creator");
view.setStartKeyArray(user.getUsername());
view.setEndKeyArray(user.getUsername(), "{}");
final ViewResults sessions = getDatabase().view(view);
final List<Session> result = new ArrayList<Session>();
for (final Document d : sessions.getResults()) {
final Session session = (Session) JSONObject.toBean(
d.getJSONObject().getJSONObject("value"),
Session.class
);
session.setCreator(d.getJSONObject().getJSONArray("key").getString(0));
session.setName(d.getJSONObject().getJSONArray("key").getString(1));
session.set_id(d.getId());
result.add(session);
}
return result;
}
@Override
public final List<SessionInfo> getMyPublicPoolSessionsInfo(final User user) {
final List<Session> sessions = this.getMyPublicPoolSessions(user);
return getInfosForSessions(sessions);
}
@Override
public final List<SessionInfo> getMySessionsInfo(final User user) {
final List<Session> sessions = this.getMySessions(user);
return getInfosForSessions(sessions);
}
private List<SessionInfo> getInfosForSessions(final List<Session> sessions) {
final ExtendedView questionCountView = new ExtendedView("skill_question/count_by_session");
final ExtendedView answerCountView = new ExtendedView("skill_question/count_answers_by_session");
final ExtendedView interposedCountView = new ExtendedView("interposed_question/count_by_session");
final ExtendedView unredInterposedCountView = new ExtendedView("interposed_question/count_by_session_reading");
interposedCountView.setSessionIdKeys(sessions);
interposedCountView.setGroup(true);
questionCountView.setSessionIdKeys(sessions);
questionCountView.setGroup(true);
answerCountView.setSessionIdKeys(sessions);
answerCountView.setGroup(true);
List<String> unredInterposedQueryKeys = new ArrayList<String>();
for (Session s : sessions) {
unredInterposedQueryKeys.add("[\"" + s.get_id() + "\",\"unread\"]");
unredInterposedCountView.setKeys(unredInterposedQueryKeys);
unredInterposedCountView.setGroup(true);
return getSessionInfoData(sessions, questionCountView, answerCountView, interposedCountView, unredInterposedCountView);
}
private List<SessionInfo> getInfosForVisitedSessions(final List<Session> sessions, final User user) {
final ExtendedView answeredQuestionsView = new ExtendedView("answer/by_user");
final ExtendedView questionIdsView = new ExtendedView("skill_question/by_session_only_id_for_all");
questionIdsView.setSessionIdKeys(sessions);
List<String> answeredQuestionQueryKeys = new ArrayList<String>();
for (Session s : sessions) {
answeredQuestionQueryKeys.add("[\"" + user.getUsername() + "\",\"" + s.get_id() + "\"]");
}
answeredQuestionsView.setKeys(answeredQuestionQueryKeys);
return getVisitedSessionInfoData(sessions, answeredQuestionsView, questionIdsView);
private List<SessionInfo> getVisitedSessionInfoData(List<Session> sessions,
ExtendedView answeredQuestionsView, ExtendedView questionIdsView) {
final Map<String, Set<String>> answeredQuestionsMap = new HashMap<String, Set<String>>();
final Map<String, Set<String>> questionIdMap = new HashMap<String, Set<String>>();
final ViewResults answeredQuestionsViewResults = getDatabase().view(answeredQuestionsView);
final ViewResults questionIdsViewResults = getDatabase().view(questionIdsView);
// Maps a session ID to a set of question IDs of answered questions of that session
for (final Document d : answeredQuestionsViewResults.getResults()) {
final String sessionId = d.getJSONArray("key").getString(1);
final String questionId = d.getString("value");
Set<String> questionIdsInSession = answeredQuestionsMap.get(sessionId);
if (questionIdsInSession == null) {
questionIdsInSession = new HashSet<String>();
questionIdsInSession.add(questionId);
answeredQuestionsMap.put(sessionId, questionIdsInSession);
}
// Maps a session ID to a set of question IDs of that session
for (final Document d : questionIdsViewResults.getResults()) {
final String sessionId = d.getString("key");
final String questionId = d.getId();
Set<String> questionIdsInSession = questionIdMap.get(sessionId);
if (questionIdsInSession == null) {
questionIdsInSession = new HashSet<String>();
questionIdsInSession.add(questionId);
questionIdMap.put(sessionId, questionIdsInSession);
}
// For each session, count the question IDs that are not yet answered
Map<String, Integer> unansweredQuestionsCountMap = new HashMap<String, Integer>();
for (final Session s : sessions) {
if (!questionIdMap.containsKey(s.get_id())) {
continue;
}
// Note: create a copy of the first set so that we don't modify the contents in the original set
Set<String> questionIdsInSession = new HashSet<String>(questionIdMap.get(s.get_id()));
Set<String> answeredQuestionIdsInSession = answeredQuestionsMap.get(s.get_id());
if (answeredQuestionIdsInSession == null) {
answeredQuestionIdsInSession = new HashSet<String>();
}
questionIdsInSession.removeAll(answeredQuestionIdsInSession);
unansweredQuestionsCountMap.put(s.get_id(), questionIdsInSession.size());
}
List<SessionInfo> sessionInfos = new ArrayList<SessionInfo>();
for (Session session : sessions) {
int numUnanswered = 0;
if (unansweredQuestionsCountMap.containsKey(session.get_id())) {
numUnanswered = unansweredQuestionsCountMap.get(session.get_id());
}
SessionInfo info = new SessionInfo(session);
info.setNumUnanswered(numUnanswered);
sessionInfos.add(info);
}
return sessionInfos;
}
private List<SessionInfo> getSessionInfoData(final List<Session> sessions,
final ExtendedView questionCountView,
final ExtendedView answerCountView,
final ExtendedView interposedCountView,
final ExtendedView unredInterposedCountView) {
final ViewResults questionCountViewResults = getDatabase().view(questionCountView);
final ViewResults answerCountViewResults = getDatabase().view(answerCountView);
final ViewResults interposedCountViewResults = getDatabase().view(interposedCountView);
final ViewResults unredInterposedCountViewResults = getDatabase().view(unredInterposedCountView);
Map<String, Integer> questionCountMap = new HashMap<String, Integer>();
for (final Document d : questionCountViewResults.getResults()) {
questionCountMap.put(d.getString("key"), d.getInt("value"));
}
Map<String, Integer> answerCountMap = new HashMap<String, Integer>();
for (final Document d : answerCountViewResults.getResults()) {
answerCountMap.put(d.getString("key"), d.getInt("value"));
}
Map<String, Integer> interposedCountMap = new HashMap<String, Integer>();
for (final Document d : interposedCountViewResults.getResults()) {
interposedCountMap.put(d.getString("key"), d.getInt("value"));
Map<String, Integer> unredInterposedCountMap = new HashMap<String, Integer>();
for (final Document d : unredInterposedCountViewResults.getResults()) {
unredInterposedCountMap.put(d.getJSONArray("key").getString(0), d.getInt("value"));
List<SessionInfo> sessionInfos = new ArrayList<SessionInfo>();
for (Session session : sessions) {
int numQuestions = 0;
int numAnswers = 0;
int numInterposed = 0;
int numUnredInterposed = 0;
if (questionCountMap.containsKey(session.get_id())) {
numQuestions = questionCountMap.get(session.get_id());
}
if (answerCountMap.containsKey(session.get_id())) {
numAnswers = answerCountMap.get(session.get_id());
}
if (interposedCountMap.containsKey(session.get_id())) {
numInterposed = interposedCountMap.get(session.get_id());
}
if (unredInterposedCountMap.containsKey(session.get_id())) {
numUnredInterposed = unredInterposedCountMap.get(session.get_id());
}
SessionInfo info = new SessionInfo(session);
info.setNumQuestions(numQuestions);
info.setNumAnswers(numAnswers);
info.setNumInterposed(numInterposed);
info.setNumUnredInterposed(numUnredInterposed);
sessionInfos.add(info);
}
return sessionInfos;
}
/**
* @deprecated The decision to load data depending on the user should be made by a service class, not this
* database class. Please use getSkillQuestionsForUsers or getSkillQuestionsForTeachers as this will enable
* caching.
*/
@Deprecated
@Override
Paul-Christian Volkmer
committed
public final List<Question> getSkillQuestions(final User user, final Session session) {
String viewName;
if (session.getCreator().equals(user.getUsername())) {
viewName = "skill_question/by_session_sorted_by_subject_and_text";
} else {
viewName = "skill_question/by_session_for_all_full";
}
return getQuestions(new NovaView(viewName), session);
}
@Cacheable("skillquestions")
@Override
public final List<Question> getSkillQuestionsForUsers(final Session session) {
String viewName = "skill_question/by_session_for_all_full";
return getQuestions(new NovaView(viewName), session);
}
@Cacheable("skillquestions")
@Override
public final List<Question> getSkillQuestionsForTeachers(final Session session) {
String viewName = "skill_question/by_session_sorted_by_subject_and_text";
return getQuestions(new NovaView(viewName), session);
}
public final int getSkillQuestionCount(final Session session) {
return getQuestionCount(new NovaView("skill_question/count_by_session"), session);
@Cacheable("sessions")
public final Session getSessionFromKeyword(final String keyword) {
Paul-Christian Volkmer
committed
final NovaView view = new NovaView("session/by_keyword");
view.setKey(keyword);
Paul-Christian Volkmer
committed
final ViewResults results = getDatabase().view(view);
if (results.getJSONArray("rows").optJSONObject(0) == null) {
return (Session) JSONObject.toBean(
results.getJSONArray("rows").optJSONObject(0).optJSONObject("value"),
Session.class
@Cacheable("sessions")
public final Session getSessionFromId(final String sessionId) {
Paul-Christian Volkmer
committed
final NovaView view = new NovaView("session/by_id");
view.setKey(sessionId);
Paul-Christian Volkmer
committed
final ViewResults sessions = getDatabase().view(view);
if (sessions.getJSONArray("rows").optJSONObject(0) == null) {
return (Session) JSONObject.toBean(
sessions.getJSONArray("rows").optJSONObject(0).optJSONObject("value"),
public final Session saveSession(final User user, final Session session) {
Paul-Christian Volkmer
committed
final Document sessionDocument = new Document();
sessionDocument.put("name", session.getName());
sessionDocument.put("shortName", session.getShortName());
sessionDocument.put("keyword", sessionService.generateKeyword());
sessionDocument.put("creator", user.getUsername());
sessionDocument.put("active", true);
sessionDocument.put("courseType", session.getCourseType());
sessionDocument.put("courseId", session.getCourseId());
sessionDocument.put("creationTime", session.getCreationTime());
sessionDocument.put("learningProgressType", session.getLearningProgressType());
sessionDocument.put("ppAuthorName", session.getPpAuthorName());
sessionDocument.put("ppAuthorMail", session.getPpAuthorMail());
sessionDocument.put("ppUniversity", session.getPpUniversity());
sessionDocument.put("ppLogo", session.getPpLogo());
sessionDocument.put("ppSubject", session.getPpSubject());
sessionDocument.put("ppLicense", session.getPpLicense());
sessionDocument.put("ppDescription", session.getPpDescription());
sessionDocument.put("ppFaculty", session.getPpFaculty());
sessionDocument.put("ppLevel", session.getPpLevel());
Felix Schmidt
committed
sessionDocument.put("sessionType", session.getSessionType());
try {
database.saveDocument(sessionDocument);
Paul-Christian Volkmer
committed
} catch (final IOException e) {
// session caching is done by loading the created session
Paul-Christian Volkmer
committed
return getSession(sessionDocument.getString("keyword"));
@Transactional(isolation = Isolation.READ_COMMITTED)
public final boolean sessionKeyAvailable(final String keyword) {
Paul-Christian Volkmer
committed
final View view = new View("session/by_keyword");
final ViewResults results = getDatabase().view(view);
private String getSessionKeyword(final String internalSessionId) throws IOException {
Paul-Christian Volkmer
committed
final Document document = getDatabase().getDocument(internalSessionId);
if (document.has("keyword")) {
return (String) document.get("keyword");
LOGGER.error("No session found for internal id: {}", internalSessionId);
return null;
}
private Database getDatabase() {
if (database == null) {
try {
Paul-Christian Volkmer
committed
final com.fourspaces.couchdb.Session session = new com.fourspaces.couchdb.Session(
databaseHost,
databasePort
database = session.getDatabase(databaseName);
Paul-Christian Volkmer
committed
} catch (final Exception e) {
LOGGER.error(
"Cannot connect to CouchDB database '" + databaseName
+ "' on host '" + databaseHost
+ "' using port " + databasePort
@CachePut(value = "questions", key="#question")
public final Question saveQuestion(final Session session, final Question question) {
Paul-Christian Volkmer
committed
final Document q = toQuestionDocument(session, question);
try {
database.saveDocument(q);
question.set_id(q.getId());
question.set_rev(q.getRev());
return question;
Paul-Christian Volkmer
committed
} catch (final IOException e) {
LOGGER.error("Could not save question {}", question);
}
return null;
}
private Document toQuestionDocument(final Session session, final Question question) {
Paul-Christian Volkmer
committed
final Document q = new Document();
q.put("type", "skill_question");
q.put("questionType", question.getQuestionType());
q.put("questionVariant", question.getQuestionVariant());
q.put("sessionId", session.get_id());
q.put("subject", question.getSubject());
q.put("text", question.getText());
q.put("active", question.isActive());
q.put("number", 0); // TODO: This number is now unused. A clean up is necessary.
q.put("releasedFor", question.getReleasedFor());
q.put("possibleAnswers", question.getPossibleAnswers());
q.put("noCorrect", question.isNoCorrect());
q.put("piRound", question.getPiRound());
q.put("showStatistic", question.isShowStatistic());
q.put("showAnswer", question.isShowAnswer());
q.put("abstention", question.isAbstention());
q.put("fcImage", question.getFcImage());
q.put("gridSize", question.getGridSize());
q.put("offsetX", question.getOffsetX());
q.put("offsetY", question.getOffsetY());
q.put("zoomLvl", question.getZoomLvl());
q.put("gridOffsetX", question.getGridOffsetX());
q.put("gridOffsetY", question.getGridOffsetY());
q.put("gridZoomLvl", question.getGridZoomLvl());
q.put("gridSizeX", question.getGridSizeX());
q.put("gridSizeY", question.getGridSizeY());
q.put("gridIsHidden", question.getGridIsHidden());
q.put("imgRotation", question.getImgRotation());
q.put("toggleFieldsLeft", question.getToggleFieldsLeft());
q.put("numClickableFields", question.getNumClickableFields());
Felix Schmidt
committed
q.put("thresholdCorrectAnswers", question.getThresholdCorrectAnswers());
q.put("cvIsColored", question.getCvIsColored());
q.put("gridLineColor", question.getGridLineColor());
q.put("numberOfDots", question.getNumberOfDots());
Felix Schmidt
committed
q.put("gridType", question.getGridType());
q.put("scaleFactor", question.getScaleFactor());
q.put("gridScaleFactor", question.getGridScaleFactor());
return q;
public final Question updateQuestion(final Question question) {
Paul-Christian Volkmer
committed
final Document q = database.getDocument(question.get_id());
q.put("subject", question.getSubject());
q.put("text", question.getText());
q.put("active", question.isActive());
q.put("releasedFor", question.getReleasedFor());
q.put("possibleAnswers", question.getPossibleAnswers());
q.put("noCorrect", question.isNoCorrect());
q.put("piRound", question.getPiRound());
q.put("showStatistic", question.isShowStatistic());
q.put("showAnswer", question.isShowAnswer());
q.put("abstention", question.isAbstention());
q.put("fcImage", question.getFcImage());
q.put("gridSize", question.getGridSize());
q.put("offsetX", question.getOffsetX());
q.put("offsetY", question.getOffsetY());
q.put("zoomLvl", question.getZoomLvl());
q.put("gridOffsetX", question.getGridOffsetX());
q.put("gridOffsetY", question.getGridOffsetY());
q.put("gridZoomLvl", question.getGridZoomLvl());
q.put("gridSizeX", question.getGridSizeX());
q.put("gridSizeY", question.getGridSizeY());
q.put("gridIsHidden", question.getGridIsHidden());
q.put("imgRotation", question.getImgRotation());
q.put("toggleFieldsLeft", question.getToggleFieldsLeft());
q.put("numClickableFields", question.getNumClickableFields());
Felix Schmidt
committed
q.put("thresholdCorrectAnswers", question.getThresholdCorrectAnswers());
q.put("cvIsColored", question.getCvIsColored());
q.put("gridLineColor", question.getGridLineColor());
q.put("numberOfDots", question.getNumberOfDots());
Felix Schmidt
committed
q.put("gridType", question.getGridType());
q.put("scaleFactor", question.getScaleFactor());
q.put("gridScaleFactor", question.getGridScaleFactor());
Paul-Christian Volkmer
committed
database.saveDocument(q);
question.set_rev(q.getRev());
return question;
Paul-Christian Volkmer
committed
} catch (final IOException e) {
LOGGER.error("Could not update question {}", question);
}
return null;
public final InterposedQuestion saveQuestion(final Session session, final InterposedQuestion question, User user) {
Paul-Christian Volkmer
committed
final Document q = new Document();
q.put("type", "interposed_question");
q.put("sessionId", session.get_id());
q.put("subject", question.getSubject());
q.put("text", question.getText());
Felix Schmidt
committed
if (question.getTimestamp() != 0) {
q.put("timestamp", question.getTimestamp());
} else {
q.put("timestamp", System.currentTimeMillis());
}
q.put("creator", user.getUsername());
question.set_id(q.getId());
question.set_rev(q.getRev());
return question;
Paul-Christian Volkmer
committed
} catch (final IOException e) {
LOGGER.error("Could not save interposed question {}", question);
}
return null;
@Override
public final Question getQuestion(final String id) {
Paul-Christian Volkmer
committed
final NovaView view = new NovaView("skill_question/by_id");
Paul-Christian Volkmer
committed
final ViewResults results = getDatabase().view(view);
if (results.getJSONArray("rows").optJSONObject(0) == null) {
return null;
}
Paul-Christian Volkmer
committed
final Question q = (Question) JSONObject.toBean(
results.getJSONArray("rows").optJSONObject(0).optJSONObject("value"),
Question.class
final JSONArray possibleAnswers = results.getJSONArray("rows").optJSONObject(0).optJSONObject("value")
@SuppressWarnings("unchecked")
final Collection<PossibleAnswer> answers = JSONArray.toCollection(possibleAnswers, PossibleAnswer.class);
q.setPossibleAnswers(new ArrayList<PossibleAnswer>(answers));
Paul-Christian Volkmer
committed
q.setSessionKeyword(getSessionKeyword(q.getSessionId()));
Paul-Christian Volkmer
committed
} catch (final IOException e) {
LOGGER.error("Could not get question with id {}", id);
}
return null;
}
public final LoggedIn registerAsOnlineUser(final User user, final Session session) {
Paul-Christian Volkmer
committed
final NovaView view = new NovaView("logged_in/all");
view.setKey(user.getUsername());
Paul-Christian Volkmer
committed
final ViewResults results = getDatabase().view(view);
LoggedIn loggedIn = new LoggedIn();
if (results.getJSONArray("rows").optJSONObject(0) != null) {
Paul-Christian Volkmer
committed
final JSONObject json = results.getJSONArray("rows").optJSONObject(0).optJSONObject("value");
loggedIn = (LoggedIn) JSONObject.toBean(json, LoggedIn.class);
Paul-Christian Volkmer
committed
final JSONArray vs = json.optJSONArray("visitedSessions");
Paul-Christian Volkmer
committed
final Collection<VisitedSession> visitedSessions = JSONArray.toCollection(vs, VisitedSession.class);
loggedIn.setVisitedSessions(new ArrayList<VisitedSession>(visitedSessions));
}
/* Do not clutter CouchDB. Only update once every 3 hours per session. */
if (loggedIn.getSessionId().equals(session.get_id()) && loggedIn.getTimestamp() > System.currentTimeMillis() - 3 * 3600000) {
return loggedIn;
}
loggedIn.setUser(user.getUsername());
loggedIn.setSessionId(session.get_id());
loggedIn.addVisitedSession(session);
Christoph Thelen
committed
loggedIn.updateTimestamp();
Paul-Christian Volkmer
committed
final JSONObject json = JSONObject.fromObject(loggedIn);
final Document doc = new Document(json);
if (doc.getId().isEmpty()) {
// If this is a new user without a logged_in document, we have
// to remove the following
// pre-filled fields. Otherwise, CouchDB will take these empty
// fields as genuine
// identifiers, and will throw errors afterwards.
doc.remove("_id");
doc.remove("_rev");
}
Paul-Christian Volkmer
committed
getDatabase().saveDocument(doc);
Paul-Christian Volkmer
committed
final LoggedIn l = (LoggedIn) JSONObject.toBean(doc.getJSONObject(), LoggedIn.class);
final JSONArray vs = doc.getJSONObject().optJSONArray("visitedSessions");
Paul-Christian Volkmer
committed
final Collection<VisitedSession> visitedSessions = JSONArray.toCollection(vs, VisitedSession.class);
l.setVisitedSessions(new ArrayList<VisitedSession>(visitedSessions));
}
Paul-Christian Volkmer
committed
} catch (final IOException e) {
return null;
}
}
Christoph Thelen
committed
@Override
@CachePut(value = "sessions")
public final Session updateSessionOwnerActivity(final Session session) {
Christoph Thelen
committed
try {
/* Do not clutter CouchDB. Only update once every 3 hours. */
if (session.getLastOwnerActivity() > System.currentTimeMillis() - 3 * 3600000) {
Christoph Thelen
committed
session.setLastOwnerActivity(System.currentTimeMillis());
Paul-Christian Volkmer
committed
final JSONObject json = JSONObject.fromObject(session);
getDatabase().saveDocument(new Document(json));
Paul-Christian Volkmer
committed
} catch (final IOException e) {
LOGGER.error("Failed to update lastOwnerActivity for Session {}", session);
Christoph Thelen
committed
}
}
Christoph Thelen
committed
public final List<String> getQuestionIds(final Session session, final User user) {
NovaView view = new NovaView("skill_question/by_session_only_id_for_all");
view.setKey(session.get_id());
return collectQuestionIds(view);
@Caching(evict = { @CacheEvict("questions"),
@CacheEvict(value = "skillquestions", allEntries = true),
@CacheEvict(value = "lecturequestions", allEntries = true),
@CacheEvict(value = "preparationquestions", allEntries = true),
@CacheEvict(value = "flashcardquestions", allEntries = true) })
public final void deleteQuestionWithAnswers(final Question question) {
Paul-Christian Volkmer
committed
deleteAnswers(question);
deleteDocument(question.get_id());
} catch (final IOException e) {
LOGGER.error("IOException: Could not delete question {}", question.get_id());
@Caching(evict = { @CacheEvict("questions"),
@CacheEvict(value = "skillquestions", allEntries = true),
@CacheEvict(value = "lecturequestions", allEntries = true),
@CacheEvict(value = "preparationquestions", allEntries = true),
@CacheEvict(value = "flashcardquestions", allEntries = true) })
@Override
Paul-Christian Volkmer
committed
public final void deleteAllQuestionsWithAnswers(final Session session) {
final NovaView view = new NovaView("skill_question/by_session");
deleteAllQuestionDocumentsWithAnswers(session, view);
}
Paul-Christian Volkmer
committed
private void deleteAllQuestionDocumentsWithAnswers(final Session session, final NovaView view) {
view.setStartKeyArray(session.get_id());
view.setEndKey(session.get_id(), "{}");
Paul-Christian Volkmer
committed
final ViewResults results = getDatabase().view(view);
List<Question> questions = new ArrayList<Question>();
Paul-Christian Volkmer
committed
for (final Document d : results.getResults()) {
final Question q = new Question();
q.set_id(d.getId());
q.set_rev(d.getJSONObject("value").getString("_rev"));
deleteAllAnswersWithQuestions(questions);
private void deleteDocument(final String documentId) throws IOException {
Paul-Christian Volkmer
committed
final Document d = getDatabase().getDocument(documentId);
getDatabase().deleteDocument(d);
@Override
public final void deleteAnswers(final Question question) {
Paul-Christian Volkmer
committed
final NovaView view = new NovaView("answer/cleanup");
view.setKey(question.get_id());
view.setIncludeDocs(true);
Paul-Christian Volkmer
committed
final ViewResults results = getDatabase().view(view);
List<Document> answersToDelete = new ArrayList<Document>();
for (final Document a : results.getResults()) {
final Document d = new Document(a.getJSONObject("doc"));
d.put("_deleted", true);
answersToDelete.add(d);
database.bulkSaveDocuments(answersToDelete.toArray(new Document[answersToDelete.size()]));
Paul-Christian Volkmer
committed
} catch (final IOException e) {
LOGGER.error("IOException: Could not delete answers for question {}", question.get_id());
public final List<String> getUnAnsweredQuestionIds(final Session session, final User user) {
Paul-Christian Volkmer
committed
final NovaView view = new NovaView("answer/by_user");
view.setKey(user.getUsername(), session.get_id());
return collectUnansweredQuestionIds(getQuestionIds(session, user), view);
Paul-Christian Volkmer
committed
public final Answer getMyAnswer(final User me, final String questionId, final int piRound) {
Paul-Christian Volkmer
committed
final NovaView view = new NovaView("answer/by_question_and_user_and_piround");
view.setKey(questionId, me.getUsername(), "2");
} else {
/* needed for legacy questions whose piRound property has not been set */
view.setStartKey(questionId, me.getUsername());
view.setEndKey(questionId, me.getUsername(), "1");
Paul-Christian Volkmer
committed
final ViewResults results = getDatabase().view(view);
if (results.getResults().isEmpty()) {
return null;
}
return (Answer) JSONObject.toBean(
results.getJSONArray("rows").optJSONObject(0).optJSONObject("value"),
Answer.class
Paul-Christian Volkmer
committed
public final List<Answer> getAnswers(final String questionId, final int piRound) {
final NovaView view = new NovaView("skill_question/count_answers_by_question_and_piround");
if (2 == piRound) {
view.setStartKey(questionId, "2");
view.setEndKey(questionId, "2", "{}");
} else {
/* needed for legacy questions whose piRound property has not been set */
view.setStartKeyArray(questionId);
view.setEndKeyArray(questionId, "1", "{}");
view.setGroup(true);
Paul-Christian Volkmer
committed
final ViewResults results = getDatabase().view(view);
final int abstentionCount = getAbstentionAnswerCount(questionId);
final List<Answer> answers = new ArrayList<Answer>();
for (final Document d : results.getResults()) {
final Answer a = new Answer();
a.setAnswerCount(d.getInt("value"));
a.setAbstentionCount(abstentionCount);
a.setQuestionId(d.getJSONObject().getJSONArray("key").getString(0));
a.setPiRound(piRound);
Paul-Christian Volkmer
committed
final String answerText = d.getJSONObject().getJSONArray("key").getString(2);
a.setAnswerText("null".equals(answerText) ? null : answerText);
answers.add(a);
}
return answers;
@Override
public int getAbstentionAnswerCount(final String questionId) {
Paul-Christian Volkmer
committed
final NovaView view = new NovaView("skill_question/count_abstention_answers_by_question");
view.setKey(questionId);
view.setGroup(true);
Paul-Christian Volkmer
committed
final ViewResults results = getDatabase().view(view);
if (results.getResults().size() == 0) {
return 0;
return results.getJSONArray("rows").optJSONObject(0).optInt("value");
public final int getAnswerCount(final Question question, final int piRound) {
Paul-Christian Volkmer
committed
final NovaView view = new NovaView("skill_question/count_total_answers_by_question_and_piround");
view.setGroup(true);
view.setStartKey(question.get_id(), String.valueOf(piRound));
view.setEndKey(question.get_id(), String.valueOf(piRound), "{}");
Paul-Christian Volkmer
committed
final ViewResults results = getDatabase().view(view);
if (results.getResults().size() == 0) {
return 0;
return results.getJSONArray("rows").optJSONObject(0).optInt("value");
Paul-Christian Volkmer
committed
private boolean isEmptyResults(final ViewResults results) {
return results == null || results.getResults().isEmpty() || results.getJSONArray("rows").size() == 0;
}
Paul-Christian Volkmer
committed
public List<Answer> getFreetextAnswers(final String questionId) {
final List<Answer> answers = new ArrayList<Answer>();
final NovaView view = new NovaView("skill_question/freetext_answers_full");
view.setKey(questionId);
Paul-Christian Volkmer
committed
final ViewResults results = getDatabase().view(view);
if (results.getResults().isEmpty()) {
Paul-Christian Volkmer
committed
for (final Document d : results.getResults()) {
final Answer a = (Answer) JSONObject.toBean(d.getJSONObject().getJSONObject("value"), Answer.class);
a.setQuestionId(questionId);
answers.add(a);
}
return answers;
public List<Answer> getMyAnswers(final User me, final Session s) {
Paul-Christian Volkmer
committed
final NovaView view = new NovaView("answer/by_user_and_session_full");
view.setKey(me.getUsername(), s.get_id());
Paul-Christian Volkmer
committed
final ViewResults results = getDatabase().view(view);
final List<Answer> answers = new ArrayList<Answer>();
if (results == null || results.getResults() == null || results.getResults().isEmpty()) {
Paul-Christian Volkmer
committed
for (final Document d : results.getResults()) {
final Answer a = (Answer) JSONObject.toBean(d.getJSONObject().getJSONObject("value"), Answer.class);
a.set_id(d.getId());
a.set_rev(d.getRev());
a.setUser(me.getUsername());
a.setSessionId(s.get_id());
answers.add(a);
}
return answers;
Paul-Christian Volkmer
committed
public int getTotalAnswerCount(final String sessionKey) {
final Session s = getSessionFromKeyword(sessionKey);
if (s == null) {
throw new NotFoundException();
}
Paul-Christian Volkmer
committed
final NovaView view = new NovaView("skill_question/count_answers_by_session");
view.setKey(s.get_id());
Paul-Christian Volkmer
committed
final ViewResults results = getDatabase().view(view);
if (results.getResults().size() == 0) {
return 0;
return results.getJSONArray("rows").optJSONObject(0).optInt("value");
Paul-Christian Volkmer
committed
public int getInterposedCount(final String sessionKey) {
final Session s = getSessionFromKeyword(sessionKey);
if (s == null) {
throw new NotFoundException();
}
Paul-Christian Volkmer
committed
final NovaView view = new NovaView("interposed_question/count_by_session");
view.setKey(s.get_id());
view.setGroup(true);
Paul-Christian Volkmer
committed
final ViewResults results = getDatabase().view(view);
if (results.size() == 0 || results.getResults().size() == 0) {
return 0;
return results.getJSONArray("rows").optJSONObject(0).optInt("value");
Paul-Christian Volkmer
committed
public InterposedReadingCount getInterposedReadingCount(final Session session) {
final NovaView view = new NovaView("interposed_question/count_by_session_reading");
view.setStartKeyArray(session.get_id());
view.setEndKeyArray(session.get_id(), "{}");
view.setGroup(true);
return getInterposedReadingCount(view);
}
@Override
public InterposedReadingCount getInterposedReadingCount(final Session session, final User user) {
final NovaView view = new NovaView("interposed_question/count_by_session_reading_for_creator");
view.setStartKeyArray(session.get_id(), user.getUsername());
view.setEndKeyArray(session.get_id(), user.getUsername(), "{}");
view.setGroup(true);
return getInterposedReadingCount(view);
}
private InterposedReadingCount getInterposedReadingCount(final NovaView view) {
Paul-Christian Volkmer
committed
final ViewResults results = getDatabase().view(view);
if (results.size() == 0 || results.getResults().size() == 0) {
return new InterposedReadingCount();
// A complete result looks like this. Note that the second row is optional, and that the first one may be
// 'unread' or 'read', i.e., results may be switched around or only one result may be present.
// count = {"rows":[
// {"key":["cecebabb21b096e592d81f9c1322b877","Guestc9350cf4a3","read"],"value":1},
// {"key":["cecebabb21b096e592d81f9c1322b877","Guestc9350cf4a3","unread"],"value":1}
// ]}
int read = 0, unread = 0;
String type = "";
final JSONObject fst = results.getJSONArray("rows").getJSONObject(0);
final JSONObject snd = results.getJSONArray("rows").optJSONObject(1);
final JSONArray fstkey = fst.getJSONArray("key");
if (fstkey.size() == 2) {
type = fstkey.getString(1);
} else if (fstkey.size() == 3) {
type = fstkey.getString(2);
}