/*
* This file is part of ARSnova Backend.
* Copyright (C) 2012-2017 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 com.fourspaces.couchdb.Database;
import com.fourspaces.couchdb.Document;
import com.fourspaces.couchdb.Results;
import com.fourspaces.couchdb.RowResult;
import com.fourspaces.couchdb.View;
import com.fourspaces.couchdb.ViewResults;
import com.google.common.collect.Lists;
import de.thm.arsnova.connector.model.Course;
import de.thm.arsnova.domain.CourseScore;
import de.thm.arsnova.entities.*;
import de.thm.arsnova.entities.transport.AnswerQueueElement;
import de.thm.arsnova.entities.transport.ImportExportSession;
import de.thm.arsnova.entities.transport.ImportExportSession.ImportExportQuestion;
import de.thm.arsnova.events.NewAnswerEvent;
import de.thm.arsnova.exceptions.NotFoundException;
import de.thm.arsnova.services.ISessionService;
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.checkerframework.checker.nullness.qual.NonNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.aop.framework.AopContext;
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.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.context.annotation.Profile;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;
import java.io.IOException;
import java.util.*;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentLinkedQueue;
/**
* Database implementation based on CouchDB.
*
* Note to developers:
*
* This class makes use of Spring Framework's caching annotations. When you are about to add new functionality,
* you should also think about the possibility of caching. Ideally, your methods should be dependent on domain
* objects like Session or Question, which can be used as cache keys. Relying on plain String objects as a key, e.g.
* by passing only a Session's keyword, will make your cache annotations less readable. You will also need to think
* about cases where your cache needs to be updated and evicted.
*
* In order to use cached methods from within this object, you have to use the getDatabaseDao() method instead of
* using the "this" pointer. This is because caching is only available if the method is called through a Spring Proxy,
* which is not the case when using "this".
*
* @see <a href="http://docs.spring.io/spring/docs/current/spring-framework-reference/html/cache.html">Spring Framework's Cache Abstraction</a>
* @see <a href="https://github.com/thm-projects/arsnova-backend/wiki/Caching">Caching in ARSnova explained</a>
*/
@Profile("!test")
@Service("databaseDao")
public class CouchDBDao implements IDatabaseDao, ApplicationEventPublisherAware {
private static final int BULK_PARTITION_SIZE = 500;
@Autowired
private ISessionService sessionService;
private String databaseHost;
private int databasePort;
private String databaseName;
private Database database;
private ApplicationEventPublisher publisher;
private final Queue<AbstractMap.SimpleEntry<Document, AnswerQueueElement>> answerQueue = new ConcurrentLinkedQueue<>();
private static final Logger logger = LoggerFactory.getLogger(CouchDBDao.class);
@Value("${couchdb.host}")
public void setDatabaseHost(final String newDatabaseHost) {
databaseHost = newDatabaseHost;
}
@Value("${couchdb.port}")
public void setDatabasePort(final String newDatabasePort) {
databasePort = Integer.parseInt(newDatabasePort);
}
@Value("${couchdb.name}")
public void setDatabaseName(final String newDatabaseName) {
databaseName = newDatabaseName;
}
public void setSessionService(final ISessionService service) {
sessionService = service;
}
/**
* Allows access to the proxy object. It has to be used instead of <code>this</code> for local calls to public
* methods for caching purposes. This is an ugly but necessary temporary workaround until a better solution is
* implemented (e.g. use of AspectJ's weaving).
* @return the proxy for CouchDBDao
*/
private @NonNull IDatabaseDao getDatabaseDao() {
return (IDatabaseDao) AopContext.currentProxy();
}
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
this.publisher = publisher;
}
@Override
public void log(String event, Map<String, Object> payload, LogEntry.LogLevel level) {
final Document d = new Document();
d.put("timestamp", System.currentTimeMillis());
d.put("type", "log");
d.put("event", event);
d.put("level", level.ordinal());
d.put("payload", payload);
try {
database.saveDocument(d);
} catch (final IOException e) {
logger.error("Logging of '{}' event to database failed.", event, e);
}
}
@Override
public void log(String event, Map<String, Object> payload) {
log(event, payload, LogEntry.LogLevel.INFO);
}
@Override
public void log(String event, LogEntry.LogLevel level, Object... rawPayload) {
if (rawPayload.length % 2 != 0) {
throw new IllegalArgumentException("");
}
Map<String, Object> payload = new HashMap<>();
for (int i = 0; i < rawPayload.length; i += 2) {
payload.put((String) rawPayload[i], rawPayload[i + 1]);
}
log(event, payload, level);
}
@Override
public void log(String event, Object... rawPayload) {
log(event, LogEntry.LogLevel.INFO, rawPayload);
}
@Override
public List<Session> getMySessions(final User user, final int start, final int limit) {
return this.getDatabaseDao().getSessionsForUsername(user.getUsername(), start, limit);
}
@Override
public List<Session> getSessionsForUsername(String username, final int start, final int limit) {
final View view = new View("session/partial_by_sessiontype_creator_name");
if (start > 0) {
view.setSkip(start);
}
if (limit > 0) {
view.setLimit(limit);
}
view.setStartKeyArray("", username);
view.setEndKeyArray("", username, "{}");
final Results<Session> results = getDatabase().queryView(view, Session.class);
final List<Session> result = new ArrayList<>();
for (final RowResult<Session> row : results.getRows()) {
final Session session = row.getValue();
session.setCreator(row.getKey().getString(1));
session.setName(row.getKey().getString(2));
session.set_id(row.getId());
result.add(session);
}
return result;
}
@Override
public List<Session> getPublicPoolSessions() {
// TODO replace with new view
final View view = new View("session/partial_by_ppsubject_name_for_publicpool");
final ViewResults sessions = getDatabase().view(view);
final List<Session> result = new ArrayList<>();
for (final Document d : sessions.getResults()) {
final Session session = (Session) JSONObject.toBean(
d.getJSONObject().getJSONObject("value"),
Session.class
);
session.set_id(d.getId());
result.add(session);
}
return result;
}
@Override
public List<SessionInfo> getPublicPoolSessionsInfo() {
final List<Session> sessions = this.getPublicPoolSessions();
return getInfosForSessions(sessions);
}
@Override
public List<Session> getMyPublicPoolSessions(final User user) {
final View view = new View("session/partial_by_sessiontype_creator_name");
view.setStartKeyArray("public_pool", user.getUsername());
view.setEndKeyArray("public_pool", user.getUsername(), "{}");
final ViewResults sessions = getDatabase().view(view);
final List<Session> result = new ArrayList<>();
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(1));
session.setName(d.getJSONObject().getJSONArray("key").getString(2));
session.set_id(d.getId());
result.add(session);
}
return result;
}
@Override
public List<SessionInfo> getMyPublicPoolSessionsInfo(final User user) {
final List<Session> sessions = this.getMyPublicPoolSessions(user);
if (sessions.isEmpty()) {
return new ArrayList<>();
}
return getInfosForSessions(sessions);
}
@Override
public List<SessionInfo> getMySessionsInfo(final User user, final int start, final int limit) {
final List<Session> sessions = this.getMySessions(user, start, limit);
if (sessions.isEmpty()) {
return new ArrayList<>();
}
return getInfosForSessions(sessions);
}
private List<SessionInfo> getInfosForSessions(final List<Session> sessions) {
/* TODO: migrate to new view */
final ExtendedView questionCountView = new ExtendedView("content/by_sessionid");
final ExtendedView answerCountView = new ExtendedView("answer/by_sessionid");
final ExtendedView interposedCountView = new ExtendedView("comment/by_sessionid");
final ExtendedView unreadInterposedCountView = new ExtendedView("comment/by_sessionid_read");
interposedCountView.setSessionIdKeys(sessions);
interposedCountView.setGroup(true);
questionCountView.setSessionIdKeys(sessions);
questionCountView.setGroup(true);
answerCountView.setSessionIdKeys(sessions);
answerCountView.setGroup(true);
List<String> unreadInterposedQueryKeys = new ArrayList<>();
for (Session s : sessions) {
unreadInterposedQueryKeys.add("[\"" + s.get_id() + "\",false]");
}
unreadInterposedCountView.setKeys(unreadInterposedQueryKeys);
unreadInterposedCountView.setGroup(true);
return getSessionInfoData(sessions, questionCountView, answerCountView, interposedCountView, unreadInterposedCountView);
}
private List<SessionInfo> getInfosForVisitedSessions(final List<Session> sessions, final User user) {
final ExtendedView answeredQuestionsView = new ExtendedView("answer/by_user_sessionid");
final ExtendedView questionIdsView = new ExtendedView("content/by_sessionid");
questionIdsView.setSessionIdKeys(sessions);
List<String> answeredQuestionQueryKeys = new ArrayList<>();
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<>();
final Map<String, Set<String>> questionIdMap = new HashMap<>();
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<>();
}
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<>();
}
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<>();
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<>(questionIdMap.get(s.get_id()));
Set<String> answeredQuestionIdsInSession = answeredQuestionsMap.get(s.get_id());
if (answeredQuestionIdsInSession == null) {
answeredQuestionIdsInSession = new HashSet<>();
}
questionIdsInSession.removeAll(answeredQuestionIdsInSession);
unansweredQuestionsCountMap.put(s.get_id(), questionIdsInSession.size());
}
List<SessionInfo> sessionInfos = new ArrayList<>();
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<>();
for (final Document d : questionCountViewResults.getResults()) {
questionCountMap.put(d.getString("key"), d.getInt("value"));
}
Map<String, Integer> answerCountMap = new HashMap<>();
for (final Document d : answerCountViewResults.getResults()) {
answerCountMap.put(d.getString("key"), d.getInt("value"));
}
Map<String, Integer> interposedCountMap = new HashMap<>();
for (final Document d : interposedCountViewResults.getResults()) {
interposedCountMap.put(d.getString("key"), d.getInt("value"));
}
Map<String, Integer> unredInterposedCountMap = new HashMap<>();
for (final Document d : unredInterposedCountViewResults.getResults()) {
unredInterposedCountMap.put(d.getJSONArray("key").getString(0), d.getInt("value"));
}
List<SessionInfo> sessionInfos = new ArrayList<>();
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;
}
@Cacheable("skillquestions")
@Override
public List<Question> getSkillQuestionsForUsers(final Session session) {
final List<Question> questions = new ArrayList<>();
final String viewName = "content/doc_by_sessionid_variant_active";
final View view1 = new View(viewName);
final View view2 = new View(viewName);
final View view3 = new View(viewName);
view1.setStartKey(session.get_id(), "lecture", true);
view1.setEndKey(session.get_id(), "lecture", true, "{}");
view2.setStartKey(session.get_id(), "preparation", true);
view2.setEndKey(session.get_id(), "preparation", true, "{}");
view3.setStartKey(session.get_id(), "flashcard", true);
view3.setEndKey(session.get_id(), "flashcard", true, "{}");
questions.addAll(getQuestions(view1, session));
questions.addAll(getQuestions(view2, session));
questions.addAll(getQuestions(view3, session));
return questions;
}
@Cacheable("skillquestions")
@Override
public List<Question> getSkillQuestionsForTeachers(final Session session) {
final View view = new View("content/doc_by_sessionid_variant_active");
view.setStartKey(session.get_id());
view.setEndKey(session.get_id(), "{}");
return getQuestions(view, session);
}
@Override
public int getSkillQuestionCount(final Session session) {
final View view = new View("content/by_sessionid_variant_active");
view.setStartKey(session.get_id());
view.setEndKey(session.get_id(), "{}");
return getQuestionCount(view);
}
@Override
@Cacheable("sessions")
public Session getSessionFromKeyword(final String keyword) {
final View view = new View("session/by_keyword");
view.setIncludeDocs(true);
view.setKey(keyword);
final ViewResults results = getDatabase().view(view);
if (results.getJSONArray("rows").optJSONObject(0) == null) {
throw new NotFoundException();
}
return (Session) JSONObject.toBean(
results.getJSONArray("rows").optJSONObject(0).optJSONObject("doc"),
Session.class
);
}
@Override
@Cacheable("sessions")
public Session getSessionFromId(final String sessionId) {
try {
final Document doc = getDatabase().getDocument(sessionId);
if (!"session".equals(doc.getString("type"))) {
return null;
}
return (Session) JSONObject.toBean(
doc.getJSONObject(),
Session.class
);
} catch (IOException e) {
return null;
}
}
@Override
public Session saveSession(final User user, final Session session) {
session.setKeyword(sessionService.generateKeyword());
session.setActive(true);
session.setFeedbackLock(false);
final Document sessionDocument = new Document();
sessionDocument.put("type", "session");
sessionDocument.put("name", session.getName());
sessionDocument.put("shortName", session.getShortName());
sessionDocument.put("keyword", session.getKeyword());
sessionDocument.put("creator", user.getUsername());
sessionDocument.put("active", session.isActive());
sessionDocument.put("courseType", session.getCourseType());
sessionDocument.put("courseId", session.getCourseId());
sessionDocument.put("creationTime", session.getCreationTime());
sessionDocument.put("learningProgressOptions", JSONObject.fromObject(session.getLearningProgressOptions()));
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());
sessionDocument.put("sessionType", session.getSessionType());
sessionDocument.put("features", JSONObject.fromObject(session.getFeatures()));
sessionDocument.put("feedbackLock", session.getFeedbackLock());
try {
database.saveDocument(sessionDocument);
session.set_id(sessionDocument.getId());
} catch (final IOException e) {
return null;
}
return session.get_id() != null ? session : null;
}
@Override
public boolean sessionKeyAvailable(final String keyword) {
final View view = new View("session/by_keyword");
view.setKey(keyword);
final ViewResults results = getDatabase().view(view);
return !results.containsKey(keyword);
}
private String getSessionKeyword(final String internalSessionId) throws IOException {
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 {
final com.fourspaces.couchdb.Session session = new com.fourspaces.couchdb.Session(
databaseHost,
databasePort
);
database = session.getDatabase(databaseName);
} catch (final Exception e) {
logger.error("Cannot connect to CouchDB database '{}' on host '{}' using port {}.",
databaseName, databaseHost, databasePort, e);
}
}
return database;
}
@Caching(evict = {@CacheEvict(value = "skillquestions", key = "#session"),
@CacheEvict(value = "lecturequestions", key = "#session", condition = "#question.getQuestionVariant().equals('lecture')"),
@CacheEvict(value = "preparationquestions", key = "#session", condition = "#question.getQuestionVariant().equals('preparation')"),
@CacheEvict(value = "flashcardquestions", key = "#session", condition = "#question.getQuestionVariant().equals('flashcard')") },
put = {@CachePut(value = "questions", key = "#question._id")})
@Override
public Question saveQuestion(final Session session, final Question question) {
final Document q = toQuestionDocument(session, question);
try {
database.saveDocument(q);
question.set_id(q.getId());
question.set_rev(q.getRev());
return question;
} catch (final IOException e) {
logger.error("Could not save question {}.", question, e);
}
return null;
}
private Document toQuestionDocument(final Session session, final Question question) {
Document q = new Document();
question.updateRoundManagementState();
q.put("type", "skill_question");
q.put("questionType", question.getQuestionType());
q.put("ignoreCaseSensitive", question.isIgnoreCaseSensitive());
q.put("ignoreWhitespaces", question.isIgnoreWhitespaces());
q.put("ignorePunctuation", question.isIgnorePunctuation());
q.put("fixedAnswer", question.isFixedAnswer());
q.put("strictMode", question.isStrictMode());
q.put("rating", question.getRating());
q.put("correctAnswer", question.getCorrectAnswer());
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("votingDisabled", question.isVotingDisabled());
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("piRoundStartTime", question.getPiRoundStartTime());
q.put("piRoundEndTime", question.getPiRoundEndTime());
q.put("piRoundFinished", question.isPiRoundFinished());
q.put("piRoundActive", question.isPiRoundActive());
q.put("showStatistic", question.isShowStatistic());
q.put("showAnswer", question.isShowAnswer());
q.put("abstention", question.isAbstention());
q.put("image", question.getImage());
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());
q.put("thresholdCorrectAnswers", question.getThresholdCorrectAnswers());
q.put("cvIsColored", question.getCvIsColored());
q.put("gridLineColor", question.getGridLineColor());
q.put("numberOfDots", question.getNumberOfDots());
q.put("gridType", question.getGridType());
q.put("scaleFactor", question.getScaleFactor());
q.put("gridScaleFactor", question.getGridScaleFactor());
q.put("imageQuestion", question.isImageQuestion());
q.put("textAnswerEnabled", question.isTextAnswerEnabled());
q.put("timestamp", question.getTimestamp());
q.put("hint", question.getHint());
q.put("solution", question.getSolution());
return q;
}
/* TODO: Only evict cache entry for the question's session. This requires some refactoring. */
@Caching(evict = {@CacheEvict(value = "skillquestions", allEntries = true),
@CacheEvict(value = "lecturequestions", allEntries = true, condition = "#question.getQuestionVariant().equals('lecture')"),
@CacheEvict(value = "preparationquestions", allEntries = true, condition = "#question.getQuestionVariant().equals('preparation')"),
@CacheEvict(value = "flashcardquestions", allEntries = true, condition = "#question.getQuestionVariant().equals('flashcard')") },
put = {@CachePut(value = "questions", key = "#question._id")})
@Override
public Question updateQuestion(final Question question) {
try {
final Document q = database.getDocument(question.get_id());
question.updateRoundManagementState();
q.put("subject", question.getSubject());
q.put("text", question.getText());
q.put("active", question.isActive());
q.put("votingDisabled", question.isVotingDisabled());
q.put("releasedFor", question.getReleasedFor());
q.put("possibleAnswers", question.getPossibleAnswers());
q.put("noCorrect", question.isNoCorrect());
q.put("piRound", question.getPiRound());
q.put("piRoundStartTime", question.getPiRoundStartTime());
q.put("piRoundEndTime", question.getPiRoundEndTime());
q.put("piRoundFinished", question.isPiRoundFinished());
q.put("piRoundActive", question.isPiRoundActive());
q.put("showStatistic", question.isShowStatistic());
q.put("ignoreCaseSensitive", question.isIgnoreCaseSensitive());
q.put("ignoreWhitespaces", question.isIgnoreWhitespaces());
q.put("ignorePunctuation", question.isIgnorePunctuation());
q.put("fixedAnswer", question.isFixedAnswer());
q.put("strictMode", question.isStrictMode());
q.put("rating", question.getRating());
q.put("correctAnswer", question.getCorrectAnswer());
q.put("showAnswer", question.isShowAnswer());
q.put("abstention", question.isAbstention());
q.put("image", question.getImage());
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());
q.put("thresholdCorrectAnswers", question.getThresholdCorrectAnswers());
q.put("cvIsColored", question.getCvIsColored());
q.put("gridLineColor", question.getGridLineColor());
q.put("numberOfDots", question.getNumberOfDots());
q.put("gridType", question.getGridType());
q.put("scaleFactor", question.getScaleFactor());
q.put("gridScaleFactor", question.getGridScaleFactor());
q.put("imageQuestion", question.isImageQuestion());
q.put("hint", question.getHint());
q.put("solution", question.getSolution());
database.saveDocument(q);
question.set_rev(q.getRev());
return question;
} catch (final IOException e) {
logger.error("Could not update question {}.", question, e);
}
return null;
}
@Override
public InterposedQuestion saveQuestion(final Session session, final InterposedQuestion question, User user) {
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());
if (question.getTimestamp() != 0) {
q.put("timestamp", question.getTimestamp());
} else {
q.put("timestamp", System.currentTimeMillis());
}
q.put("read", false);
q.put("creator", user.getUsername());
try {
database.saveDocument(q);
question.set_id(q.getId());
question.set_rev(q.getRev());
return question;
} catch (final IOException e) {
logger.error("Could not save interposed question {}.", question, e);
}
return null;
}
@Cacheable("questions")
@Override
public Question getQuestion(final String id) {
try {
final Document q = getDatabase().getDocument(id);
if (q == null) {
return null;
}
final Question question = (Question) JSONObject.toBean(q.getJSONObject(), Question.class);
final JSONArray possibleAnswers = q.getJSONObject().getJSONArray("possibleAnswers");
@SuppressWarnings("unchecked")
final Collection<PossibleAnswer> answers = JSONArray.toCollection(possibleAnswers, PossibleAnswer.class);
question.updateRoundManagementState();
question.setPossibleAnswers(new ArrayList<>(answers));
question.setSessionKeyword(getSessionKeyword(question.getSessionId()));
return question;
} catch (final IOException e) {
logger.error("Could not get question {}.", id, e);
}
return null;
}
@Override
public LoggedIn registerAsOnlineUser(final User user, final Session session) {
try {
final View view = new View("logged_in/all");
view.setKey(user.getUsername());
final ViewResults results = getDatabase().view(view);
LoggedIn loggedIn = new LoggedIn();
if (results.getJSONArray("rows").optJSONObject(0) != null) {
final JSONObject json = results.getJSONArray("rows").optJSONObject(0).optJSONObject("value");
loggedIn = (LoggedIn) JSONObject.toBean(json, LoggedIn.class);
final JSONArray vs = json.optJSONArray("visitedSessions");
if (vs != null) {
@SuppressWarnings("unchecked")
final Collection<VisitedSession> visitedSessions = JSONArray.toCollection(vs, VisitedSession.class);
loggedIn.setVisitedSessions(new ArrayList<>(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);
loggedIn.updateTimestamp();
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");
}
getDatabase().saveDocument(doc);
final LoggedIn l = (LoggedIn) JSONObject.toBean(doc.getJSONObject(), LoggedIn.class);
final JSONArray vs = doc.getJSONObject().optJSONArray("visitedSessions");
if (vs != null) {
@SuppressWarnings("unchecked")
final Collection<VisitedSession> visitedSessions = JSONArray.toCollection(vs, VisitedSession.class);
l.setVisitedSessions(new ArrayList<>(visitedSessions));
}
return l;
} catch (final IOException e) {
return null;
}
}
@Override
@CachePut(value = "sessions")
public Session updateSessionOwnerActivity(final Session session) {
try {
/* Do not clutter CouchDB. Only update once every 3 hours. */
if (session.getLastOwnerActivity() > System.currentTimeMillis() - 3 * 3600000) {
return session;
}
session.setLastOwnerActivity(System.currentTimeMillis());
final JSONObject json = JSONObject.fromObject(session);
getDatabase().saveDocument(new Document(json));
return session;
} catch (final IOException e) {
logger.error("Failed to update lastOwnerActivity for session {}.", session, e);
return session;
}
}
@Override
public List<String> getQuestionIds(final Session session, final User user) {
View view = new View("content/by_sessionid_variant_active");
view.setKey(session.get_id());
return collectQuestionIds(view);
}
/* TODO: Only evict cache entry for the question's session. This requires some refactoring. */
@Caching(evict = { @CacheEvict(value = "questions", key = "#question._id"),
@CacheEvict(value = "skillquestions", allEntries = true),
@CacheEvict(value = "lecturequestions", allEntries = true, condition = "#question.getQuestionVariant().equals('lecture')"),
@CacheEvict(value = "preparationquestions", allEntries = true, condition = "#question.getQuestionVariant().equals('preparation')"),
@CacheEvict(value = "flashcardquestions", allEntries = true, condition = "#question.getQuestionVariant().equals('flashcard')") })
@Override
public int deleteQuestionWithAnswers(final Question question) {
try {
int count = deleteAnswers(question);
deleteDocument(question.get_id());
log("delete", "type", "question", "answerCount", count);
return count;
} catch (final IOException e) {
logger.error("Could not delete question {}.", question.get_id(), e);
}
return 0;
}
@Caching(evict = { @CacheEvict(value = "questions", allEntries = true),
@CacheEvict(value = "skillquestions", key = "#session"),
@CacheEvict(value = "lecturequestions", key = "#session"),
@CacheEvict(value = "preparationquestions", key = "#session"),
@CacheEvict(value = "flashcardquestions", key = "#session") })
@Override
public int[] deleteAllQuestionsWithAnswers(final Session session) {
final View view = new View("content/by_sessionid_variant_active");
view.setStartKeyArray(session.get_id());
view.setEndKey(session.get_id(), "{}");
return deleteAllQuestionDocumentsWithAnswers(view);
}
private int[] deleteAllQuestionDocumentsWithAnswers(final View view) {
final ViewResults results = getDatabase().view(view);
List<Question> questions = new ArrayList<>();
for (final Document d : results.getResults()) {
final Question q = new Question();
q.set_id(d.getId());
q.set_rev(d.getString("value"));
questions.add(q);
}
int[] count = deleteAllAnswersWithQuestions(questions);
log("delete", "type", "question", "questionCount", count[0]);
log("delete", "type", "answer", "answerCount", count[1]);
return count;
}
private void deleteDocument(final String documentId) throws IOException {
final Document d = getDatabase().getDocument(documentId);
getDatabase().deleteDocument(d);
}
@CacheEvict("answers")
@Override
public int deleteAnswers(final Question question) {
try {
final View view = new View("answer/by_questionid");
view.setKey(question.get_id());
view.setIncludeDocs(true);
final ViewResults results = getDatabase().view(view);
final List<List<Document>> partitions = Lists.partition(results.getResults(), BULK_PARTITION_SIZE);
int count = 0;
for (List<Document> partition: partitions) {
List<Document> answersToDelete = new ArrayList<>();
for (final Document a : partition) {
final Document d = new Document(a.getJSONObject("doc"));
d.put("_deleted", true);
answersToDelete.add(d);
}
if (database.bulkSaveDocuments(answersToDelete.toArray(new Document[answersToDelete.size()]))) {
count += partition.size();
} else {
logger.error("Could not bulk delete answers.");
}
}
log("delete", "type", "answer", "answerCount", count);
return count;
} catch (final IOException e) {
logger.error("Could not delete answers for question {}.", question.get_id(), e);
}
return 0;
}
@Override
public List<String> getUnAnsweredQuestionIds(final Session session, final User user) {
final View view = new View("answer/questionid_by_user_sessionid_variant");
view.setStartKeyArray(user.getUsername(), session.get_id());
view.setEndKeyArray(user.getUsername(), session.get_id(), "{}");
return collectUnansweredQuestionIds(getQuestionIds(session, user), view);
}
@Override
public Answer getMyAnswer(final User me, final String questionId, final int piRound) {
final View view = new View("answer/doc_by_questionid_user_piround");
if (2 == 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");
}
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
);
}
@SuppressWarnings("unchecked")
@Override
public <T> T getObjectFromId(final String documentId, final Class<T> klass) {
try {
final Document doc = getDatabase().getDocument(documentId);
if (doc == null) {
return null;
}
// TODO: This needs some more error checking...
return (T) JSONObject.toBean(doc.getJSONObject(), klass);
} catch (ClassCastException | IOException | net.sf.json.JSONException e) {
return null;
}
}
@Override
public List<Answer> getAnswers(final Question question, final int piRound) {
final String questionId = question.get_id();
final View view = new View("answer/by_questionid_piround_text_subject");
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);
final ViewResults results = getDatabase().view(view);
final int abstentionCount = getDatabaseDao().getAbstentionAnswerCount(questionId);
final List<Answer> answers = new ArrayList<>();
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);
final String answerText = d.getJSONObject().getJSONArray("key").getString(3);
a.setAnswerText("null".equals(answerText) ? null : answerText);
answers.add(a);
}
return answers;
}
@Override
public List<Answer> getAllAnswers(final Question question) {
final String questionId = question.get_id();
final View view = new View("answer/by_questionid_piround_text_subject");
view.setStartKeyArray(questionId);
view.setEndKeyArray(questionId, "{}");
view.setGroup(true);
final ViewResults results = getDatabase().view(view);
final int abstentionCount = getDatabaseDao().getAbstentionAnswerCount(questionId);
final List<Answer> answers = new ArrayList<>();
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));
final String answerText = d.getJSONObject().getJSONArray("key").getString(3);
final String answerSubject = d.getJSONObject().getJSONArray("key").getString(4);
final boolean successfulFreeTextAnswer = d.getJSONObject().getJSONArray("key").getBoolean(5);
a.setAnswerText("null".equals(answerText) ? null : answerText);
a.setAnswerSubject("null".equals(answerSubject) ? null : answerSubject);
a.setSuccessfulFreeTextAnswer(successfulFreeTextAnswer);
answers.add(a);
}
return answers;
}
@Cacheable("answers")
@Override
public List<Answer> getAnswers(final Question question) {
return this.getAnswers(question, question.getPiRound());
}
@Override
public int getAbstentionAnswerCount(final String questionId) {
final View view = new View("answer/by_questionid_piround_text_subject");
view.setStartKeyArray(questionId);
view.setEndKeyArray(questionId, "{}");
view.setGroup(true);
final ViewResults results = getDatabase().view(view);
if (results.getResults().isEmpty()) {
return 0;
}
return results.getJSONArray("rows").optJSONObject(0).optInt("value");
}
@Override
public int getAnswerCount(final Question question, final int piRound) {
final View view = new View("answer/by_questionid_piround_text_subject");
view.setStartKey(question.get_id(), piRound);
view.setEndKey(question.get_id(), piRound, "{}");
view.setGroup(true);
final ViewResults results = getDatabase().view(view);
if (results.getResults().isEmpty()) {
return 0;
}
return results.getJSONArray("rows").optJSONObject(0).optInt("value");
}
@Override
public int getTotalAnswerCountByQuestion(final Question question) {
final View view = new View("answer/by_questionid_piround_text_subject");
view.setStartKeyArray(question.get_id());
view.setEndKeyArray(question.get_id(), "{}");
view.setGroup(true);
final ViewResults results = getDatabase().view(view);
if (results.getResults().isEmpty()) {
return 0;
}
return results.getJSONArray("rows").optJSONObject(0).optInt("value");
}
private boolean isEmptyResults(final ViewResults results) {
return results == null || results.getResults().isEmpty() || results.getJSONArray("rows").isEmpty();
}
@Override
public List<Answer> getFreetextAnswers(final String questionId, final int start, final int limit) {
final List<Answer> answers = new ArrayList<>();
final View view = new View("answer/doc_by_questionid_timestamp");
if (start > 0) {
view.setSkip(start);
}
if (limit > 0) {
view.setLimit(limit);
}
view.setDescending(true);
view.setStartKeyArray(questionId, "{}");
view.setEndKeyArray(questionId);
final ViewResults results = getDatabase().view(view);
if (results.getResults().isEmpty()) {
return answers;
}
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;
}
@Override
public List<Answer> getMyAnswers(final User me, final Session s) {
final View view = new View("answer/doc_by_user_sessionid");
view.setKey(me.getUsername(), s.get_id());
final ViewResults results = getDatabase().view(view);
final List<Answer> answers = new ArrayList<>();
if (results == null || results.getResults() == null || results.getResults().isEmpty()) {
return answers;
}
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;
}
@Override
public int getTotalAnswerCount(final String sessionKey) {
final Session s = getDatabaseDao().getSessionFromKeyword(sessionKey);
if (s == null) {
throw new NotFoundException();
}
final View view = new View("answer/by_sessionid_variant");
view.setKey(s.get_id());
final ViewResults results = getDatabase().view(view);
if (results.getResults().isEmpty()) {
return 0;
}
return results.getJSONArray("rows").optJSONObject(0).optInt("value");
}
@Override
public int getInterposedCount(final String sessionKey) {
final Session s = getDatabaseDao().getSessionFromKeyword(sessionKey);
if (s == null) {
throw new NotFoundException();
}
final View view = new View("comment/by_sessionid");
view.setKey(s.get_id());
view.setGroup(true);
final ViewResults results = getDatabase().view(view);
if (results.isEmpty() || results.getResults().isEmpty()) {
return 0;
}
return results.getJSONArray("rows").optJSONObject(0).optInt("value");
}
@Override
public InterposedReadingCount getInterposedReadingCount(final Session session) {
final View view = new View("comment/by_sessionid_read");
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 View view = new View("comment/by_sessionid_creator_read");
view.setStartKeyArray(session.get_id(), user.getUsername());
view.setEndKeyArray(session.get_id(), user.getUsername(), "{}");
view.setGroup(true);
return getInterposedReadingCount(view);
}
private InterposedReadingCount getInterposedReadingCount(final View view) {
final ViewResults results = getDatabase().view(view);
if (results.isEmpty() || results.getResults().isEmpty()) {
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;
boolean isRead = false;
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) {
isRead = fstkey.getBoolean(1);
} else if (fstkey.size() == 3) {
isRead = fstkey.getBoolean(2);
}
if (isRead) {
read = fst.optInt("value");
} else {
unread = fst.optInt("value");
}
if (snd != null) {
final JSONArray sndkey = snd.getJSONArray("key");
if (sndkey.size() == 2) {
isRead = sndkey.getBoolean(1);
} else {
isRead = sndkey.getBoolean(2);
}
if (isRead) {
read = snd.optInt("value");
} else {
unread = snd.optInt("value");
}
}
return new InterposedReadingCount(read, unread);
}
@Override
public List<InterposedQuestion> getInterposedQuestions(final Session session, final int start, final int limit) {
final View view = new View("comment/doc_by_sessionid_timestamp");
if (start > 0) {
view.setSkip(start);
}
if (limit > 0) {
view.setLimit(limit);
}
view.setDescending(true);
view.setStartKeyArray(session.get_id(), "{}");
view.setEndKeyArray(session.get_id());
final ViewResults questions = getDatabase().view(view);
if (questions == null || questions.isEmpty()) {
return null;
}
return createInterposedList(session, questions);
}
@Override
public List<InterposedQuestion> getInterposedQuestions(final Session session, final User user, final int start, final int limit) {
final View view = new View("comment/doc_by_sessionid_creator_timestamp");
if (start > 0) {
view.setSkip(start);
}
if (limit > 0) {
view.setLimit(limit);
}
view.setDescending(true);
view.setStartKeyArray(session.get_id(), user.getUsername(), "{}");
view.setEndKeyArray(session.get_id(), user.getUsername());
final ViewResults questions = getDatabase().view(view);
if (questions == null || questions.isEmpty()) {
return null;
}
return createInterposedList(session, questions);
}
private List<InterposedQuestion> createInterposedList(
final Session session, final ViewResults questions) {
final List<InterposedQuestion> result = new ArrayList<>();
for (final Document document : questions.getResults()) {
final InterposedQuestion question = (InterposedQuestion) JSONObject.toBean(
document.getJSONObject().getJSONObject("value"),
InterposedQuestion.class
);
question.setSessionId(session.getKeyword());
question.set_id(document.getId());
result.add(question);
}
return result;
}
@Cacheable("statistics")
@Override
public Statistics getStatistics() {
final Statistics stats = new Statistics();
try {
final View statsView = new View("statistics/statistics");
final View creatorView = new View("statistics/unique_session_creators");
final View studentUserView = new View("statistics/active_student_users");
statsView.setGroup(true);
creatorView.setGroup(true);
studentUserView.setGroup(true);
final ViewResults statsResults = getDatabase().view(statsView);
final ViewResults creatorResults = getDatabase().view(creatorView);
final ViewResults studentUserResults = getDatabase().view(studentUserView);
if (!isEmptyResults(statsResults)) {
final JSONArray rows = statsResults.getJSONArray("rows");
for (int i = 0; i < rows.size(); i++) {
final JSONObject row = rows.getJSONObject(i);
final int value = row.getInt("value");
switch (row.getString("key")) {
case "openSessions":
stats.setOpenSessions(stats.getOpenSessions() + value);
break;
case "closedSessions":
stats.setClosedSessions(stats.getClosedSessions() + value);
break;
case "deletedSessions":
/* Deleted sessions are not exposed separately for now. */
stats.setClosedSessions(stats.getClosedSessions() + value);
break;
case "answers":
stats.setAnswers(stats.getAnswers() + value);
break;
case "lectureQuestions":
stats.setLectureQuestions(stats.getLectureQuestions() + value);
break;
case "preparationQuestions":
stats.setPreparationQuestions(stats.getPreparationQuestions() + value);
break;
case "interposedQuestions":
stats.setInterposedQuestions(stats.getInterposedQuestions() + value);
break;
case "conceptQuestions":
stats.setConceptQuestions(stats.getConceptQuestions() + value);
break;
case "flashcards":
stats.setFlashcards(stats.getFlashcards() + value);
break;
}
}
}
if (!isEmptyResults(creatorResults)) {
final JSONArray rows = creatorResults.getJSONArray("rows");
Set<String> creators = new HashSet<>();
for (int i = 0; i < rows.size(); i++) {
final JSONObject row = rows.getJSONObject(i);
creators.add(row.getString("key"));
}
stats.setCreators(creators.size());
}
if (!isEmptyResults(studentUserResults)) {
final JSONArray rows = studentUserResults.getJSONArray("rows");
Set<String> students = new HashSet<>();
for (int i = 0; i < rows.size(); i++) {
final JSONObject row = rows.getJSONObject(i);
students.add(row.getString("key"));
}
stats.setActiveStudents(students.size());
}
return stats;
} catch (final Exception e) {
logger.error("Could not retrieve session count.", e);
}
return stats;
}
@Override
public InterposedQuestion getInterposedQuestion(final String questionId) {
try {
final Document document = getDatabase().getDocument(questionId);
final InterposedQuestion question = (InterposedQuestion) JSONObject.toBean(document.getJSONObject(),
InterposedQuestion.class);
question.setSessionId(getSessionKeyword(question.getSessionId()));
return question;
} catch (final IOException e) {
logger.error("Could not load interposed question {}.", questionId, e);
}
return null;
}
@Override
public void markInterposedQuestionAsRead(final InterposedQuestion question) {
try {
question.setRead(true);
final Document document = getDatabase().getDocument(question.get_id());
document.put("read", question.isRead());
getDatabase().saveDocument(document);
} catch (final IOException e) {
logger.error("Could not mark interposed question as read {}.", question.get_id(), e);
}
}
@Override
public List<Session> getMyVisitedSessions(final User user, final int start, final int limit) {
final View view = new View("logged_in/visited_sessions_by_user");
if (start > 0) {
view.setSkip(start);
}
if (limit > 0) {
view.setLimit(limit);
}
view.setKey(user.getUsername());
final ViewResults sessions = getDatabase().view(view);
final List<Session> allSessions = new ArrayList<>();
for (final Document d : sessions.getResults()) {
// Not all users have visited sessions
if (d.getJSONObject().optJSONArray("value") != null) {
@SuppressWarnings("unchecked")
final Collection<Session> visitedSessions = JSONArray.toCollection(
d.getJSONObject().getJSONArray("value"),
Session.class
);
allSessions.addAll(visitedSessions);
}
}
// Filter sessions that don't exist anymore, also filter my own sessions
final List<Session> result = new ArrayList<>();
final List<Session> filteredSessions = new ArrayList<>();
for (final Session s : allSessions) {
try {
final Session session = getDatabaseDao().getSessionFromKeyword(s.getKeyword());
if (session != null && !session.isCreator(user)) {
result.add(session);
} else {
filteredSessions.add(s);
}
} catch (final NotFoundException e) {
filteredSessions.add(s);
}
}
if (filteredSessions.isEmpty()) {
return result;
}
// Update document to remove sessions that don't exist anymore
try {
List<VisitedSession> visitedSessions = new ArrayList<>();
for (final Session s : result) {
visitedSessions.add(new VisitedSession(s));
}
final LoggedIn loggedIn = new LoggedIn();
final Document loggedInDocument = getDatabase().getDocument(sessions.getResults().get(0).getString("id"));
loggedIn.setSessionId(loggedInDocument.getString("sessionId"));
loggedIn.setUser(user.getUsername());
loggedIn.setTimestamp(loggedInDocument.getLong("timestamp"));
loggedIn.setType(loggedInDocument.getString("type"));
loggedIn.setVisitedSessions(visitedSessions);
loggedIn.set_id(loggedInDocument.getId());
loggedIn.set_rev(loggedInDocument.getRev());
final JSONObject json = JSONObject.fromObject(loggedIn);
final Document doc = new Document(json);
getDatabase().saveDocument(doc);
} catch (IOException e) {
logger.error("Could not clean up logged_in document of {}.", user.getUsername(), e);
}
return result;
}
@Override
public List<Session> getVisitedSessionsForUsername(String username, final int start, final int limit) {
final View view = new View("logged_in/visited_sessions_by_user");
if (start > 0) {
view.setSkip(start);
}
if (limit > 0) {
view.setLimit(limit);
}
view.setKey(username);
final ViewResults sessions = getDatabase().view(view);
final List<Session> allSessions = new ArrayList<>();
for (final Document d : sessions.getResults()) {
// Not all users have visited sessions
if (d.getJSONObject().optJSONArray("value") != null) {
@SuppressWarnings("unchecked")
final Collection<Session> visitedSessions = JSONArray.toCollection(
d.getJSONObject().getJSONArray("value"),
Session.class
);
allSessions.addAll(visitedSessions);
}
}
// Filter sessions that don't exist anymore, also filter my own sessions
final List<Session> result = new ArrayList<>();
final List<Session> filteredSessions = new ArrayList<>();
for (final Session s : allSessions) {
try {
final Session session = getDatabaseDao().getSessionFromKeyword(s.getKeyword());
if (session != null && !(session.getCreator().equals(username))) {
result.add(session);
} else {
filteredSessions.add(s);
}
} catch (final NotFoundException e) {
filteredSessions.add(s);
}
}
if (filteredSessions.isEmpty()) {
return result;
}
// Update document to remove sessions that don't exist anymore
try {
List<VisitedSession> visitedSessions = new ArrayList<>();
for (final Session s : result) {
visitedSessions.add(new VisitedSession(s));
}
final LoggedIn loggedIn = new LoggedIn();
final Document loggedInDocument = getDatabase().getDocument(sessions.getResults().get(0).getString("id"));
loggedIn.setSessionId(loggedInDocument.getString("sessionId"));
loggedIn.setUser(username);
loggedIn.setTimestamp(loggedInDocument.getLong("timestamp"));
loggedIn.setType(loggedInDocument.getString("type"));
loggedIn.setVisitedSessions(visitedSessions);
loggedIn.set_id(loggedInDocument.getId());
loggedIn.set_rev(loggedInDocument.getRev());
final JSONObject json = JSONObject.fromObject(loggedIn);
final Document doc = new Document(json);
getDatabase().saveDocument(doc);
} catch (IOException e) {
logger.error("Could not clean up logged_in document of {}.", username, e);
}
return result;
}
@Override
public List<SessionInfo> getMyVisitedSessionsInfo(final User user, final int start, final int limit) {
List<Session> sessions = this.getMyVisitedSessions(user, start, limit);
if (sessions.isEmpty()) {
return new ArrayList<>();
}
return this.getInfosForVisitedSessions(sessions, user);
}
@CacheEvict(value = "answers", key = "#question")
@Override
public Answer saveAnswer(final Answer answer, final User user, final Question question, final Session session) {
final Document a = new Document();
a.put("type", "skill_question_answer");
a.put("sessionId", answer.getSessionId());
a.put("questionId", answer.getQuestionId());
a.put("answerSubject", answer.getAnswerSubject());
a.put("questionVariant", answer.getQuestionVariant());
a.put("questionValue", answer.getQuestionValue());
a.put("answerText", answer.getAnswerText());
a.put("answerTextRaw", answer.getAnswerTextRaw());
a.put("successfulFreeTextAnswer", answer.isSuccessfulFreeTextAnswer());
a.put("timestamp", answer.getTimestamp());
a.put("user", user.getUsername());
a.put("piRound", answer.getPiRound());
a.put("abstention", answer.isAbstention());
a.put("answerImage", answer.getAnswerImage());
a.put("answerThumbnailImage", answer.getAnswerThumbnailImage());
AnswerQueueElement answerQueueElement = new AnswerQueueElement(session, question, answer, user);
this.answerQueue.offer(new AbstractMap.SimpleEntry<>(a, answerQueueElement));
return answer;
}
@Scheduled(fixedDelay = 5000)
public void flushAnswerQueue() {
final Map<Document, Answer> map = new HashMap<>();
final List<Document> answerList = new ArrayList<>();
final List<AnswerQueueElement> elements = new ArrayList<>();
AbstractMap.SimpleEntry<Document, AnswerQueueElement> entry;
while ((entry = this.answerQueue.poll()) != null) {
final Document doc = entry.getKey();
final Answer answer = entry.getValue().getAnswer();
map.put(doc, answer);
answerList.add(doc);
elements.add(entry.getValue());
}
if (answerList.isEmpty()) {
// no need to send an empty bulk request. ;-)
return;
}
try {
getDatabase().bulkSaveDocuments(answerList.toArray(new Document[answerList.size()]));
for (Document d : answerList) {
final Answer answer = map.get(d);
answer.set_id(d.getId());
answer.set_rev(d.getRev());
}
// Send NewAnswerEvents ...
for (AnswerQueueElement e : elements) {
this.publisher.publishEvent(new NewAnswerEvent(this, e.getSession(), e.getAnswer(), e.getUser(), e.getQuestion()));
}
} catch (IOException e) {
logger.error("Could not bulk save answers from queue.", e);
}
}
/* TODO: Only evict cache entry for the answer's question. This requires some refactoring. */
@CacheEvict(value = "answers", allEntries = true)
@Override
public Answer updateAnswer(final Answer answer) {
try {
final Document a = database.getDocument(answer.get_id());
a.put("answerSubject", answer.getAnswerSubject());
a.put("answerText", answer.getAnswerText());
a.put("answerTextRaw", answer.getAnswerTextRaw());
a.put("successfulFreeTextAnswer", answer.isSuccessfulFreeTextAnswer());
a.put("timestamp", answer.getTimestamp());
a.put("abstention", answer.isAbstention());
a.put("questionValue", answer.getQuestionValue());
a.put("answerImage", answer.getAnswerImage());
a.put("answerThumbnailImage", answer.getAnswerThumbnailImage());
a.put("read", answer.isRead());
database.saveDocument(a);
answer.set_rev(a.getRev());
return answer;
} catch (final IOException e) {
logger.error("Could not update answer {}.", answer, e);
}
return null;
}
/* TODO: Only evict cache entry for the answer's session. This requires some refactoring. */
@CacheEvict(value = "answers", allEntries = true)
@Override
public void deleteAnswer(final String answerId) {
try {
database.deleteDocument(database.getDocument(answerId));
log("delete", "type", "answer");
} catch (final IOException e) {
logger.error("Could not delete answer {}.", answerId, e);
}
}
@Override
public void deleteInterposedQuestion(final InterposedQuestion question) {
try {
deleteDocument(question.get_id());
log("delete", "type", "comment");
} catch (final IOException e) {
logger.error("Could not delete interposed question {}.", question.get_id(), e);
}
}
@Override
public List<Session> getCourseSessions(final List<Course> courses) {
final ExtendedView view = new ExtendedView("session/by_courseid");
view.setIncludeDocs(true);
view.setCourseIdKeys(courses);
final ViewResults sessions = getDatabase().view(view);
final List<Session> result = new ArrayList<>();
for (final Document d : sessions.getResults()) {
final Session session = (Session) JSONObject.toBean(
d.getJSONObject().getJSONObject("doc"),
Session.class
);
result.add(session);
}
return result;
}
/**
* Adds convenience methods to CouchDB4J's view class.
*/
private static class ExtendedView extends View {
ExtendedView(final String fullname) {
super(fullname);
}
void setCourseIdKeys(final List<Course> courses) {
List<String> courseIds = new ArrayList<>();
for (Course c : courses) {
courseIds.add(c.getId());
}
setKeys(courseIds);
}
void setSessionIdKeys(final List<Session> sessions) {
List<String> sessionIds = new ArrayList<>();
for (Session s : sessions) {
sessionIds.add(s.get_id());
}
setKeys(sessionIds);
}
}
@Override
@CachePut(value = "sessions")
public Session updateSession(final Session session) {
try {
final Document s = database.getDocument(session.get_id());
s.put("name", session.getName());
s.put("shortName", session.getShortName());
s.put("active", session.isActive());
s.put("ppAuthorName", session.getPpAuthorName());
s.put("ppAuthorMail", session.getPpAuthorMail());
s.put("ppUniversity", session.getPpUniversity());
s.put("ppLogo", session.getPpLogo());
s.put("ppSubject", session.getPpSubject());
s.put("ppLicense", session.getPpLicense());
s.put("ppDescription", session.getPpDescription());
s.put("ppFaculty", session.getPpFaculty());
s.put("ppLevel", session.getPpLevel());
s.put("learningProgressOptions", JSONObject.fromObject(session.getLearningProgressOptions()));
s.put("features", JSONObject.fromObject(session.getFeatures()));
s.put("feedbackLock", session.getFeedbackLock());
database.saveDocument(s);
session.set_rev(s.getRev());
return session;
} catch (final IOException e) {
logger.error("Could not update session {}.", session, e);
}
return null;
}
@Override
@Caching(evict = { @CacheEvict("sessions"), @CacheEvict(cacheNames = "sessions", key = "#p0.keyword") })
public Session changeSessionCreator(Session session, final String newCreator) {
try {
final Document s = database.getDocument(session.get_id());
s.put("creator", newCreator);
database.saveDocument(s);
session.set_rev(s.getRev());
} catch (final IOException e) {
logger.error("Could not update creator for session {}.", session, e);
}
return session;
}
@Override
@Caching(evict = { @CacheEvict("sessions"), @CacheEvict(cacheNames = "sessions", key = "#p0.keyword") })
public int[] deleteSession(final Session session) {
int[] count = new int[] {0, 0};
try {
count = deleteAllQuestionsWithAnswers(session);
deleteDocument(session.get_id());
logger.debug("Deleted session document {} and related data.", session.get_id());
log("delete", "type", "session", "id", session.get_id());
} catch (final IOException e) {
logger.error("Could not delete session {}.", session, e);
}
return count;
}
@Override
public int[] deleteInactiveGuestSessions(long lastActivityBefore) {
View view = new View("session/by_lastactivity_for_guests");
view.setEndKey(lastActivityBefore);
final List<Document> results = this.getDatabase().view(view).getResults();
int[] count = new int[3];
for (Document oldDoc : results) {
Session s = new Session();
s.set_id(oldDoc.getId());
s.set_rev(oldDoc.getJSONObject("value").getString("_rev"));
int[] qaCount = deleteSession(s);
count[1] += qaCount[0];
count[2] += qaCount[1];
}
if (!results.isEmpty()) {
logger.info("Deleted {} inactive guest sessions.", results.size());
log("cleanup", "type", "session", "sessionCount", results.size(), "questionCount", count[1], "answerCount", count[2]);
}
count[0] = results.size();
return count;
}
@Override
public int deleteInactiveGuestVisitedSessionLists(long lastActivityBefore) {
try {
View view = new View("logged_in/by_last_activity_for_guests");
view.setEndKey(lastActivityBefore);
List<Document> results = this.getDatabase().view(view).getResults();
int count = 0;
List<List<Document>> partitions = Lists.partition(results, BULK_PARTITION_SIZE);
for (List<Document> partition: partitions) {
final List<Document> newDocs = new ArrayList<>();
for (final Document oldDoc : partition) {
final Document newDoc = new Document();
newDoc.setId(oldDoc.getId());
newDoc.setRev(oldDoc.getJSONObject("value").getString("_rev"));
newDoc.put("_deleted", true);
newDocs.add(newDoc);
logger.debug("Marked logged_in document {} for deletion.", oldDoc.getId());
/* Use log type 'user' since effectively the user is deleted in case of guests */
log("delete", "type", "user", "id", oldDoc.getId());
}
if (!newDocs.isEmpty()) {
if (getDatabase().bulkSaveDocuments(newDocs.toArray(new Document[newDocs.size()]))) {
count += newDocs.size();
} else {
logger.error("Could not bulk delete visited session lists.");
}
}
}
if (count > 0) {
logger.info("Deleted {} visited session lists of inactive users.", count);
log("cleanup", "type", "visitedsessions", "count", count);
}
return count;
} catch (IOException e) {
logger.error("Could not delete visited session lists of inactive users.", e);
}
return 0;
}
@Cacheable("lecturequestions")
@Override
public List<Question> getLectureQuestionsForUsers(final Session session) {
final View view = new View("content/doc_by_sessionid_variant_active");
view.setStartKeyArray(session.get_id(), "lecture", true);
view.setEndKeyArray(session.get_id(), "lecture", true, "{}");
return getQuestions(view, session);
}
@Override
public List<Question> getLectureQuestionsForTeachers(final Session session) {
final View view = new View("content/doc_by_sessionid_variant_active");
view.setStartKeyArray(session.get_id(), "lecture");
view.setEndKeyArray(session.get_id(), "lecture", "{}");
return getQuestions(view, session);
}
@Cacheable("flashcardquestions")
@Override
public List<Question> getFlashcardsForUsers(final Session session) {
final View view = new View("content/doc_by_sessionid_variant_active");
view.setStartKeyArray(session.get_id(), "flashcard", true);
view.setEndKeyArray(session.get_id(), "flashcard", true, "{}");
return getQuestions(view, session);
}
@Override
public List<Question> getFlashcardsForTeachers(final Session session) {
final View view = new View("content/doc_by_sessionid_variant_active");
view.setStartKeyArray(session.get_id(), "flashcard");
view.setEndKeyArray(session.get_id(), "{}");
return getQuestions(view, session);
}
@Cacheable("preparationquestions")
@Override
public List<Question> getPreparationQuestionsForUsers(final Session session) {
final View view = new View("content/doc_by_sessionid_variant_active");
view.setStartKeyArray(session.get_id(), "preparation", true);
view.setEndKeyArray(session.get_id(), "preparation", true, "{}");
return getQuestions(view, session);
}
@Override
public List<Question> getPreparationQuestionsForTeachers(final Session session) {
final View view = new View("content/doc_by_sessionid_variant_active");
view.setStartKeyArray(session.get_id(), "preparation");
view.setEndKeyArray(session.get_id(), "preparation", "{}");
return getQuestions(view, session);
}
@Override
public List<Question> getAllSkillQuestions(final Session session) {
final View view = new View("content/doc_by_sessionid_variant_active");
view.setStartKeyArray(session.get_id());
view.setEndKeyArray(session.get_id(), "{}");
return getQuestions(view, session);
}
private List<Question> getQuestions(final View view, final Session session) {
final ViewResults viewResults = getDatabase().view(view);
if (viewResults == null || viewResults.isEmpty()) {
return null;
}
final List<Question> questions = new ArrayList<>();
Results<Question> results = getDatabase().queryView(view, Question.class);
for (final RowResult<Question> row : results.getRows()) {
Question question = row.getValue();
question.updateRoundManagementState();
question.setSessionKeyword(session.getKeyword());
if (!"freetext".equals(question.getQuestionType()) && 0 == question.getPiRound()) {
/* needed for legacy questions whose piRound property has not been set */
question.setPiRound(1);
}
questions.add(question);
}
return questions;
}
@Override
public int getLectureQuestionCount(final Session session) {
final View view = new View("content/by_sessionid_variant_active");
view.setStartKeyArray(session.get_id(), "lecture");
view.setEndKeyArray(session.get_id(), "lecture", "{}");
return getQuestionCount(view);
}
@Override
public int getFlashcardCount(final Session session) {
final View view = new View("content/by_sessionid_variant_active");
view.setStartKeyArray(session.get_id(), "flashcard");
view.setEndKeyArray(session.get_id(), "flashcard", "{}");
return getQuestionCount(view);
}
@Override
public int getPreparationQuestionCount(final Session session) {
final View view = new View("content/by_sessionid_variant_active");
view.setStartKeyArray(session.get_id(), "preparation");
view.setEndKeyArray(session.get_id(), "preparation", "{}");
return getQuestionCount(view);
}
private int getQuestionCount(final View view) {
view.setReduce(true);
final ViewResults results = getDatabase().view(view);
if (results.getJSONArray("rows").optJSONObject(0) == null) {
return 0;
}
return results.getJSONArray("rows").optJSONObject(0).optInt("value");
}
@Override
public int countLectureQuestionAnswers(final Session session) {
return countQuestionVariantAnswers(session, "lecture");
}
@Override
public int countPreparationQuestionAnswers(final Session session) {
return countQuestionVariantAnswers(session, "preparation");
}
private int countQuestionVariantAnswers(final Session session, final String variant) {
final View view = new View("answer/by_sessionid_variant");
view.setKey(session.get_id(), variant);
view.setReduce(true);
final ViewResults results = getDatabase().view(view);
if (results.getResults().isEmpty()) {
return 0;
}
return results.getJSONArray("rows").optJSONObject(0).optInt("value");
}
/* TODO: Only evict cache entry for the answer's question. This requires some refactoring. */
@Caching(evict = { @CacheEvict(value = "questions", allEntries = true),
@CacheEvict("skillquestions"),
@CacheEvict("lecturequestions"),
@CacheEvict(value = "answers", allEntries = true)})
@Override
public int[] deleteAllLectureQuestionsWithAnswers(final Session session) {
final View view = new View("content/by_sessionid_variant_active");
view.setStartKeyArray(session.get_id(), "lecture");
view.setEndKey(session.get_id(), "lecture", "{}");
return deleteAllQuestionDocumentsWithAnswers(view);
}
/* TODO: Only evict cache entry for the answer's question. This requires some refactoring. */
@Caching(evict = { @CacheEvict(value = "questions", allEntries = true),
@CacheEvict("skillquestions"),
@CacheEvict("flashcardquestions"),
@CacheEvict(value = "answers", allEntries = true)})
@Override
public int[] deleteAllFlashcardsWithAnswers(final Session session) {
final View view = new View("content/by_sessionid_variant_active");
view.setStartKeyArray(session.get_id(), "flashcard");
view.setEndKey(session.get_id(), "flashcard", "{}");
return deleteAllQuestionDocumentsWithAnswers(view);
}
/* TODO: Only evict cache entry for the answer's question. This requires some refactoring. */
@Caching(evict = { @CacheEvict(value = "questions", allEntries = true),
@CacheEvict("skillquestions"),
@CacheEvict("preparationquestions"),
@CacheEvict(value = "answers", allEntries = true)})
@Override
public int[] deleteAllPreparationQuestionsWithAnswers(final Session session) {
final View view = new View("content/by_sessionid_variant_active");
view.setStartKeyArray(session.get_id(), "preparation");
view.setEndKey(session.get_id(), "preparation", "{}");
return deleteAllQuestionDocumentsWithAnswers(view);
}
@Override
public List<String> getUnAnsweredLectureQuestionIds(final Session session, final User user) {
final View view = new View("answer/questionid_piround_by_user_sessionid_variant");
view.setKey(user.getUsername(), session.get_id(), "lecture");
return collectUnansweredQuestionIdsByPiRound(getDatabaseDao().getLectureQuestionsForUsers(session), view);
}
@Override
public List<String> getUnAnsweredPreparationQuestionIds(final Session session, final User user) {
final View view = new View("answer/questionid_piround_by_user_sessionid_variant");
view.setKey(user.getUsername(), session.get_id(), "preparation");
return collectUnansweredQuestionIdsByPiRound(getDatabaseDao().getPreparationQuestionsForUsers(session), view);
}
private List<String> collectUnansweredQuestionIds(
final List<String> questions,
final View view
) {
final ViewResults answeredQuestions = getDatabase().view(view);
final List<String> answered = new ArrayList<>();
for (final Document d : answeredQuestions.getResults()) {
answered.add(d.getString("value"));
}
final List<String> unanswered = new ArrayList<>();
for (final String questionId : questions) {
if (!answered.contains(questionId)) {
unanswered.add(questionId);
}
}
return unanswered;
}
private List<String> collectUnansweredQuestionIdsByPiRound(
final List<Question> questions,
final View view
) {
final ViewResults answeredQuestions = getDatabase().view(view);
final Map<String, Integer> answered = new HashMap<>();
for (final Document d : answeredQuestions.getResults()) {
answered.put(d.getJSONArray("value").getString(0), d.getJSONArray("value").getInt(1));
}
final List<String> unanswered = new ArrayList<>();
for (final Question question : questions) {
if (!"slide".equals(question.getQuestionType()) && (!answered.containsKey(question.get_id())
|| (answered.containsKey(question.get_id()) && answered.get(question.get_id()) != question.getPiRound()))) {
unanswered.add(question.get_id());
}
}
return unanswered;
}
private List<String> collectQuestionIds(final View view) {
final ViewResults results = getDatabase().view(view);
if (results.getResults().isEmpty()) {
return new ArrayList<>();
}
final List<String> ids = new ArrayList<>();
for (final Document d : results.getResults()) {
ids.add(d.getId());
}
return ids;
}
@Override
public int deleteAllInterposedQuestions(final Session session) {
final View view = new View("comment/by_sessionid");
view.setKey(session.get_id());
final ViewResults questions = getDatabase().view(view);
return deleteAllInterposedQuestions(session, questions);
}
@Override
public int deleteAllInterposedQuestions(final Session session, final User user) {
final View view = new View("comment/by_sessionid_creator_read");
view.setStartKeyArray(session.get_id(), user.getUsername());
view.setEndKeyArray(session.get_id(), user.getUsername(), "{}");
final ViewResults questions = getDatabase().view(view);
return deleteAllInterposedQuestions(session, questions);
}
private int deleteAllInterposedQuestions(final Session session, final ViewResults questions) {
if (questions == null || questions.isEmpty()) {
return 0;
}
List<Document> results = questions.getResults();
/* TODO: use bulk delete */
for (final Document document : results) {
try {
deleteDocument(document.getId());
} catch (final IOException e) {
logger.error("Could not delete all interposed questions {}.", session, e);
}
}
/* This does account for failed deletions */
log("delete", "type", "comment", "commentCount", results.size());
return results.size();
}
@Override
public List<Question> publishAllQuestions(final Session session, final boolean publish) {
final View view = new View("content/doc_by_sessionid_variant_active");
view.setStartKeyArray(session.get_id());
view.setEndKeyArray(session.get_id(), "{}");
final List<Question> questions = getQuestions(view, session);
getDatabaseDao().publishQuestions(session, publish, questions);
return questions;
}
@Caching(evict = { @CacheEvict(value = "questions", allEntries = true),
@CacheEvict(value = "skillquestions", key = "#session"),
@CacheEvict(value = "lecturequestions", key = "#session"),
@CacheEvict(value = "preparationquestions", key = "#session"),
@CacheEvict(value = "flashcardquestions", key = "#session") })
@Override
public void publishQuestions(final Session session, final boolean publish, List<Question> questions) {
for (final Question q : questions) {
q.setActive(publish);
}
final List<Document> documents = new ArrayList<>();
for (final Question q : questions) {
final Document d = toQuestionDocument(session, q);
d.setId(q.get_id());
d.setRev(q.get_rev());
documents.add(d);
}
try {
database.bulkSaveDocuments(documents.toArray(new Document[documents.size()]));
} catch (final IOException e) {
logger.error("Could not bulk publish all questions.", e);
}
}
@Override
public List<Question> setVotingAdmissionForAllQuestions(final Session session, final boolean disableVoting) {
final View view = new View("content/doc_by_sessionid_variant_active");
view.setStartKeyArray(session.get_id());
view.setEndKeyArray(session.get_id(), "{}");
final List<Question> questions = getQuestions(view, session);
getDatabaseDao().setVotingAdmissions(session, disableVoting, questions);
return questions;
}
@Caching(evict = { @CacheEvict(value = "questions", allEntries = true),
@CacheEvict(value = "skillquestions", key = "#session"),
@CacheEvict(value = "lecturequestions", key = "#session"),
@CacheEvict(value = "preparationquestions", key = "#session"),
@CacheEvict(value = "flashcardquestions", key = "#session") })
@Override
public void setVotingAdmissions(final Session session, final boolean disableVoting, List<Question> questions) {
for (final Question q : questions) {
if (!"flashcard".equals(q.getQuestionType())) {
q.setVotingDisabled(disableVoting);
}
}
final List<Document> documents = new ArrayList<>();
for (final Question q : questions) {
final Document d = toQuestionDocument(session, q);
d.setId(q.get_id());
d.setRev(q.get_rev());
documents.add(d);
}
try {
database.bulkSaveDocuments(documents.toArray(new Document[documents.size()]));
} catch (final IOException e) {
logger.error("Could not bulk set voting admission for all questions.", e);
}
}
/* TODO: Only evict cache entry for the answer's question. This requires some refactoring. */
@CacheEvict(value = "answers", allEntries = true)
@Override
public int deleteAllQuestionsAnswers(final Session session) {
final View view = new View("content/doc_by_sessionid_variant_active");
view.setStartKeyArray(session.get_id());
view.setEndKeyArray(session.get_id(), "{}");
final List<Question> questions = getQuestions(view, session);
getDatabaseDao().resetQuestionsRoundState(session, questions);
return deleteAllAnswersForQuestions(questions);
}
/* TODO: Only evict cache entry for the answer's question. This requires some refactoring. */
@CacheEvict(value = "answers", allEntries = true)
@Override
public int deleteAllPreparationAnswers(final Session session) {
final View view = new View("content/doc_by_sessionid_variant_active");
view.setStartKeyArray(session.get_id(), "preparation");
view.setEndKeyArray(session.get_id(), "preparation", "{}");
final List<Question> questions = getQuestions(view, session);
getDatabaseDao().resetQuestionsRoundState(session, questions);
return deleteAllAnswersForQuestions(questions);
}
/* TODO: Only evict cache entry for the answer's question. This requires some refactoring. */
@CacheEvict(value = "answers", allEntries = true)
@Override
public int deleteAllLectureAnswers(final Session session) {
final View view = new View("content/doc_by_sessionid_variant_active");
view.setStartKeyArray(session.get_id(), "lecture");
view.setEndKeyArray(session.get_id(), "lecture", "{}");
final List<Question> questions = getQuestions(view, session);
getDatabaseDao().resetQuestionsRoundState(session, questions);
return deleteAllAnswersForQuestions(questions);
}
@Caching(evict = { @CacheEvict(value = "questions", allEntries = true),
@CacheEvict(value = "skillquestions", key = "#session"),
@CacheEvict(value = "lecturequestions", key = "#session"),
@CacheEvict(value = "preparationquestions", key = "#session"),
@CacheEvict(value = "flashcardquestions", key = "#session") })
@Override
public void resetQuestionsRoundState(final Session session, List<Question> questions) {
for (final Question q : questions) {
q.resetQuestionState();
}
final List<Document> documents = new ArrayList<>();
for (final Question q : questions) {
final Document d = toQuestionDocument(session, q);
d.setId(q.get_id());
d.setRev(q.get_rev());
documents.add(d);
}
try {
database.bulkSaveDocuments(documents.toArray(new Document[documents.size()]));
} catch (final IOException e) {
logger.error("Could not bulk reset all questions round state.", e);
}
}
private int deleteAllAnswersForQuestions(List<Question> questions) {
List<String> questionIds = new ArrayList<>();
for (Question q : questions) {
questionIds.add(q.get_id());
}
final View bulkView = new View("answer/by_questionid");
bulkView.setKeys(questionIds);
bulkView.setIncludeDocs(true);
final List<Document> result = getDatabase().view(bulkView).getResults();
final List<Document> allAnswers = new ArrayList<>();
for (Document a : result) {
final Document d = new Document(a.getJSONObject("doc"));
d.put("_deleted", true);
allAnswers.add(d);
}
try {
getDatabase().bulkSaveDocuments(allAnswers.toArray(new Document[allAnswers.size()]));
return allAnswers.size();
} catch (IOException e) {
logger.error("Could not bulk delete answers.", e);
}
return 0;
}
private int[] deleteAllAnswersWithQuestions(List<Question> questions) {
List<String> questionIds = new ArrayList<>();
final List<Document> allQuestions = new ArrayList<>();
for (Question q : questions) {
final Document d = new Document();
d.put("_id", q.get_id());
d.put("_rev", q.get_rev());
d.put("_deleted", true);
questionIds.add(q.get_id());
allQuestions.add(d);
}
final View bulkView = new View("answer/by_questionid");
bulkView.setKeys(questionIds);
bulkView.setIncludeDocs(true);
final List<Document> result = getDatabase().view(bulkView).getResults();
final List<Document> allAnswers = new ArrayList<>();
for (Document a : result) {
final Document d = new Document(a.getJSONObject("doc"));
d.put("_deleted", true);
allAnswers.add(d);
}
try {
List<Document> deleteList = new ArrayList<>(allAnswers);
deleteList.addAll(allQuestions);
getDatabase().bulkSaveDocuments(deleteList.toArray(new Document[deleteList.size()]));
return new int[] {deleteList.size(), result.size()};
} catch (IOException e) {
logger.error("Could not bulk delete questions and answers.", e);
}
return new int[] {0, 0};
}
@Cacheable("learningprogress")
@Override
public CourseScore getLearningProgress(final Session session) {
final View maximumValueView = new View("learning_progress/maximum_value_of_question");
final View answerSumView = new View("learning_progress/question_value_achieved_for_user");
maximumValueView.setStartKeyArray(session.get_id());
maximumValueView.setEndKeyArray(session.get_id(), "{}");
answerSumView.setStartKeyArray(session.get_id());
answerSumView.setEndKeyArray(session.get_id(), "{}");
final List<Document> maximumValueResult = getDatabase().view(maximumValueView).getResults();
final List<Document> answerSumResult = getDatabase().view(answerSumView).getResults();
CourseScore courseScore = new CourseScore();
// no results found
if (maximumValueResult.isEmpty() && answerSumResult.isEmpty()) {
return courseScore;
}
// collect mapping (questionId -> max value)
for (Document d : maximumValueResult) {
String questionId = d.getJSONArray("key").getString(1);
JSONObject value = d.getJSONObject("value");
int questionScore = value.getInt("value");
String questionVariant = value.getString("questionVariant");
int piRound = value.getInt("piRound");
courseScore.addQuestion(questionId, questionVariant, piRound, questionScore);
}
// collect mapping (questionId -> (user -> value))
for (Document d : answerSumResult) {
String username = d.getJSONArray("key").getString(1);
JSONObject value = d.getJSONObject("value");
String questionId = value.getString("questionId");
int userscore = value.getInt("score");
int piRound = value.getInt("piRound");
courseScore.addAnswer(questionId, piRound, username, userscore);
}
return courseScore;
}
@Override
public DbUser createOrUpdateUser(DbUser user) {
try {
String id = user.getId();
String rev = user.getRev();
Document d = new Document();
if (null != id) {
d = database.getDocument(id, rev);
}
d.put("type", "userdetails");
d.put("username", user.getUsername());
d.put("password", user.getPassword());
d.put("activationKey", user.getActivationKey());
d.put("passwordResetKey", user.getPasswordResetKey());
d.put("passwordResetTime", user.getPasswordResetTime());
d.put("creation", user.getCreation());
d.put("lastLogin", user.getLastLogin());
database.saveDocument(d, id);
user.setId(d.getId());
user.setRev(d.getRev());
return user;
} catch (IOException e) {
logger.error("Could not save user {}.", user, e);
}
return null;
}
@Override
public DbUser getUser(String username) {
View view = new View("user/doc_by_username");
view.setKey(username);
ViewResults results = this.getDatabase().view(view);
if (results.getJSONArray("rows").optJSONObject(0) == null) {
return null;
}
return (DbUser) JSONObject.toBean(
results.getJSONArray("rows").optJSONObject(0).optJSONObject("value"),
DbUser.class
);
}
@Override
public boolean deleteUser(final DbUser dbUser) {
try {
this.deleteDocument(dbUser.getId());
log("delete", "type", "user", "id", dbUser.getId());
return true;
} catch (IOException e) {
logger.error("Could not delete user {}.", dbUser.getId(), e);
}
return false;
}
@Override
public int deleteInactiveUsers(long lastActivityBefore) {
try {
View view = new View("user/by_creation_for_inactive");
view.setEndKey(lastActivityBefore);
List<Document> results = this.getDatabase().view(view).getResults();
int count = 0;
final List<List<Document>> partitions = Lists.partition(results, BULK_PARTITION_SIZE);
for (List<Document> partition: partitions) {
final List<Document> newDocs = new ArrayList<>();
for (Document oldDoc : partition) {
final Document newDoc = new Document();
newDoc.setId(oldDoc.getId());
newDoc.setRev(oldDoc.getJSONObject("value").getString("_rev"));
newDoc.put("_deleted", true);
newDocs.add(newDoc);
logger.debug("Marked user document {} for deletion.", oldDoc.getId());
}
if (newDocs.size() > 0) {
if (getDatabase().bulkSaveDocuments(newDocs.toArray(new Document[newDocs.size()]))) {
count += newDocs.size();
}
}
}
if (count > 0) {
logger.info("Deleted {} inactive users.", count);
log("cleanup", "type", "user", "count", count);
}
return count;
} catch (IOException e) {
logger.error("Could not delete inactive users.", e);
}
return 0;
}
@Override
public SessionInfo importSession(User user, ImportExportSession importSession) {
final Session session = this.saveSession(user, importSession.generateSessionEntity(user));
List<Document> questions = new ArrayList<>();
// We need to remember which answers belong to which question.
// The answers need a questionId, so we first store the questions to get the IDs.
// Then we update the answer objects and store them as well.
Map<Document, ImportExportQuestion> mapping = new HashMap<>();
// Later, generate all answer documents
List<Document> answers = new ArrayList<>();
// We can then push answers together with interposed questions in one large bulk request
List<Document> interposedQuestions = new ArrayList<>();
// Motds shouldn't be forgotten, too
List<Document> motds = new ArrayList<>();
try {
// add session id to all questions and generate documents
for (ImportExportQuestion question : importSession.getQuestions()) {
Document doc = toQuestionDocument(session, question);
question.setSessionId(session.get_id());
questions.add(doc);
mapping.put(doc, question);
}
database.bulkSaveDocuments(questions.toArray(new Document[questions.size()]));
// bulk import answers together with interposed questions
for (Entry<Document, ImportExportQuestion> entry : mapping.entrySet()) {
final Document doc = entry.getKey();
final ImportExportQuestion question = entry.getValue();
question.set_id(doc.getId());
question.set_rev(doc.getRev());
for (de.thm.arsnova.entities.transport.Answer answer : question.getAnswers()) {
final Answer a = answer.generateAnswerEntity(user, question);
final Document answerDoc = new Document();
answerDoc.put("type", "skill_question_answer");
answerDoc.put("sessionId", a.getSessionId());
answerDoc.put("questionId", a.getQuestionId());
answerDoc.put("answerSubject", a.getAnswerSubject());
answerDoc.put("questionVariant", a.getQuestionVariant());
answerDoc.put("questionValue", a.getQuestionValue());
answerDoc.put("answerText", a.getAnswerText());
answerDoc.put("answerTextRaw", a.getAnswerTextRaw());
answerDoc.put("timestamp", a.getTimestamp());
answerDoc.put("piRound", a.getPiRound());
answerDoc.put("abstention", a.isAbstention());
answerDoc.put("successfulFreeTextAnswer", a.isSuccessfulFreeTextAnswer());
// we do not store the user's name
answerDoc.put("user", "");
answers.add(answerDoc);
}
}
for (de.thm.arsnova.entities.transport.InterposedQuestion i : importSession.getFeedbackQuestions()) {
final Document q = new Document();
q.put("type", "interposed_question");
q.put("sessionId", session.get_id());
q.put("subject", i.getSubject());
q.put("text", i.getText());
q.put("timestamp", i.getTimestamp());
q.put("read", i.isRead());
// we do not store the creator's name
q.put("creator", "");
interposedQuestions.add(q);
}
for (Motd m : importSession.getMotds()) {
final Document d = new Document();
d.put("type", "motd");
d.put("motdkey", m.getMotdkey());
d.put("title", m.getTitle());
d.put("text", m.getText());
d.put("audience", m.getAudience());
d.put("sessionkey", session.getKeyword());
d.put("startdate", String.valueOf(m.getStartdate().getTime()));
d.put("enddate", String.valueOf(m.getEnddate().getTime()));
motds.add(d);
}
List<Document> documents = new ArrayList<>(answers);
database.bulkSaveDocuments(interposedQuestions.toArray(new Document[interposedQuestions.size()]));
database.bulkSaveDocuments(motds.toArray(new Document[motds.size()]));
database.bulkSaveDocuments(documents.toArray(new Document[documents.size()]));
} catch (IOException e) {
logger.error("Could not import session.", e);
// Something went wrong, delete this session since we do not want a partial import
this.deleteSession(session);
return null;
}
return this.calculateSessionInfo(importSession, session);
}
@Override
public ImportExportSession exportSession(String sessionkey, Boolean withAnswers, Boolean withFeedbackQuestions) {
ImportExportSession importExportSession = new ImportExportSession();
Session session = getDatabaseDao().getSessionFromKeyword(sessionkey);
importExportSession.setSessionFromSessionObject(session);
List<Question> questionList = getDatabaseDao().getAllSkillQuestions(session);
for (Question question : questionList) {
List<de.thm.arsnova.entities.transport.Answer> answerList = new ArrayList<>();
if (withAnswers) {
for (Answer a : this.getDatabaseDao().getAllAnswers(question)) {
de.thm.arsnova.entities.transport.Answer transportAnswer = new de.thm.arsnova.entities.transport.Answer(a);
answerList.add(transportAnswer);
}
// getAllAnswers does not grep for whole answer object so i need to add empty entries for abstentions
int i = this.getDatabaseDao().getAbstentionAnswerCount(question.get_id());
for (int b = 0; b < i; b++) {
de.thm.arsnova.entities.transport.Answer ans = new de.thm.arsnova.entities.transport.Answer();
ans.setAnswerSubject("");
ans.setAnswerImage("");
ans.setAnswerText("");
ans.setAbstention(true);
answerList.add(ans);
}
}
importExportSession.addQuestionWithAnswers(question, answerList);
}
if (withFeedbackQuestions) {
List<de.thm.arsnova.entities.transport.InterposedQuestion> interposedQuestionList = new ArrayList<>();
for (InterposedQuestion i : getDatabaseDao().getInterposedQuestions(session, 0, 0)) {
de.thm.arsnova.entities.transport.InterposedQuestion transportInterposedQuestion = new de.thm.arsnova.entities.transport.InterposedQuestion(i);
interposedQuestionList.add(transportInterposedQuestion);
}
importExportSession.setFeedbackQuestions(interposedQuestionList);
}
if (withAnswers) {
importExportSession.setSessionInfo(this.calculateSessionInfo(importExportSession, session));
}
importExportSession.setMotds(getDatabaseDao().getMotdsForSession(session.getKeyword()));
return importExportSession;
}
private SessionInfo calculateSessionInfo(ImportExportSession importExportSession, Session session) {
int unreadInterposed = 0;
int numUnanswered = 0;
int numAnswers = 0;
for (de.thm.arsnova.entities.transport.InterposedQuestion i : importExportSession.getFeedbackQuestions()) {
if (!i.isRead()) {
unreadInterposed++;
}
}
for (ImportExportQuestion question : importExportSession.getQuestions()) {
numAnswers += question.getAnswers().size();
if (question.getAnswers().isEmpty()) {
numUnanswered++;
}
}
final SessionInfo info = new SessionInfo(session);
info.setNumQuestions(importExportSession.getQuestions().size());
info.setNumUnanswered(numUnanswered);
info.setNumAnswers(numAnswers);
info.setNumInterposed(importExportSession.getFeedbackQuestions().size());
info.setNumUnredInterposed(unreadInterposed);
return info;
}
@Override
public List<String> getSubjects(Session session, String questionVariant) {
final View view = new View("content/by_sessionid_variant_active");
view.setStartKeyArray(session.get_id(), questionVariant);
view.setEndKeyArray(session.get_id(), questionVariant, "{}");
ViewResults results = this.getDatabase().view(view);
if (results.getJSONArray("rows").optJSONObject(0) == null) {
return null;
}
Set<String> uniqueSubjects = new HashSet<>();
for (final Document d : results.getResults()) {
uniqueSubjects.add(d.getJSONArray("key").getString(3));
}
return new ArrayList<>(uniqueSubjects);
}
/* TODO: remove if this method is no longer used */
@Override
public List<String> getQuestionIdsBySubject(Session session, String questionVariant, String subject) {
final View view = new View("content/by_sessionid_variant_active");
view.setStartKeyArray(session.get_id(), questionVariant, 1, subject);
view.setEndKeyArray(session.get_id(), questionVariant, 1, subject, "{}");
ViewResults results = this.getDatabase().view(view);
if (results.getJSONArray("rows").optJSONObject(0) == null) {
return null;
}
List<String> qids = new ArrayList<>();
for (final Document d : results.getResults()) {
final String s = d.getId();
qids.add(s);
}
return qids;
}
@Override
public List<Question> getQuestionsByIds(List<String> ids, final Session session) {
View view = new View("_all_docs");
view.setKeys(ids);
view.setIncludeDocs(true);
final List<Document> questiondocs = getDatabase().view(view).getResults();
if (questiondocs == null || questiondocs.isEmpty()) {
return null;
}
final List<Question> result = new ArrayList<>();
final MorpherRegistry morpherRegistry = JSONUtils.getMorpherRegistry();
final Morpher dynaMorpher = new BeanMorpher(PossibleAnswer.class, morpherRegistry);
morpherRegistry.registerMorpher(dynaMorpher);
for (final Document document : questiondocs) {
if (!"".equals(document.optString("error"))) {
// Skip documents we could not load. Maybe they were deleted.
continue;
}
final Question question = (Question) JSONObject.toBean(
document.getJSONObject().getJSONObject("doc"),
Question.class
);
@SuppressWarnings("unchecked")
final Collection<PossibleAnswer> answers = JSONArray.toCollection(
document.getJSONObject().getJSONObject("doc").getJSONArray("possibleAnswers"),
PossibleAnswer.class
);
question.setPossibleAnswers(new ArrayList<>(answers));
question.setSessionKeyword(session.getKeyword());
if (!"freetext".equals(question.getQuestionType()) && 0 == question.getPiRound()) {
/* needed for legacy questions whose piRound property has not been set */
question.setPiRound(1);
}
if (question.getImage() != null) {
question.setImage("true");
}
result.add(question);
}
return result;
}
@Override
public List<Motd> getAdminMotds() {
final View view = new View("motd/doc_by_audience_for_global");
return getMotds(view);
}
@Override
@Cacheable(cacheNames = "motds", key = "'all'")
public List<Motd> getMotdsForAll() {
final View view = new View("motd/doc_by_audience_for_global");
return getMotds(view);
}
@Override
@Cacheable(cacheNames = "motds", key = "'loggedIn'")
public List<Motd> getMotdsForLoggedIn() {
final View view = new View("motd/doc_by_audience_for_global");
view.setKey("loggedIn");
return getMotds(view);
}
@Override
@Cacheable(cacheNames = "motds", key = "'tutors'")
public List<Motd> getMotdsForTutors() {
final View view1 = new View("motd/doc_by_audience_for_global");
final View view2 = new View("motd/doc_by_audience_for_global");
view1.setKey("loggedIn");
view2.setKey("tutors");
final List<Motd> union = new ArrayList<>();
union.addAll(getMotds(view1));
union.addAll(getMotds(view2));
return union;
}
@Override
@Cacheable(cacheNames = "motds", key = "'students'")
public List<Motd> getMotdsForStudents() {
final View view1 = new View("motd/doc_by_audience_for_global");
final View view2 = new View("motd/doc_by_audience_for_global");
view1.setKey("loggedIn");
view2.setKey("students");
final List<Motd> union = new ArrayList<>();
union.addAll(getMotds(view1));
union.addAll(getMotds(view2));
return union;
}
@Override
@Cacheable(cacheNames = "motds", key = "('session').concat(#p0)")
public List<Motd> getMotdsForSession(final String sessionkey) {
final View view = new View("motd/doc_by_sessionkey");
view.setKey(sessionkey);
return getMotds(view);
}
@Override
public List<Motd> getMotds(View view) {
final ViewResults motddocs = this.getDatabase().view(view);
List<Motd> motdlist = new ArrayList<>();
for (final Document d : motddocs.getResults()) {
Motd motd = new Motd();
motd.set_id(d.getId());
motd.set_rev(d.getJSONObject("value").getString("_rev"));
motd.setMotdkey(d.getJSONObject("value").getString("motdkey"));
Date start = new Date(Long.parseLong(d.getJSONObject("value").getString("startdate")));
motd.setStartdate(start);
Date end = new Date(Long.parseLong(d.getJSONObject("value").getString("enddate")));
motd.setEnddate(end);
motd.setTitle(d.getJSONObject("value").getString("title"));
motd.setText(d.getJSONObject("value").getString("text"));
motd.setAudience(d.getJSONObject("value").getString("audience"));
motd.setSessionkey(d.getJSONObject("value").getString("sessionkey"));
motdlist.add(motd);
}
return motdlist;
}
@Override
public Motd getMotdByKey(String key) {
final View view = new View("motd/by_motdkey");
view.setIncludeDocs(true);
view.setKey(key);
Motd motd = new Motd();
ViewResults results = this.getDatabase().view(view);
for (final Document d : results.getResults()) {
motd.set_id(d.getId());
motd.set_rev(d.getJSONObject("doc").getString("_rev"));
motd.setMotdkey(d.getJSONObject("doc").getString("motdkey"));
Date start = new Date(Long.parseLong(d.getJSONObject("doc").getString("startdate")));
motd.setStartdate(start);
Date end = new Date(Long.parseLong(d.getJSONObject("doc").getString("enddate")));
motd.setEnddate(end);
motd.setTitle(d.getJSONObject("doc").getString("title"));
motd.setText(d.getJSONObject("doc").getString("text"));
motd.setAudience(d.getJSONObject("doc").getString("audience"));
motd.setSessionkey(d.getJSONObject("doc").getString("sessionkey"));
}
return motd;
}
@Override
@CacheEvict(cacheNames = "motds", key = "#p0.audience.concat(#p0.sessionkey)")
public Motd createOrUpdateMotd(Motd motd) {
try {
String id = motd.get_id();
String rev = motd.get_rev();
Document d = new Document();
if (null != id) {
d = database.getDocument(id, rev);
} else {
motd.setMotdkey(sessionService.generateKeyword());
d.put("motdkey", motd.getMotdkey());
}
d.put("type", "motd");
d.put("startdate", String.valueOf(motd.getStartdate().getTime()));
d.put("enddate", String.valueOf(motd.getEnddate().getTime()));
d.put("title", motd.getTitle());
d.put("text", motd.getText());
d.put("audience", motd.getAudience());
d.put("sessionId", motd.getSessionId());
d.put("sessionkey", motd.getSessionkey());
database.saveDocument(d, id);
motd.set_id(d.getId());
motd.set_rev(d.getRev());
return motd;
} catch (IOException e) {
logger.error("Could not save MotD {}.", motd, e);
}
return null;
}
@Override
@CacheEvict(cacheNames = "motds", key = "#p0.audience.concat(#p0.sessionkey)")
public void deleteMotd(Motd motd) {
try {
this.deleteDocument(motd.get_id());
} catch (IOException e) {
logger.error("Could not delete MotD {}.", motd.get_id(), e);
}
}
@Override
@Cacheable(cacheNames = "motdlist", key = "#p0")
public MotdList getMotdListForUser(final String username) {
View view = new View("motdlist/doc_by_username");
view.setKey(username);
ViewResults results = this.getDatabase().view(view);
MotdList motdlist = new MotdList();
for (final Document d : results.getResults()) {
motdlist.set_id(d.getId());
motdlist.set_rev(d.getJSONObject("value").getString("_rev"));
motdlist.setUsername(d.getJSONObject("value").getString("username"));
motdlist.setMotdkeys(d.getJSONObject("value").getString("motdkeys"));
}
return motdlist;
}
@Override
@CachePut(cacheNames = "motdlist", key = "#p0.username")
public MotdList createOrUpdateMotdList(MotdList motdlist) {
try {
String id = motdlist.get_id();
String rev = motdlist.get_rev();
Document d = new Document();
if (null != id) {
d = database.getDocument(id, rev);
}
d.put("type", "motdlist");
d.put("username", motdlist.getUsername());
d.put("motdkeys", motdlist.getMotdkeys());
database.saveDocument(d, id);
motdlist.set_id(d.getId());
motdlist.set_rev(d.getRev());
return motdlist;
} catch (IOException e) {
logger.error("Could not save MotD list {}.", motdlist, e);
}
return null;
}
}