Commit 98401962 authored by Daniel Gerhardt's avatar Daniel Gerhardt

Separate comment persistance code and migrate it to Ektorp

InterposedQuestion has been renamed to Comment. Method names have not
been touched yet to ease reviewing the changes.
parent ef299621
......@@ -30,11 +30,11 @@ public class CacheBuster implements ICacheBuster, NovaEventVisitor {
@CacheEvict(value = "statistics", allEntries = true)
@Override
public void visit(NewInterposedQuestionEvent event) { }
public void visit(NewCommentEvent event) { }
@CacheEvict(value = "statistics", allEntries = true)
@Override
public void visit(DeleteInterposedQuestionEvent event) { }
public void visit(DeleteCommentEvent event) { }
@Override
public void visit(NewQuestionEvent event) { }
......
......@@ -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.Comment;
import de.thm.arsnova.entities.DbUser;
import de.thm.arsnova.entities.LogEntry;
import de.thm.arsnova.entities.Motd;
......@@ -30,10 +31,12 @@ 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.CommentRepository;
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.CouchDbCommentRepository;
import de.thm.arsnova.persistance.couchdb.CouchDbLogEntryRepository;
import de.thm.arsnova.persistance.couchdb.CouchDbMotdRepository;
import de.thm.arsnova.persistance.couchdb.CouchDbSessionRepository;
......@@ -292,6 +295,11 @@ public class AppConfig extends WebMvcConfigurerAdapter {
return new CouchDbSessionRepository(Session.class, couchDbConnector(), false);
}
@Bean
public CommentRepository commentRepository() throws Exception {
return new CouchDbCommentRepository(Comment.class, couchDbConnector(), false);
}
@Bean
public UserRepository userRepository() throws Exception {
return new CouchDbUserRepository(DbUser.class, couchDbConnector(), false);
......
......@@ -17,8 +17,8 @@
*/
package de.thm.arsnova.controller;
import de.thm.arsnova.entities.InterposedReadingCount;
import de.thm.arsnova.entities.transport.InterposedQuestion;
import de.thm.arsnova.entities.CommentReadingCount;
import de.thm.arsnova.entities.transport.Comment;
import de.thm.arsnova.exceptions.BadRequestException;
import de.thm.arsnova.services.IQuestionService;
import de.thm.arsnova.web.DeprecatedApi;
......@@ -41,17 +41,17 @@ import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* Handles requests related to audience questions, which are also called interposed or feedback questions.
* Handles requests related to comments.
*/
@RestController
@RequestMapping("/audiencequestion")
@Api(value = "/audiencequestion", description = "the Audience Question API")
public class AudienceQuestionController extends PaginationController {
public class CommentController extends PaginationController {
@Autowired
private IQuestionService questionService;
@ApiOperation(value = "Count all the questions in current session",
@ApiOperation(value = "Count all the comments in current session",
nickname = "getAudienceQuestionCount")
@RequestMapping(value = "/count", method = RequestMethod.GET)
@DeprecatedApi
......@@ -60,31 +60,31 @@ public class AudienceQuestionController extends PaginationController {
return questionService.getInterposedCount(sessionkey);
}
@ApiOperation(value = "count all unread interposed questions",
@ApiOperation(value = "count all unread comments",
nickname = "getUnreadInterposedCount")
@RequestMapping(value = "/readcount", method = RequestMethod.GET)
@DeprecatedApi
@Deprecated
public InterposedReadingCount getUnreadInterposedCount(@ApiParam(value = "Session-Key from current session", required = true) @RequestParam("sessionkey") final String sessionkey, String user) {
public CommentReadingCount getUnreadInterposedCount(@ApiParam(value = "Session-Key from current session", required = true) @RequestParam("sessionkey") final String sessionkey, String user) {
return questionService.getInterposedReadingCount(sessionkey, user);
}
@ApiOperation(value = "Retrieves all Interposed Questions for a Session",
@ApiOperation(value = "Retrieves all Comments for a Session",
nickname = "getInterposedQuestions")
@RequestMapping(value = "/", method = RequestMethod.GET)
@Pagination
public List<InterposedQuestion> getInterposedQuestions(@ApiParam(value = "Session-Key from current session", required = true) @RequestParam final String sessionkey) {
return InterposedQuestion.fromList(questionService.getInterposedQuestions(sessionkey, offset, limit));
public List<Comment> getInterposedQuestions(@ApiParam(value = "Session-Key from current session", required = true) @RequestParam final String sessionkey) {
return Comment.fromList(questionService.getInterposedQuestions(sessionkey, offset, limit));
}
@ApiOperation(value = "Retrieves an InterposedQuestion",
@ApiOperation(value = "Retrieves an Comment",
nickname = "getInterposedQuestion")
@RequestMapping(value = "/{questionId}", method = RequestMethod.GET)
public InterposedQuestion getInterposedQuestion(@ApiParam(value = "ID of the question that needs to be deleted", required = true) @PathVariable final String questionId) {
return new InterposedQuestion(questionService.readInterposedQuestion(questionId));
public Comment getInterposedQuestion(@ApiParam(value = "ID of the Comment that needs to be deleted", required = true) @PathVariable final String questionId) {
return new Comment(questionService.readInterposedQuestion(questionId));
}
@ApiOperation(value = "Creates a new Interposed Question for a Session and returns the InterposedQuestion's data",
@ApiOperation(value = "Creates a new Comment for a Session and returns the Comment's data",
nickname = "postInterposedQuestion")
@ApiResponses(value = {
@ApiResponse(code = 400, message = HTML_STATUS_400)
......@@ -93,19 +93,19 @@ public class AudienceQuestionController extends PaginationController {
@ResponseStatus(HttpStatus.CREATED)
public void postInterposedQuestion(
@ApiParam(value = "Session-Key from current session", required = true) @RequestParam final String sessionkey,
@ApiParam(value = "the body from the new question", required = true) @RequestBody final de.thm.arsnova.entities.InterposedQuestion question
@ApiParam(value = "the body from the new comment", required = true) @RequestBody final de.thm.arsnova.entities.Comment comment
) {
if (questionService.saveQuestion(question)) {
if (questionService.saveQuestion(comment)) {
return;
}
throw new BadRequestException();
}
@ApiOperation(value = "Deletes an InterposedQuestion",
@ApiOperation(value = "Deletes a Comment",
nickname = "deleteInterposedQuestion")
@RequestMapping(value = "/{questionId}", method = RequestMethod.DELETE)
public void deleteInterposedQuestion(@ApiParam(value = "ID of the question that needs to be deleted", required = true) @PathVariable final String questionId) {
public void deleteInterposedQuestion(@ApiParam(value = "ID of the comment that needs to be deleted", required = true) @PathVariable final String questionId) {
questionService.deleteInterposedQuestion(questionId);
}
}
......@@ -298,7 +298,7 @@ public class SessionController extends PaginationController {
public List<ImportExportSession> getExport(
@ApiParam(value = "sessionkey", required = true) @RequestParam(value = "sessionkey", defaultValue = "") final List<String> sessionkey,
@ApiParam(value = "wether statistics shall be exported", required = true) @RequestParam(value = "withAnswerStatistics", defaultValue = "false") final Boolean withAnswerStatistics,
@ApiParam(value = "wether interposed questions shall be exported", required = true) @RequestParam(value = "withFeedbackQuestions", defaultValue = "false") final Boolean withFeedbackQuestions,
@ApiParam(value = "wether comments shall be exported", required = true) @RequestParam(value = "withFeedbackQuestions", defaultValue = "false") final Boolean withFeedbackQuestions,
final HttpServletResponse response
) {
List<ImportExportSession> sessions = new ArrayList<>();
......
......@@ -359,33 +359,6 @@ public class CouchDBDao implements IDatabaseDao, ApplicationEventPublisherAware
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.getId());
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) {
......@@ -722,138 +695,6 @@ public class CouchDBDao implements IDatabaseDao, ApplicationEventPublisherAware
return results.getJSONArray("rows").optJSONObject(0).optInt("value");
}
@Override
public int getInterposedCount(final String sessionKey) {
final Session s = sessionRepository.getSessionFromKeyword(sessionKey);
if (s == null) {
throw new NotFoundException();
}
final View view = new View("comment/by_sessionid");
view.setKey(s.getId());
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.getId());
view.setEndKeyArray(session.getId(), "{}");
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.getId(), user.getUsername());
view.setEndKeyArray(session.getId(), 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.getId(), "{}");
view.setEndKeyArray(session.getId());
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.getId(), user.getUsername(), "{}");
view.setEndKeyArray(session.getId(), 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() {
......@@ -932,33 +773,6 @@ public class CouchDBDao implements IDatabaseDao, ApplicationEventPublisherAware
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);
/* TODO: Refactor code so the next line can be removed */
question.setSessionId(sessionRepository.getSessionFromKeyword(question.getSessionId()).getId());
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);
}
}
@CacheEvict(value = "answers", key = "#question")
@Override
public Answer saveAnswer(final Answer answer, final User user, final Question question, final Session session) {
......@@ -1053,16 +867,6 @@ public class CouchDBDao implements IDatabaseDao, ApplicationEventPublisherAware
}
}
@Override
public void deleteInterposedQuestion(final InterposedQuestion question) {
try {
deleteDocument(question.get_id());
dbLogger.log("delete", "type", "comment");
} catch (final IOException e) {
logger.error("Could not delete interposed question {}.", question.get_id(), e);
}
}
/**
* Adds convenience methods to CouchDB4J's view class.
*/
......@@ -1390,45 +1194,6 @@ public class CouchDBDao implements IDatabaseDao, ApplicationEventPublisherAware
return ids;
}
@Override
public int deleteAllInterposedQuestions(final Session session) {
final View view = new View("comment/by_sessionid");
view.setKey(session.getId());
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.getId(), user.getUsername());
view.setEndKeyArray(session.getId(), 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 */
dbLogger.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");
......
......@@ -28,8 +28,6 @@ import java.util.List;
public interface IDatabaseDao {
Question saveQuestion(Session session, Question question);
InterposedQuestion saveQuestion(Session session, InterposedQuestion question, User user);
Question getQuestion(String id);
List<Question> getSkillQuestionsForUsers(Session session);
......@@ -66,20 +64,6 @@ public interface IDatabaseDao {
int getTotalAnswerCount(String sessionKey);
int getInterposedCount(String sessionKey);
InterposedReadingCount getInterposedReadingCount(Session session);
InterposedReadingCount getInterposedReadingCount(Session session, User user);
List<InterposedQuestion> getInterposedQuestions(Session session, final int start, final int limit);
List<InterposedQuestion> getInterposedQuestions(Session session, User user, final int start, final int limit);
InterposedQuestion getInterposedQuestion(String questionId);
void markInterposedQuestionAsRead(InterposedQuestion question);
Question updateQuestion(Question question);
int deleteAnswers(Question question);
......@@ -90,8 +74,6 @@ public interface IDatabaseDao {
void deleteAnswer(String answerId);
void deleteInterposedQuestion(InterposedQuestion question);
int deleteInactiveGuestVisitedSessionLists(long lastActivityBefore);
List<Question> getLectureQuestionsForUsers(Session session);
......@@ -128,10 +110,6 @@ public interface IDatabaseDao {
List<String> getUnAnsweredPreparationQuestionIds(Session session, User user);
int deleteAllInterposedQuestions(Session session);
int deleteAllInterposedQuestions(Session session, User user);
void publishQuestions(Session session, boolean publish, List<Question> questions);
List<Question> publishAllQuestions(Session session, boolean publish);
......
......@@ -51,10 +51,10 @@ public class LearningProgressFactory implements NovaEventVisitor, ILearningProgr
}
@Override
public void visit(NewInterposedQuestionEvent event) { }
public void visit(NewCommentEvent event) { }
@Override
public void visit(DeleteInterposedQuestionEvent deleteInterposedQuestionEvent) { }
public void visit(DeleteCommentEvent deleteCommentEvent) { }
@CacheEvict(value = "learningprogress", key = "#event.Session")
@Override
......
......@@ -17,21 +17,18 @@
*/
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;
import java.io.Serializable;
/**
* A question the user is asking the teacher. Also known as feedback or audience question.
* A question the user is asking the teacher. Also known as comment, feedback or audience question.
*/
@ApiModel(value = "audiencequestion", description = "the interposed question entity")
public class InterposedQuestion implements Serializable {
private String _id;
private String _rev;
private String type;
@ApiModel(value = "comment", description = "the comment entity")
public class Comment implements Entity {
private String id;
private String rev;
private String subject;
private String text;
/* FIXME sessionId actually is used to hold the sessionKey.
......@@ -44,77 +41,87 @@ public class InterposedQuestion implements Serializable {
private boolean read;
private String creator;
@ApiModelProperty(required = true, value = "the couchDB ID")
public String get_id() {
return _id;
@JsonView({View.Persistence.class, View.Public.class})
public String getId() {
return id;
}
public void set_id(String _id) {
this._id = _id;
@JsonView({View.Persistence.class, View.Public.class})
public void setId(final String id) {
this.id = id;
}
public String get_rev() {
return _rev;
@JsonView({View.Persistence.class, View.Public.class})
public void setRevision(final String rev) {
this.rev = rev;
}
public void set_rev(String _rev) {
this._rev = _rev;
@JsonView({View.Persistence.class, View.Public.class})
public String getRevision() {
return rev;
}
@ApiModelProperty(required = true, value = "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 = "used to display the type")
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
@ApiModelProperty(required = true, value = "the subject")
@JsonView({View.Persistence.class, View.Public.class})
public String getSubject() {
return subject;
}
@JsonView({View.Persistence.class, View.Public.class})
public void setSubject(String subject) {
this.subject = subject;
}
@ApiModelProperty(required = true, value = "the Text")
@JsonView({View.Persistence.class, View.Public.class})
public String getText() {
return text;
}
@JsonView({View.Persistence.class, View.Public.class})
public void setText(String text) {
this.text = text;
}
@ApiModelProperty(required = true, value = "ID of the session, the question is assigned to")
@ApiModelProperty(required = true, value = "ID of the session, the comment is assigned to")
@JsonView(View.Persistence.class)
public String getSessionId() {
return sessionId;
}
@JsonView({View.Persistence.class, View.Public.class})
public void setSessionId(String sessionId) {
this.sessionId = sessionId;
}
@ApiModelProperty(required = true, value = "creation date timestamp")
@JsonView({View.Persistence.class, View.Public.class})
public long getTimestamp() {
return timestamp;
}
@JsonView({View.Persistence.class, View.Public.class})
public void setTimestamp(long timestamp) {
this.timestamp = timestamp;
}
/* TODO: use JsonViews instead of JsonIgnore when supported by Spring (4.1)
* http://wiki.fasterxml.com/JacksonJsonViews
* https://jira.spring.io/browse/SPR-7156 */
@JsonIgnore
@JsonView(View.Persistence.class)
public String getCreator() {
return creator;
}
@JsonView(View.Persistence.class)
public void setCreator(String creator) {
this.creator = creator;
}
......
......@@ -23,25 +23,25 @@ import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
/**
* Wrapper class for counting read and unread interposed questions for a session or a single user.
* Wrapper class for counting read and unread comments for a session or a single user.
*/
@ApiModel(value = "audiencequestion/readcount", description = "the interposed reading count entity")
public class InterposedReadingCount {
@ApiModel(value = "audiencequestion/readcount", description = "the comment reading count entity")
public class CommentReadingCount {
private int read;
private int unread;
public InterposedReadingCount(int readCount, int unreadCount) {
public CommentReadingCount(int readCount, int unreadCount) {
this.read = readCount;
this.unread = unreadCount;
}
public InterposedReadingCount() {
public CommentReadingCount() {
this.read = 0;
this.unread = 0;
}
@ApiModelProperty(required = true, value = "the number of read interposed questions")
@ApiModelProperty(required = true, value = "the number of read comments")
@JsonView(View.Public.class)
public int getRead() {
return read;
......@@ -51,7 +51,7 @@ public class InterposedReadingCount {
this.read = read;
}
@ApiModelProperty(required = true, value = "the number of unread interposed questions")
@ApiModelProperty(required = true, value = "the number of unread comments")
@JsonView(View.Public.class)
public int getUnread() {
return unread;
......@@ -61,7 +61,7 @@ public class InterposedReadingCount {
this.unread = unread;
}
@ApiModelProperty(required = true, value = "the number of total interposed questions")
@ApiModelProperty(required = true, value = "the number of total comments")
@JsonView(View.Public.class)
public int getTotal() {
return getRead() + getUnread();
......
......@@ -44,8 +44,8 @@ public class SessionInfo {
private int numQuestions;
private int numAnswers;
private int numInterposed;
private int numUnredInterposed;
private int numComments;
private int numUnreadComments;
</