diff --git a/src/main/java/de/thm/arsnova/config/AppConfig.java b/src/main/java/de/thm/arsnova/config/AppConfig.java index 16db42433580ec2d1ced4da85b1cfc0954017d4a..49297579d48ab90dbfefeff778afce060688eb3c 100644 --- a/src/main/java/de/thm/arsnova/config/AppConfig.java +++ b/src/main/java/de/thm/arsnova/config/AppConfig.java @@ -23,6 +23,7 @@ import com.fasterxml.jackson.databind.SerializationFeature; import de.thm.arsnova.ImageUtils; import de.thm.arsnova.connector.client.ConnectorClient; import de.thm.arsnova.connector.client.ConnectorClientImpl; +import de.thm.arsnova.entities.Answer; import de.thm.arsnova.entities.Comment; import de.thm.arsnova.entities.DbUser; import de.thm.arsnova.entities.LogEntry; @@ -32,12 +33,14 @@ import de.thm.arsnova.entities.Session; import de.thm.arsnova.entities.serialization.CouchDbDocumentModule; import de.thm.arsnova.entities.serialization.CouchDbObjectMapperFactory; import de.thm.arsnova.entities.serialization.View; +import de.thm.arsnova.persistance.AnswerRepository; import de.thm.arsnova.persistance.CommentRepository; import de.thm.arsnova.persistance.ContentRepository; import de.thm.arsnova.persistance.LogEntryRepository; import de.thm.arsnova.persistance.MotdRepository; import de.thm.arsnova.persistance.SessionRepository; import de.thm.arsnova.persistance.UserRepository; +import de.thm.arsnova.persistance.couchdb.CouchDbAnswerRepository; import de.thm.arsnova.persistance.couchdb.CouchDbCommentRepository; import de.thm.arsnova.persistance.couchdb.CouchDbContentRepository; import de.thm.arsnova.persistance.couchdb.CouchDbLogEntryRepository; @@ -308,6 +311,11 @@ public class AppConfig extends WebMvcConfigurerAdapter { return new CouchDbContentRepository(Content.class, couchDbConnector(), false); } + @Bean + public AnswerRepository answerRepository() throws Exception { + return new CouchDbAnswerRepository(Answer.class, couchDbConnector(), false); + } + @Bean public UserRepository userRepository() throws Exception { return new CouchDbUserRepository(DbUser.class, couchDbConnector(), false); diff --git a/src/main/java/de/thm/arsnova/dao/CouchDBDao.java b/src/main/java/de/thm/arsnova/dao/CouchDBDao.java index 72ff1ff4fcd1d3ba34af724462c45112e8cc77fc..247d432cb39afe0ed21a8f73f98af00258c2be7e 100644 --- a/src/main/java/de/thm/arsnova/dao/CouchDBDao.java +++ b/src/main/java/de/thm/arsnova/dao/CouchDBDao.java @@ -25,9 +25,6 @@ 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.events.NewAnswerEvent; -import de.thm.arsnova.exceptions.NotFoundException; import de.thm.arsnova.persistance.ContentRepository; import de.thm.arsnova.persistance.LogEntryRepository; import de.thm.arsnova.persistance.MotdRepository; @@ -41,25 +38,18 @@ 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.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 java.io.IOException; -import java.util.AbstractMap; import java.util.ArrayList; -import java.util.HashMap; import java.util.HashSet; import java.util.List; -import java.util.Map; -import java.util.Queue; import java.util.Set; -import java.util.concurrent.ConcurrentLinkedQueue; /** * Database implementation based on CouchDB. @@ -81,7 +71,7 @@ import java.util.concurrent.ConcurrentLinkedQueue; */ @Profile("!test") @Service("databaseDao") -public class CouchDBDao implements IDatabaseDao, ApplicationEventPublisherAware { +public class CouchDBDao implements IDatabaseDao { private static final int BULK_PARTITION_SIZE = 500; @@ -105,10 +95,6 @@ public class CouchDBDao implements IDatabaseDao, ApplicationEventPublisherAware 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}") @@ -142,11 +128,6 @@ public class CouchDBDao implements IDatabaseDao, ApplicationEventPublisherAware return this; } - @Override - public void setApplicationEventPublisher(ApplicationEventPublisher publisher) { - this.publisher = publisher; - } - private Database getDatabase() { if (database == null) { try { @@ -169,61 +150,6 @@ public class CouchDBDao implements IDatabaseDao, ApplicationEventPublisherAware getDatabase().deleteDocument(d); } - @CacheEvict("answers") - @Override - public int deleteAnswers(final Content content) { - try { - final View view = new View("answer/by_questionid"); - view.setKey(content.getId()); - 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."); - } - } - dbLogger.log("delete", "type", "answer", "answerCount", count); - - return count; - } catch (final IOException e) { - logger.error("Could not delete answers for content {}.", content.getId(), e); - } - - return 0; - } - - @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) { @@ -239,176 +165,10 @@ public class CouchDBDao implements IDatabaseDao, ApplicationEventPublisherAware } } - @Override - public List<Answer> getAnswers(final Content content, final int piRound) { - final String questionId = content.getId(); - 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 Content content) { - final String questionId = content.getId(); - 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 Content content) { - return this.getAnswers(content, content.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 Content content, final int piRound) { - final View view = new View("answer/by_questionid_piround_text_subject"); - view.setStartKey(content.getId(), piRound); - view.setEndKey(content.getId(), 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 Content content) { - final View view = new View("answer/by_questionid_piround_text_subject"); - view.setStartKeyArray(content.getId()); - view.setEndKeyArray(content.getId(), "{}"); - 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.getId()); - 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.getId()); - answers.add(a); - } - return answers; - } - - @Override - public int getTotalAnswerCount(final String sessionKey) { - final Session s = sessionRepository.getSessionFromKeyword(sessionKey); - if (s == null) { - throw new NotFoundException(); - } - - final View view = new View("answer/by_sessionid_variant"); - view.setKey(s.getId()); - final ViewResults results = getDatabase().view(view); - if (results.getResults().isEmpty()) { - return 0; - } - return results.getJSONArray("rows").optJSONObject(0).optInt("value"); - } - @Cacheable("statistics") @Override public Statistics getStatistics() { @@ -487,100 +247,6 @@ public class CouchDBDao implements IDatabaseDao, ApplicationEventPublisherAware return stats; } - @CacheEvict(value = "answers", key = "#content") - @Override - public Answer saveAnswer(final Answer answer, final User user, final Content content, 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, content, 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)); - dbLogger.log("delete", "type", "answer"); - } catch (final IOException e) { - logger.error("Could not delete answer {}.", answerId, e); - } - } - /** * Adds convenience methods to CouchDB4J's view class. */ @@ -651,119 +317,6 @@ public class CouchDBDao implements IDatabaseDao, ApplicationEventPublisherAware return 0; } - @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.getId(), 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. */ - @CacheEvict(value = "answers", allEntries = true) - @Override - public int deleteAllQuestionsAnswers(final Session session) { - final List<Content> contents = contentRepository.getQuestions(session.getId()); - contentRepository.resetQuestionsRoundState(session, contents); - - return deleteAllAnswersForQuestions(contents); - } - - /* 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 List<Content> contents = contentRepository.getQuestions(session.getId(), "preparation"); - contentRepository.resetQuestionsRoundState(session, contents); - - return deleteAllAnswersForQuestions(contents); - } - - /* 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 List<Content> contents = contentRepository.getQuestions(session.getId(), "lecture"); - contentRepository.resetQuestionsRoundState(session, contents); - - return deleteAllAnswersForQuestions(contents); - } - - public int deleteAllAnswersForQuestions(List<Content> contents) { - List<String> questionIds = new ArrayList<>(); - for (Content q : contents) { - questionIds.add(q.getId()); - } - 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; - } - - public int[] deleteAllAnswersWithQuestions(List<Content> contents) { - List<String> questionIds = new ArrayList<>(); - final List<Document> allQuestions = new ArrayList<>(); - for (Content q : contents) { - final Document d = new Document(); - d.put("_id", q.getId()); - d.put("_rev", q.getRevision()); - d.put("_deleted", true); - questionIds.add(q.getId()); - 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 contents and answers.", e); - } - - return new int[] {0, 0}; - } - @Cacheable("learningprogress") @Override public CourseScore getLearningProgress(final Session session) { diff --git a/src/main/java/de/thm/arsnova/dao/IDatabaseDao.java b/src/main/java/de/thm/arsnova/dao/IDatabaseDao.java index 532729fdf3fc8d101a9c2700b350209c5876c5f2..8337277a7d243e8857e89c04d1f76fbb548dabb4 100644 --- a/src/main/java/de/thm/arsnova/dao/IDatabaseDao.java +++ b/src/main/java/de/thm/arsnova/dao/IDatabaseDao.java @@ -26,48 +26,11 @@ import java.util.List; * All methods the database must support. */ public interface IDatabaseDao { - Answer getMyAnswer(User me, String questionId, int piRound); - - List<Answer> getAnswers(Content content, int piRound); - - List<Answer> getAnswers(Content content); - - List<Answer> getAllAnswers(Content content); - - int getAnswerCount(Content content, int piRound); - - int getTotalAnswerCountByQuestion(Content content); - - int getAbstentionAnswerCount(String questionId); - - List<Answer> getFreetextAnswers(String questionId, final int start, final int limit); - - List<Answer> getMyAnswers(User me, Session session); - - int getTotalAnswerCount(String sessionKey); - - int deleteAnswers(Content content); - - Answer saveAnswer(Answer answer, User user, Content content, Session session); - - Answer updateAnswer(Answer answer); - - void deleteAnswer(String answerId); int deleteInactiveGuestVisitedSessionLists(long lastActivityBefore); - int countLectureQuestionAnswers(Session session); - - int countPreparationQuestionAnswers(Session session); - - int deleteAllQuestionsAnswers(Session session); - CourseScore getLearningProgress(Session session); - int deleteAllPreparationAnswers(Session session); - - int deleteAllLectureAnswers(Session session); - Statistics getStatistics(); <T> T getObjectFromId(String documentId, Class<T> klass); @@ -75,6 +38,4 @@ public interface IDatabaseDao { MotdList getMotdListForUser(final String username); MotdList createOrUpdateMotdList(MotdList motdlist); - - int[] deleteAllAnswersWithQuestions(List<Content> contents); } diff --git a/src/main/java/de/thm/arsnova/entities/Answer.java b/src/main/java/de/thm/arsnova/entities/Answer.java index 283060bb155cd307944bb5b7037f428bfadd2708..146acaf85f960b692c825dc7baf2b4f5f0fafc54 100644 --- a/src/main/java/de/thm/arsnova/entities/Answer.java +++ b/src/main/java/de/thm/arsnova/entities/Answer.java @@ -18,6 +18,8 @@ package de.thm.arsnova.entities; import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonView; +import de.thm.arsnova.entities.serialization.View; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; @@ -29,10 +31,9 @@ import java.io.Serializable; * This class has additional fields to transport generated answer statistics. */ @ApiModel(value = "Answer", description = "the answer entity") -public class Answer implements Serializable { - - private String _id; - private String _rev; +public class Answer implements Entity { + private String id; + private String rev; private String type; private String sessionId; private String questionId; @@ -58,143 +59,158 @@ public class Answer implements Serializable { } @ApiModelProperty(required = true, value = "the couchDB ID") - public final String get_id() { - return _id; - } - - public final void set_id(String _id) { - this._id = _id; - } - - public final String get_rev() { - return _rev; + @JsonView({View.Persistence.class, View.Public.class}) + public String getId() { + return id; } - public final void set_rev(final String _rev) { - this._rev = _rev; + @JsonView({View.Persistence.class, View.Public.class}) + public void setId(final String id) { + this.id = id; } - @ApiModelProperty(required = true, value = "\"skill_question_answer\" - used to filter in the couchDB") - public final String getType() { - return type; + @JsonView({View.Persistence.class, View.Public.class}) + public void setRevision(final String rev) { + this.rev = rev; } - public final void setType(final String type) { - this.type = type; + @JsonView({View.Persistence.class, View.Public.class}) + public String getRevision() { + return rev; } @ApiModelProperty(required = true, value = "ID of the session, the answer is assigned to") + @JsonView({View.Persistence.class, View.Public.class}) public final String getSessionId() { return sessionId; } + @JsonView({View.Persistence.class, View.Public.class}) public final void setSessionId(final String sessionId) { this.sessionId = sessionId; } @ApiModelProperty(required = true, value = "used to display question id") + @JsonView({View.Persistence.class, View.Public.class}) public final String getQuestionId() { return questionId; } + @JsonView({View.Persistence.class, View.Public.class}) public final void setQuestionId(final String questionId) { this.questionId = questionId; } @ApiModelProperty(required = true, value = "the answer text") + @JsonView({View.Persistence.class, View.Public.class}) public final String getAnswerText() { return answerText; } + @JsonView({View.Persistence.class, View.Public.class}) public final void setAnswerText(final String answerText) { this.answerText = answerText; } + @JsonView({View.Persistence.class, View.Public.class}) public final String getAnswerTextRaw() { return this.answerTextRaw; } + @JsonView({View.Persistence.class, View.Public.class}) public final void setAnswerTextRaw(final String answerTextRaw) { this.answerTextRaw = answerTextRaw; } @ApiModelProperty(required = true, value = "the answer subject") + @JsonView({View.Persistence.class, View.Public.class}) public final String getAnswerSubject() { return answerSubject; } + @JsonView({View.Persistence.class, View.Public.class}) public final void setAnswerSubject(final String answerSubject) { this.answerSubject = answerSubject; } + @JsonView({View.Persistence.class, View.Public.class}) public final boolean isSuccessfulFreeTextAnswer() { return this.successfulFreeTextAnswer; } + @JsonView({View.Persistence.class, View.Public.class}) public final void setSuccessfulFreeTextAnswer(final boolean successfulFreeTextAnswer) { this.successfulFreeTextAnswer = successfulFreeTextAnswer; } @ApiModelProperty(required = true, value = "the peer instruction round nr.") + @JsonView({View.Persistence.class, View.Public.class}) public int getPiRound() { return piRound; } + @JsonView({View.Persistence.class, View.Public.class}) public void setPiRound(int piRound) { this.piRound = piRound; } - /* TODO: use JsonViews instead of JsonIgnore when supported by Spring (4.1) - * http://wiki.fasterxml.com/JacksonJsonViews - * https://jira.spring.io/browse/SPR-7156 */ @ApiModelProperty(required = true, value = "the user") - @JsonIgnore + @JsonView(View.Persistence.class) public final String getUser() { return user; } + @JsonView({View.Persistence.class, View.Public.class}) + public final void setUser(final String user) { + this.user = user; + } + @ApiModelProperty(required = true, value = "the answer image") - @JsonIgnore + @JsonView(View.Persistence.class) public String getAnswerImage() { return answerImage; } + @JsonView({View.Persistence.class, View.Public.class}) public void setAnswerImage(String answerImage) { this.answerImage = answerImage; } @ApiModelProperty(required = true, value = "the answer thumbnail") + @JsonView({View.Persistence.class, View.Public.class}) public String getAnswerThumbnailImage() { return answerThumbnailImage; } + @JsonView({View.Persistence.class, View.Public.class}) public void setAnswerThumbnailImage(String answerThumbnailImage) { this.answerThumbnailImage = answerThumbnailImage; } - public final void setUser(final String user) { - this.user = user; - } - @ApiModelProperty(required = true, value = "the creation date timestamp") + @JsonView({View.Persistence.class, View.Public.class}) public long getTimestamp() { return timestamp; } + @JsonView(View.Persistence.class) public void setTimestamp(long timestamp) { this.timestamp = timestamp; } @ApiModelProperty(required = true, value = "displays whether the answer is read") + @JsonView({View.Persistence.class, View.Public.class}) public boolean isRead() { return read; } + @JsonView({View.Persistence.class, View.Public.class}) public void setRead(boolean read) { this.read = read; } @ApiModelProperty(required = true, value = "the number of answers given. used for statistics") + @JsonView(View.Public.class) public final int getAnswerCount() { return answerCount; } @@ -204,15 +220,18 @@ public class Answer implements Serializable { } @ApiModelProperty(required = true, value = "the abstention") + @JsonView({View.Persistence.class, View.Public.class}) public boolean isAbstention() { return abstention; } + @JsonView({View.Persistence.class, View.Public.class}) public void setAbstention(boolean abstention) { this.abstention = abstention; } @ApiModelProperty(required = true, value = "the number of abstentions given. used for statistics") + @JsonView(View.Public.class) public int getAbstentionCount() { return abstentionCount; } @@ -222,19 +241,23 @@ public class Answer implements Serializable { } @ApiModelProperty(required = true, value = "either lecture or preparation") + @JsonView({View.Persistence.class, View.Public.class}) public String getQuestionVariant() { return questionVariant; } + @JsonView({View.Persistence.class, View.Public.class}) public void setQuestionVariant(String questionVariant) { this.questionVariant = questionVariant; } @ApiModelProperty(required = true, value = "used to display question value") + @JsonView({View.Persistence.class, View.Public.class}) public int getQuestionValue() { return questionValue; } + @JsonView({View.Persistence.class, View.Public.class}) public void setQuestionValue(int questionValue) { this.questionValue = questionValue; } @@ -255,8 +278,8 @@ public class Answer implements Serializable { // auto generated! final int prime = 31; int result = 1; - result = prime * result + ((_id == null) ? 0 : _id.hashCode()); - result = prime * result + ((_rev == null) ? 0 : _rev.hashCode()); + result = prime * result + ((id == null) ? 0 : id.hashCode()); + result = prime * result + ((rev == null) ? 0 : rev.hashCode()); result = prime * result + ((answerSubject == null) ? 0 : answerSubject.hashCode()); result = prime * result + ((answerText == null) ? 0 : answerText.hashCode()); result = prime * result + piRound; @@ -280,18 +303,18 @@ public class Answer implements Serializable { return false; } Answer other = (Answer) obj; - if (_id == null) { - if (other._id != null) { + if (id == null) { + if (other.id != null) { return false; } - } else if (!_id.equals(other._id)) { + } else if (!id.equals(other.id)) { return false; } - if (_rev == null) { - if (other._rev != null) { + if (rev == null) { + if (other.rev != null) { return false; } - } else if (!_rev.equals(other._rev)) { + } else if (!rev.equals(other.rev)) { return false; } if (answerSubject == null) { diff --git a/src/main/java/de/thm/arsnova/entities/serialization/CouchDbTypeFieldConverter.java b/src/main/java/de/thm/arsnova/entities/serialization/CouchDbTypeFieldConverter.java index 7141f02f45cbebeabb7ee15e38aa25b2087f0975..3a3c6c1ebd72cf4603c06c3b06043afce76366c9 100644 --- a/src/main/java/de/thm/arsnova/entities/serialization/CouchDbTypeFieldConverter.java +++ b/src/main/java/de/thm/arsnova/entities/serialization/CouchDbTypeFieldConverter.java @@ -20,6 +20,7 @@ package de.thm.arsnova.entities.serialization; import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.type.TypeFactory; import com.fasterxml.jackson.databind.util.Converter; +import de.thm.arsnova.entities.Answer; import de.thm.arsnova.entities.Comment; import de.thm.arsnova.entities.DbUser; import de.thm.arsnova.entities.Entity; @@ -41,6 +42,7 @@ public class CouchDbTypeFieldConverter implements Converter<Class<? extends Enti typeMapping.put(Session.class, "session"); typeMapping.put(Comment.class, "interposed_question"); typeMapping.put(Content.class, "skill_question"); + typeMapping.put(Answer.class, "skill_question_answer"); } @Override diff --git a/src/main/java/de/thm/arsnova/persistance/AnswerRepository.java b/src/main/java/de/thm/arsnova/persistance/AnswerRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..27693fa2b45a20be0526e70078d2d278f40422aa --- /dev/null +++ b/src/main/java/de/thm/arsnova/persistance/AnswerRepository.java @@ -0,0 +1,49 @@ +/* + * 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.persistance; + +import de.thm.arsnova.entities.Answer; +import de.thm.arsnova.entities.Content; +import de.thm.arsnova.entities.Session; +import de.thm.arsnova.entities.User; + +import java.util.List; + +public interface AnswerRepository { + Answer get(String id); + Answer getMyAnswer(User me, String questionId, int piRound); + List<Answer> getAnswers(Content content, int piRound); + List<Answer> getAnswers(Content content); + List<Answer> getAllAnswers(Content content); + int getAnswerCount(Content content, int piRound); + int getTotalAnswerCountByQuestion(Content content); + int getAbstentionAnswerCount(String questionId); + List<Answer> getFreetextAnswers(String questionId, final int start, final int limit); + List<Answer> getMyAnswers(User me, Session session); + int getTotalAnswerCount(String sessionKey); + int deleteAnswers(Content content); + Answer saveAnswer(Answer answer, User user, Content content, Session session); + Answer updateAnswer(Answer answer); + void deleteAnswer(String answerId); + int countLectureQuestionAnswers(Session session); + int countPreparationQuestionAnswers(Session session); + int deleteAllQuestionsAnswers(Session session); + int deleteAllPreparationAnswers(Session session); + int deleteAllLectureAnswers(Session session); + int[] deleteAllAnswersWithQuestions(List<Content> contents); +} diff --git a/src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbAnswerRepository.java b/src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbAnswerRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..4ebb3273e4c7ba6deac9ecf6ecd9090d860d52d8 --- /dev/null +++ b/src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbAnswerRepository.java @@ -0,0 +1,385 @@ +package de.thm.arsnova.persistance.couchdb; + +import com.fasterxml.jackson.databind.JsonNode; +import com.google.common.collect.Lists; +import de.thm.arsnova.entities.Answer; +import de.thm.arsnova.entities.Content; +import de.thm.arsnova.entities.Session; +import de.thm.arsnova.entities.User; +import de.thm.arsnova.entities.transport.AnswerQueueElement; +import de.thm.arsnova.events.NewAnswerEvent; +import de.thm.arsnova.exceptions.NotFoundException; +import de.thm.arsnova.persistance.AnswerRepository; +import de.thm.arsnova.persistance.ContentRepository; +import de.thm.arsnova.persistance.LogEntryRepository; +import de.thm.arsnova.persistance.SessionRepository; +import org.ektorp.BulkDeleteDocument; +import org.ektorp.ComplexKey; +import org.ektorp.CouchDbConnector; +import org.ektorp.DbAccessException; +import org.ektorp.DocumentOperationResult; +import org.ektorp.UpdateConflictException; +import org.ektorp.ViewResult; +import org.ektorp.support.CouchDbRepositorySupport; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.ApplicationEventPublisherAware; +import org.springframework.scheduling.annotation.Scheduled; + +import java.util.ArrayList; +import java.util.List; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; + +public class CouchDbAnswerRepository extends CouchDbRepositorySupport<Answer> implements AnswerRepository, ApplicationEventPublisherAware { + private static final int BULK_PARTITION_SIZE = 500; + private static final Logger logger = LoggerFactory.getLogger(CouchDbAnswerRepository.class); + + private final Queue<AnswerQueueElement> answerQueue = new ConcurrentLinkedQueue<>(); + + @Autowired + private LogEntryRepository dbLogger; + + @Autowired + private SessionRepository sessionRepository; + + @Autowired + private ContentRepository contentRepository; + + private ApplicationEventPublisher publisher; + + public CouchDbAnswerRepository(Class<Answer> type, CouchDbConnector db, boolean createIfNotExists) { + super(type, db, createIfNotExists); + } + + @Scheduled(fixedDelay = 5000) + public void flushAnswerQueue() { + if (answerQueue.isEmpty()) { + // no need to send an empty bulk request. + return; + } + + final List<Answer> answerList = new ArrayList<>(); + final List<AnswerQueueElement> elements = new ArrayList<>(); + AnswerQueueElement entry; + while ((entry = this.answerQueue.poll()) != null) { + final Answer answer = entry.getAnswer(); + answerList.add(answer); + elements.add(entry); + } + try { + db.executeBulk(answerList); + + // Send NewAnswerEvents ... + for (AnswerQueueElement e : elements) { + this.publisher.publishEvent(new NewAnswerEvent(this, e.getSession(), e.getAnswer(), e.getUser(), e.getQuestion())); + } + } catch (DbAccessException e) { + logger.error("Could not bulk save answers from queue.", e); + } + } + + @Override + public void setApplicationEventPublisher(ApplicationEventPublisher publisher) { + this.publisher = publisher; + } + + @CacheEvict("answers") + @Override + public int deleteAnswers(final Content content) { + try { + final ViewResult result = db.queryView(createQuery("by_questionid") + .key(content.getId())); + final List<List<ViewResult.Row>> partitions = Lists.partition(result.getRows(), BULK_PARTITION_SIZE); + + int count = 0; + for (List<ViewResult.Row> partition: partitions) { + List<BulkDeleteDocument> answersToDelete = new ArrayList<>(); + for (final ViewResult.Row a : partition) { + final BulkDeleteDocument d = new BulkDeleteDocument(a.getId(), a.getValueAsNode().get("_rev").asText()); + answersToDelete.add(d); + } + List<DocumentOperationResult> errors = db.executeBulk(answersToDelete); + count += partition.size() - errors.size(); + if (errors.size() > 0) { + logger.error("Could not bulk delete {} of {} answers.", errors.size(), partition.size()); + } + } + dbLogger.log("delete", "type", "answer", "answerCount", count); + + return count; + } catch (final DbAccessException e) { + logger.error("Could not delete answers for content {}.", content.getId(), e); + } + + return 0; + } + + @Override + public Answer getMyAnswer(final User me, final String questionId, final int piRound) { + final List<Answer> answerList = queryView("by_questionid_user_piround", + ComplexKey.of(questionId, me.getUsername(), piRound)); + return answerList.isEmpty() ? null : answerList.get(0); + } + + @Override + public List<Answer> getAnswers(final Content content, final int piRound) { + final String questionId = content.getId(); + final ViewResult result = db.queryView(createQuery("by_questionid_piround_text_subject") + .group(true) + .startKey(ComplexKey.of(questionId, piRound)) + .endKey(ComplexKey.of(questionId, piRound, ComplexKey.emptyObject()))); + final int abstentionCount = getAbstentionAnswerCount(questionId); + + List<Answer> answers = new ArrayList<>(); + for (final ViewResult.Row d : result) { + final Answer a = new Answer(); + a.setAnswerCount(d.getValueAsInt()); + a.setAbstentionCount(abstentionCount); + a.setQuestionId(d.getKeyAsNode().get(0).asText()); + a.setPiRound(piRound); + final JsonNode answerTextNode = d.getKeyAsNode().get(3); + a.setAnswerText(answerTextNode.isNull() ? null : answerTextNode.asText()); + answers.add(a); + } + + return answers; + } + + @Override + public List<Answer> getAllAnswers(final Content content) { + final String questionId = content.getId(); + final ViewResult result = db.queryView(createQuery("by_questionid_piround_text_subject") + .group(true) + .startKey(ComplexKey.of(questionId)) + .endKey(ComplexKey.of(questionId, ComplexKey.emptyObject()))); + final int abstentionCount = getAbstentionAnswerCount(questionId); + + final List<Answer> answers = new ArrayList<>(); + for (final ViewResult.Row d : result.getRows()) { + final Answer a = new Answer(); + a.setAnswerCount(d.getValueAsInt()); + a.setAbstentionCount(abstentionCount); + a.setQuestionId(d.getKeyAsNode().get(0).asText()); + final JsonNode answerTextNode = d.getKeyAsNode().get(3); + final JsonNode answerSubjectNode = d.getKeyAsNode().get(4); + final boolean successfulFreeTextAnswer = d.getKeyAsNode().get(5).asBoolean(); + a.setAnswerText(answerTextNode.isNull() ? null : answerTextNode.asText()); + a.setAnswerSubject(answerSubjectNode.isNull() ? null : answerSubjectNode.asText()); + a.setSuccessfulFreeTextAnswer(successfulFreeTextAnswer); + answers.add(a); + } + + return answers; + } + + @Cacheable("answers") + @Override + public List<Answer> getAnswers(final Content content) { + return this.getAnswers(content, content.getPiRound()); + } + + @Override + public int getAbstentionAnswerCount(final String questionId) { + final ViewResult result = db.queryView(createQuery("by_questionid_piround_text_subject") + //.group(true) + .startKey(ComplexKey.of(questionId)) + .endKey(ComplexKey.of(questionId, ComplexKey.emptyObject()))); + + return result.isEmpty() ? 0 : result.getRows().get(0).getValueAsInt(); + } + + @Override + public int getAnswerCount(final Content content, final int piRound) { + final ViewResult result = db.queryView(createQuery("by_questionid_piround_text_subject") + //.group(true) + .startKey(ComplexKey.of(content.getId(), piRound)) + .endKey(ComplexKey.of(content.getId(), piRound, ComplexKey.emptyObject()))); + + return result.isEmpty() ? 0 : result.getRows().get(0).getValueAsInt(); + } + + @Override + public int getTotalAnswerCountByQuestion(final Content content) { + final ViewResult result = db.queryView(createQuery("by_questionid_piround_text_subject") + //.group(true) + .startKey(ComplexKey.of(content.getId())) + .endKey(ComplexKey.of(content.getId(), ComplexKey.emptyObject()))); + + return result.isEmpty() ? 0 : result.getRows().get(0).getValueAsInt(); + } + + @Override + public List<Answer> getFreetextAnswers(final String questionId, final int start, final int limit) { + final int qSkip = start > 0 ? start : -1; + final int qLimit = limit > 0 ? limit : -1; + + final List<Answer> answers = db.queryView(createQuery("by_questionid_timestamp") + .skip(qSkip) + .limit(qLimit) + //.includeDocs(true) + .startKey(ComplexKey.of(questionId)) + .endKey(ComplexKey.of(questionId, ComplexKey.emptyObject())) + .descending(true), + Answer.class); + + return answers; + } + + @Override + public List<Answer> getMyAnswers(final User me, final Session s) { + return queryView("by_user_sessionid", ComplexKey.of(me.getUsername(), s.getId())); + } + + @Override + public int getTotalAnswerCount(final String sessionKey) { + final Session s = sessionRepository.getSessionFromKeyword(sessionKey); + if (s == null) { + throw new NotFoundException(); + } + final ViewResult result = db.queryView(createQuery("by_sessionid_variant").key(s.getId())); + + return result.isEmpty() ? 0 : result.getRows().get(0).getValueAsInt(); + } + + @CacheEvict(value = "answers", key = "#content") + @Override + public Answer saveAnswer(final Answer answer, final User user, final Content content, final Session session) { + db.create(answer); + this.answerQueue.offer(new AnswerQueueElement(session, content, answer, user)); + + return answer; + } + + /* 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 { + update(answer); + return answer; + } catch (final UpdateConflictException 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 { + /* TODO: use id and rev instead of loading the answer */ + db.delete(get(answerId)); + dbLogger.log("delete", "type", "answer"); + } catch (final DbAccessException e) { + logger.error("Could not delete answer {}.", answerId, e); + } + } + + @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 ViewResult result = db.queryView(createQuery("by_sessionid_variant") + .key(ComplexKey.of(session.getId(), variant))); + + return result.isEmpty() ? 0 : result.getRows().get(0).getValueAsInt(); + } + + /* 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 List<Content> contents = contentRepository.getQuestions(session.getId()); + contentRepository.resetQuestionsRoundState(session, contents); + + return deleteAllAnswersForQuestions(contents); + } + + /* 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 List<Content> contents = contentRepository.getQuestions(session.getId(), "preparation"); + contentRepository.resetQuestionsRoundState(session, contents); + + return deleteAllAnswersForQuestions(contents); + } + + /* 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 List<Content> contents = contentRepository.getQuestions(session.getId(), "lecture"); + contentRepository.resetQuestionsRoundState(session, contents); + + return deleteAllAnswersForQuestions(contents); + } + + public int deleteAllAnswersForQuestions(List<Content> contents) { + List<String> questionIds = new ArrayList<>(); + for (Content q : contents) { + questionIds.add(q.getId()); + } + final ViewResult result = db.queryView(createQuery("by_questionid") + .keys(questionIds)); + final List<BulkDeleteDocument> allAnswers = new ArrayList<>(); + for (ViewResult.Row a : result.getRows()) { + final BulkDeleteDocument d = new BulkDeleteDocument(a.getId(), a.getValueAsNode().get("_rev").asText()); + allAnswers.add(d); + } + try { + List<DocumentOperationResult> errors = db.executeBulk(allAnswers); + + return allAnswers.size() - errors.size(); + } catch (DbAccessException e) { + logger.error("Could not bulk delete answers.", e); + } + + return 0; + } + + public int[] deleteAllAnswersWithQuestions(List<Content> contents) { + List<String> questionIds = new ArrayList<>(); + final List<BulkDeleteDocument> allQuestions = new ArrayList<>(); + for (Content q : contents) { + final BulkDeleteDocument d = new BulkDeleteDocument(q.getId(), q.getRevision()); + questionIds.add(q.getId()); + allQuestions.add(d); + } + + final ViewResult result = db.queryView(createQuery("by_questionid") + .key(questionIds)); + final List<BulkDeleteDocument> allAnswers = new ArrayList<>(); + for (ViewResult.Row a : result.getRows()) { + final BulkDeleteDocument d = new BulkDeleteDocument(a.getId(), a.getValueAsNode().get("_rev").asText()); + allAnswers.add(d); + } + + try { + List<BulkDeleteDocument> deleteList = new ArrayList<>(allAnswers); + deleteList.addAll(allQuestions); + List<DocumentOperationResult> errors = db.executeBulk(deleteList); + + /* TODO: subtract errors from count */ + return new int[] {allQuestions.size(), allAnswers.size()}; + } catch (DbAccessException e) { + logger.error("Could not bulk delete contents and answers.", e); + } + + return new int[] {0, 0}; + } +} diff --git a/src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbContentRepository.java b/src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbContentRepository.java index ba83397e9d4426d4c60c0e828cdfb050dfda1faf..1e91fb34eb68dff249749afbdc38ec0e405eff10 100644 --- a/src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbContentRepository.java +++ b/src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbContentRepository.java @@ -1,9 +1,9 @@ package de.thm.arsnova.persistance.couchdb; -import de.thm.arsnova.dao.IDatabaseDao; import de.thm.arsnova.entities.Content; import de.thm.arsnova.entities.Session; import de.thm.arsnova.entities.User; +import de.thm.arsnova.persistance.AnswerRepository; import de.thm.arsnova.persistance.ContentRepository; import de.thm.arsnova.persistance.LogEntryRepository; import org.ektorp.ComplexKey; @@ -37,7 +37,7 @@ public class CouchDbContentRepository extends CouchDbRepositorySupport<Content> private LogEntryRepository dbLogger; @Autowired - private IDatabaseDao databaseDao; + private AnswerRepository answerRepository; public CouchDbContentRepository(Class<Content> type, CouchDbConnector db, boolean createIfNotExists) { super(type, db, createIfNotExists); @@ -145,7 +145,7 @@ public class CouchDbContentRepository extends CouchDbRepositorySupport<Content> @Override public int deleteQuestionWithAnswers(final Content content) { try { - int count = databaseDao.deleteAnswers(content); + int count = answerRepository.deleteAnswers(content); db.delete(content); dbLogger.log("delete", "type", "content", "answerCount", count); @@ -181,7 +181,7 @@ public class CouchDbContentRepository extends CouchDbRepositorySupport<Content> contents.add(q); } - int[] count = databaseDao.deleteAllAnswersWithQuestions(contents); + int[] count = answerRepository.deleteAllAnswersWithQuestions(contents); dbLogger.log("delete", "type", "question", "questionCount", count[0]); dbLogger.log("delete", "type", "answer", "answerCount", count[1]); @@ -191,7 +191,7 @@ public class CouchDbContentRepository extends CouchDbRepositorySupport<Content> @Override public List<String> getUnAnsweredQuestionIds(final Session session, final User user) { final ViewResult result = db.queryView(createQuery("questionid_by_user_sessionid_variant") - .designDocId("_design/answer") + .designDocId("_design/Answer") .startKey(ComplexKey.of(user.getUsername(), session.getId())) .endKey(ComplexKey.of(user.getUsername(), session.getId(), ComplexKey.emptyObject()))); List<String> answeredIds = new ArrayList<>(); @@ -204,7 +204,7 @@ public class CouchDbContentRepository extends CouchDbRepositorySupport<Content> @Override public List<String> getUnAnsweredLectureQuestionIds(final Session session, final User user) { final ViewResult result = db.queryView(createQuery("questionid_piround_by_user_sessionid_variant") - .designDocId("_design/answer") + .designDocId("_design/Answer") .key(ComplexKey.of(user.getUsername(), session.getId(), "lecture"))); Map<String, Integer> answeredQuestions = new HashMap<>(); for (ViewResult.Row row : result.getRows()) { @@ -217,7 +217,7 @@ public class CouchDbContentRepository extends CouchDbRepositorySupport<Content> @Override public List<String> getUnAnsweredPreparationQuestionIds(final Session session, final User user) { final ViewResult result = db.queryView(createQuery("questionid_piround_by_user_sessionid_variant") - .designDocId("_design/answer") + .designDocId("_design/Answer") .key(ComplexKey.of(user.getUsername(), session.getId(), "preparation"))); Map<String, Integer> answeredQuestions = new HashMap<>(); for (ViewResult.Row row : result.getRows()) { diff --git a/src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbSessionRepository.java b/src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbSessionRepository.java index 0d0da5868042151d096c7c8242b7661560eca471..ab0166b23e1830fc4d53ea9bcabf5d5804e6e3db 100644 --- a/src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbSessionRepository.java +++ b/src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbSessionRepository.java @@ -504,11 +504,10 @@ public class CouchDbSessionRepository extends CouchDbRepositorySupport<Session> } private List<SessionInfo> getInfosForSessions(final List<Session> sessions) { - /* TODO: migrate to new view */ List<String> sessionIds = sessions.stream().map(Session::getId).collect(Collectors.toList()); final ViewQuery questionCountView = createQuery("by_sessionid").designDocId("_design/Content") .group(true).keys(sessionIds); - final ViewQuery answerCountView = createQuery("by_sessionid").designDocId("_design/answer") + final ViewQuery answerCountView = createQuery("by_sessionid").designDocId("_design/Answer") .group(true).keys(sessionIds); final ViewQuery commentCountView = createQuery("by_sessionid").designDocId("_design/Comment") .group(true).keys(sessionIds); @@ -519,7 +518,7 @@ public class CouchDbSessionRepository extends CouchDbRepositorySupport<Session> } private List<SessionInfo> getInfosForVisitedSessions(final List<Session> sessions, final User user) { - final ViewQuery answeredQuestionsView = createQuery("by_user_sessionid").designDocId("_design/answer") + final ViewQuery answeredQuestionsView = createQuery("by_user_sessionid").designDocId("_design/Answer") .keys(sessions.stream().map(session -> ComplexKey.of(user.getUsername(), session.getId())).collect(Collectors.toList())); final ViewQuery questionIdsView = createQuery("by_sessionid").designDocId("_design/Content") .keys(sessions.stream().map(Session::getId).collect(Collectors.toList())); diff --git a/src/main/java/de/thm/arsnova/services/ContentService.java b/src/main/java/de/thm/arsnova/services/ContentService.java index c08d1ccbf8e3837132e5b536abeb5ff23139cb96..0cf430d1e755f76d38eb429aaaf06afdf0c1d893 100644 --- a/src/main/java/de/thm/arsnova/services/ContentService.java +++ b/src/main/java/de/thm/arsnova/services/ContentService.java @@ -30,6 +30,7 @@ import de.thm.arsnova.exceptions.BadRequestException; import de.thm.arsnova.exceptions.ForbiddenException; import de.thm.arsnova.exceptions.NotFoundException; import de.thm.arsnova.exceptions.UnauthorizedException; +import de.thm.arsnova.persistance.AnswerRepository; import de.thm.arsnova.persistance.CommentRepository; import de.thm.arsnova.persistance.ContentRepository; import de.thm.arsnova.persistance.SessionRepository; @@ -71,6 +72,9 @@ public class ContentService implements IContentService, ApplicationEventPublishe @Autowired private ContentRepository contentRepository; + @Autowired + private AnswerRepository answerRepository; + @Autowired private ImageUtils imageUtils; @@ -285,7 +289,7 @@ public class ContentService implements IContentService, ApplicationEventPublishe } content.resetRoundManagementState(); - databaseDao.deleteAnswers(content); + answerRepository.deleteAnswers(content); update(content); this.publisher.publishEvent(new PiRoundResetEvent(this, session, content)); } @@ -392,7 +396,7 @@ public class ContentService implements IContentService, ApplicationEventPublishe final Content content = contentRepository.getQuestion(questionId); content.resetQuestionState(); contentRepository.updateQuestion(content); - databaseDao.deleteAnswers(content); + answerRepository.deleteAnswers(content); } @Override @@ -418,12 +422,12 @@ public class ContentService implements IContentService, ApplicationEventPublishe if (content == null) { throw new NotFoundException(); } - return databaseDao.getMyAnswer(userService.getCurrentUser(), questionId, content.getPiRound()); + return answerRepository.getMyAnswer(userService.getCurrentUser(), questionId, content.getPiRound()); } @Override public void readFreetextAnswer(final String answerId, final User user) { - final Answer answer = databaseDao.getObjectFromId(answerId, Answer.class); + final Answer answer = answerRepository.get(answerId); if (answer == null) { throw new NotFoundException(); } @@ -433,7 +437,7 @@ public class ContentService implements IContentService, ApplicationEventPublishe final Session session = sessionRepository.getSessionFromId(answer.getSessionId()); if (session.isCreator(user)) { answer.setRead(true); - databaseDao.updateAnswer(answer); + answerRepository.updateAnswer(answer); } } @@ -446,7 +450,7 @@ public class ContentService implements IContentService, ApplicationEventPublishe } return "freetext".equals(content.getQuestionType()) ? getFreetextAnswers(questionId, offset, limit) - : databaseDao.getAnswers(content, piRound); + : answerRepository.getAnswers(content, piRound); } @Override @@ -459,7 +463,7 @@ public class ContentService implements IContentService, ApplicationEventPublishe if ("freetext".equals(content.getQuestionType())) { return getFreetextAnswers(questionId, offset, limit); } else { - return databaseDao.getAnswers(content); + return answerRepository.getAnswers(content); } } @@ -473,7 +477,7 @@ public class ContentService implements IContentService, ApplicationEventPublishe if ("freetext".equals(content.getQuestionType())) { return getFreetextAnswers(questionId, offset, limit); } else { - return databaseDao.getAllAnswers(content); + return answerRepository.getAllAnswers(content); } } @@ -486,9 +490,9 @@ public class ContentService implements IContentService, ApplicationEventPublishe } if ("freetext".equals(content.getQuestionType())) { - return databaseDao.getTotalAnswerCountByQuestion(content); + return answerRepository.getTotalAnswerCountByQuestion(content); } else { - return databaseDao.getAnswerCount(content, content.getPiRound()); + return answerRepository.getAnswerCount(content, content.getPiRound()); } } @@ -500,7 +504,7 @@ public class ContentService implements IContentService, ApplicationEventPublishe return 0; } - return databaseDao.getAnswerCount(content, piRound); + return answerRepository.getAnswerCount(content, piRound); } @Override @@ -511,7 +515,7 @@ public class ContentService implements IContentService, ApplicationEventPublishe return 0; } - return databaseDao.getAbstentionAnswerCount(questionId); + return answerRepository.getAbstentionAnswerCount(questionId); } @Override @@ -522,13 +526,13 @@ public class ContentService implements IContentService, ApplicationEventPublishe return 0; } - return databaseDao.getTotalAnswerCountByQuestion(content); + return answerRepository.getTotalAnswerCountByQuestion(content); } @Override @PreAuthorize("isAuthenticated()") public List<Answer> getFreetextAnswers(final String questionId, final int offset, final int limit) { - final List<Answer> answers = databaseDao.getFreetextAnswers(questionId, offset, limit); + final List<Answer> answers = answerRepository.getFreetextAnswers(questionId, offset, limit); if (answers == null) { throw new NotFoundException(); } @@ -552,7 +556,7 @@ public class ContentService implements IContentService, ApplicationEventPublishe } /* filter answers by active piRound per question */ - final List<Answer> answers = databaseDao.getMyAnswers(userService.getCurrentUser(), session); + final List<Answer> answers = answerRepository.getMyAnswers(userService.getCurrentUser(), session); final List<Answer> filteredAnswers = new ArrayList<>(); for (final Answer answer : answers) { final Content content = questionIdToQuestion.get(answer.getQuestionId()); @@ -577,7 +581,7 @@ public class ContentService implements IContentService, ApplicationEventPublishe @Override @PreAuthorize("isAuthenticated()") public int getTotalAnswerCount(final String sessionKey) { - return databaseDao.getTotalAnswerCount(sessionKey); + return answerRepository.getTotalAnswerCount(sessionKey); } @Override @@ -690,8 +694,12 @@ public class ContentService implements IContentService, ApplicationEventPublishe if (content == null) { throw new NotFoundException(); } + final Session session = sessionRepository.getSessionFromId(content.getSessionId()); Answer theAnswer = answer.generateAnswerEntity(user, content); + theAnswer.setUser(user.getUsername()); + theAnswer.setQuestionId(content.getId()); + theAnswer.setSessionId(session.getId()); if ("freetext".equals(content.getQuestionType())) { imageUtils.generateThumbnailImage(theAnswer); if (content.isFixedAnswer() && content.getText() != null) { @@ -705,7 +713,7 @@ public class ContentService implements IContentService, ApplicationEventPublishe } } - return databaseDao.saveAnswer(theAnswer, user, content, sessionRepository.getSessionFromId(content.getSessionId())); + return answerRepository.saveAnswer(theAnswer, user, content, session); } @Override @@ -722,8 +730,11 @@ public class ContentService implements IContentService, ApplicationEventPublishe imageUtils.generateThumbnailImage(realAnswer); content.checkTextStrictOptions(realAnswer); } - final Answer result = databaseDao.updateAnswer(realAnswer); final Session session = sessionRepository.getSessionFromId(content.getSessionId()); + answer.setUser(user.getUsername()); + answer.setQuestionId(content.getId()); + answer.setSessionId(session.getId()); + final Answer result = answerRepository.updateAnswer(realAnswer); this.publisher.publishEvent(new NewAnswerEvent(this, session, result, user, content)); return result; @@ -741,7 +752,7 @@ public class ContentService implements IContentService, ApplicationEventPublishe if (user == null || session == null || !session.isCreator(user)) { throw new UnauthorizedException(); } - databaseDao.deleteAnswer(answerId); + answerRepository.deleteAnswer(answerId); this.publisher.publishEvent(new DeleteAnswerEvent(this, session, content)); } @@ -832,7 +843,7 @@ public class ContentService implements IContentService, ApplicationEventPublishe */ @Override public int countLectureQuestionAnswersInternal(final String sessionkey) { - return databaseDao.countLectureQuestionAnswers(getSession(sessionkey)); + return answerRepository.countLectureQuestionAnswers(getSession(sessionkey)); } @Override @@ -845,8 +856,8 @@ public class ContentService implements IContentService, ApplicationEventPublishe } map.put("_id", questionId); - map.put("answers", databaseDao.getAnswerCount(content, content.getPiRound())); - map.put("abstentions", databaseDao.getAbstentionAnswerCount(questionId)); + map.put("answers", answerRepository.getAnswerCount(content, content.getPiRound())); + map.put("abstentions", answerRepository.getAbstentionAnswerCount(questionId)); return map; } @@ -863,7 +874,7 @@ public class ContentService implements IContentService, ApplicationEventPublishe */ @Override public int countPreparationQuestionAnswersInternal(final String sessionkey) { - return databaseDao.countPreparationQuestionAnswers(getSession(sessionkey)); + return answerRepository.countPreparationQuestionAnswers(getSession(sessionkey)); } /* @@ -966,7 +977,7 @@ public class ContentService implements IContentService, ApplicationEventPublishe if (!session.isCreator(user)) { throw new UnauthorizedException(); } - databaseDao.deleteAllQuestionsAnswers(session); + answerRepository.deleteAllQuestionsAnswers(session); this.publisher.publishEvent(new DeleteAllQuestionsAnswersEvent(this, session)); } @@ -975,7 +986,7 @@ public class ContentService implements IContentService, ApplicationEventPublishe @PreAuthorize("isAuthenticated() and hasPermission(#sessionkey, 'session', 'owner')") public void deleteAllPreparationAnswers(String sessionkey) { final Session session = getSession(sessionkey); - databaseDao.deleteAllPreparationAnswers(session); + answerRepository.deleteAllPreparationAnswers(session); this.publisher.publishEvent(new DeleteAllPreparationAnswersEvent(this, session)); } @@ -984,7 +995,7 @@ public class ContentService implements IContentService, ApplicationEventPublishe @PreAuthorize("isAuthenticated() and hasPermission(#sessionkey, 'session', 'owner')") public void deleteAllLectureAnswers(String sessionkey) { final Session session = getSession(sessionkey); - databaseDao.deleteAllLectureAnswers(session); + answerRepository.deleteAllLectureAnswers(session); this.publisher.publishEvent(new DeleteAllLectureAnswersEvent(this, session)); } @@ -1000,7 +1011,7 @@ public class ContentService implements IContentService, ApplicationEventPublishe Answer answer = null; for (Answer a : answers) { - if (answerId.equals(a.get_id())) { + if (answerId.equals(a.getId())) { answer = a; break; } diff --git a/src/test/java/de/thm/arsnova/dao/StubDatabaseDao.java b/src/test/java/de/thm/arsnova/dao/StubDatabaseDao.java index 6dc8bb931103247839034b0284de259a87f05e75..099e1dfd19216f2957049e47f184bc57d292d515 100644 --- a/src/test/java/de/thm/arsnova/dao/StubDatabaseDao.java +++ b/src/test/java/de/thm/arsnova/dao/StubDatabaseDao.java @@ -96,118 +96,18 @@ public class StubDatabaseDao implements IDatabaseDao { stubQuestions.put("12345678", contents); } - @Override - public Answer getMyAnswer(User user, String questionId, int piRound) { - // TODO Auto-generated method stub - return null; - } - - @Override - public int getAnswerCount(Content content, int piRound) { - // TODO Auto-generated method stub - return 0; - } - - @Override - public List<Answer> getFreetextAnswers(String questionId, final int start, final int limit) { - // TODO Auto-generated method stub - return null; - } - - @Override - public List<Answer> getMyAnswers(User user, Session session) { - return new ArrayList<>(); - } - - @Override - public int getTotalAnswerCount(String sessionKey) { - // TODO Auto-generated method stub - return 0; - } - - @Override - public int deleteAnswers(Content content) { - // TODO Auto-generated method stub - return 0; - } - - @Override - public Answer updateAnswer(Answer answer) { - // TODO Auto-generated method stub - return null; - } - - @Override - public void deleteAnswer(String answerId) { - // TODO Auto-generated method stub - } - @Override public int deleteInactiveGuestVisitedSessionLists(long lastActivityBefore) { // TODO Auto-generated method stub return 0; } - @Override - public int countLectureQuestionAnswers(Session session) { - // TODO Auto-generated method stub - return 0; - } - - @Override - public int countPreparationQuestionAnswers(Session session) { - // TODO Auto-generated method stub - return 0; - } - - @Override - public int deleteAllQuestionsAnswers(Session session) { - // TODO Auto-generated method stub - return 0; - } - @Override public CourseScore getLearningProgress(Session session) { // TODO Auto-generated method stub return null; } - @Override - public int deleteAllPreparationAnswers(Session session) { - // TODO Auto-generated method stub - return 0; - } - - @Override - public int deleteAllLectureAnswers(Session session) { - // TODO Auto-generated method stub - return 0; - } - - @Override - public int getAbstentionAnswerCount(String questionId) { - // TODO Auto-generated method stub - return 0; - } - - @Override - public List<Answer> getAnswers(Content content, int piRound) { - // TODO Auto-generated method stub - return null; - } - - @Override - public List<Answer> getAnswers(Content content) { - // TODO Auto-generated method stub - return null; - } - - @Override - public Answer saveAnswer(Answer answer, User user, Content content, Session session) { - // TODO Auto-generated method stub - return null; - } - @Override public Statistics getStatistics() { final Statistics stats = new Statistics(); @@ -219,18 +119,6 @@ public class StubDatabaseDao implements IDatabaseDao { return stats; } - @Override - public List<Answer> getAllAnswers(Content content) { - // TODO Auto-generated method stub - return null; - } - - @Override - public int getTotalAnswerCountByQuestion(Content content) { - // TODO Auto-generated method stub - return 0; - } - @Override public <T> T getObjectFromId(String documentId, Class<T> klass) { // TODO Auto-generated method stub @@ -248,10 +136,4 @@ public class StubDatabaseDao implements IDatabaseDao { // TODO Auto-generated method stub return null; } - - @Override - public int[] deleteAllAnswersWithQuestions(List<Content> contents) { - // TODO Auto-generated method stub - return new int[0]; - } }