From abdd42fceb8b95a387bf440d119c6733b619a5e9 Mon Sep 17 00:00:00 2001 From: Christoph Thelen <christoph.thelen@mni.thm.de> Date: Wed, 5 Nov 2014 17:28:30 +0100 Subject: [PATCH] Send unanswered questions and answer count over web sockets --- .../thm/arsnova/events/DeleteAnswerEvent.java | 33 +++++++++ .../de/thm/arsnova/events/NewAnswerEvent.java | 49 +++++++++++++ .../thm/arsnova/events/NovaEventVisitor.java | 4 ++ .../arsnova/services/IQuestionService.java | 9 +++ .../thm/arsnova/services/QuestionService.java | 38 +++++++++- .../arsnova/socket/ARSnovaSocketIOServer.java | 70 ++++++++++++++----- .../thm/arsnova/socket/message/Question.java | 20 ++++++ 7 files changed, 201 insertions(+), 22 deletions(-) create mode 100644 src/main/java/de/thm/arsnova/events/DeleteAnswerEvent.java create mode 100644 src/main/java/de/thm/arsnova/events/NewAnswerEvent.java create mode 100644 src/main/java/de/thm/arsnova/socket/message/Question.java diff --git a/src/main/java/de/thm/arsnova/events/DeleteAnswerEvent.java b/src/main/java/de/thm/arsnova/events/DeleteAnswerEvent.java new file mode 100644 index 00000000..1f5c564f --- /dev/null +++ b/src/main/java/de/thm/arsnova/events/DeleteAnswerEvent.java @@ -0,0 +1,33 @@ +package de.thm.arsnova.events; + +import de.thm.arsnova.entities.Question; +import de.thm.arsnova.entities.Session; + +public class DeleteAnswerEvent extends NovaEvent { + + private static final long serialVersionUID = 1L; + + private final Question question; + + private final Session session; + + public DeleteAnswerEvent(Object source, Question question, Session session) { + super(source); + this.question = question; + this.session = session; + } + + @Override + public void accept(NovaEventVisitor visitor) { + visitor.visit(this); + } + + public Question getQuestion() { + return question; + } + + public Session getSession() { + return session; + } + +} diff --git a/src/main/java/de/thm/arsnova/events/NewAnswerEvent.java b/src/main/java/de/thm/arsnova/events/NewAnswerEvent.java new file mode 100644 index 00000000..256d569d --- /dev/null +++ b/src/main/java/de/thm/arsnova/events/NewAnswerEvent.java @@ -0,0 +1,49 @@ +package de.thm.arsnova.events; + +import de.thm.arsnova.entities.Answer; +import de.thm.arsnova.entities.Question; +import de.thm.arsnova.entities.Session; +import de.thm.arsnova.entities.User; + +public class NewAnswerEvent extends NovaEvent { + + private static final long serialVersionUID = 1L; + + private final Answer answer; + + private final User user; + + private final Question question; + + private final Session session; + + public NewAnswerEvent(Object source, Answer answer, User user, Question question, Session session) { + super(source); + this.answer = answer; + this.user = user; + this.question = question; + this.session = session; + } + + @Override + public void accept(NovaEventVisitor visitor) { + visitor.visit(this); + } + + public Answer getAnswer() { + return answer; + } + + public User getUser() { + return user; + } + + public Question getQuestion() { + return question; + } + + public Session getSession() { + return session; + } + +} diff --git a/src/main/java/de/thm/arsnova/events/NovaEventVisitor.java b/src/main/java/de/thm/arsnova/events/NovaEventVisitor.java index 4664d21d..6f1ec04a 100644 --- a/src/main/java/de/thm/arsnova/events/NovaEventVisitor.java +++ b/src/main/java/de/thm/arsnova/events/NovaEventVisitor.java @@ -6,4 +6,8 @@ public interface NovaEventVisitor { void visit(NewQuestionEvent newQuestionEvent); + void visit(NewAnswerEvent newAnswerEvent); + + void visit(DeleteAnswerEvent deleteAnswerEvent); + } diff --git a/src/main/java/de/thm/arsnova/services/IQuestionService.java b/src/main/java/de/thm/arsnova/services/IQuestionService.java index 596d1ffd..4b1746d1 100644 --- a/src/main/java/de/thm/arsnova/services/IQuestionService.java +++ b/src/main/java/de/thm/arsnova/services/IQuestionService.java @@ -25,6 +25,7 @@ import de.thm.arsnova.entities.Answer; import de.thm.arsnova.entities.InterposedQuestion; import de.thm.arsnova.entities.InterposedReadingCount; import de.thm.arsnova.entities.Question; +import de.thm.arsnova.entities.User; public interface IQuestionService { Question saveQuestion(Question question); @@ -91,8 +92,12 @@ public interface IQuestionService { int countLectureQuestionAnswers(String sessionkey); + int countLectureQuestionAnswersInternal(String sessionkey); + int countPreparationQuestionAnswers(String sessionkey); + int countPreparationQuestionAnswersInternal(String sessionkey); + void deleteLectureQuestions(String sessionkey); void deleteFlashcards(String sessionkey); @@ -101,8 +106,12 @@ public interface IQuestionService { List<String> getUnAnsweredLectureQuestionIds(String sessionkey); + List<String> getUnAnsweredLectureQuestionIds(String sessionKey, User user); + List<String> getUnAnsweredPreparationQuestionIds(String sessionkey); + List<String> getUnAnsweredPreparationQuestionIds(String sessionKey, User user); + void deleteAllInterposedQuestions(String sessionKeyword); void publishAll(String sessionkey, boolean publish); diff --git a/src/main/java/de/thm/arsnova/services/QuestionService.java b/src/main/java/de/thm/arsnova/services/QuestionService.java index ee1d5e82..7c21c525 100644 --- a/src/main/java/de/thm/arsnova/services/QuestionService.java +++ b/src/main/java/de/thm/arsnova/services/QuestionService.java @@ -41,6 +41,8 @@ import de.thm.arsnova.entities.InterposedReadingCount; import de.thm.arsnova.entities.Question; import de.thm.arsnova.entities.Session; import de.thm.arsnova.entities.User; +import de.thm.arsnova.events.DeleteAnswerEvent; +import de.thm.arsnova.events.NewAnswerEvent; import de.thm.arsnova.events.NewInterposedQuestionEvent; import de.thm.arsnova.events.NewQuestionEvent; import de.thm.arsnova.exceptions.BadRequestException; @@ -406,7 +408,8 @@ public class QuestionService implements IQuestionService, ApplicationEventPublis } final Answer result = databaseDao.saveAnswer(answer, user); - socketIoServer.reportAnswersToLecturerQuestionAvailable(question.getSessionKeyword(), question.get_id()); + final Session session = databaseDao.getSessionFromKeyword(question.getSessionKeyword()); + this.publisher.publishEvent(new NewAnswerEvent(this, result, user, question, session)); return result; } @@ -421,7 +424,8 @@ public class QuestionService implements IQuestionService, ApplicationEventPublis final Question question = getQuestion(answer.getQuestionId()); final Answer result = databaseDao.updateAnswer(answer); - socketIoServer.reportAnswersToLecturerQuestionAvailable(question.getSessionKeyword(), question.get_id()); + final Session session = databaseDao.getSessionFromKeyword(question.getSessionKeyword()); + this.publisher.publishEvent(new NewAnswerEvent(this, result, user, question, session)); return result; } @@ -440,7 +444,7 @@ public class QuestionService implements IQuestionService, ApplicationEventPublis } databaseDao.deleteAnswer(answerId); - socketIoServer.reportAnswersToLecturerQuestionAvailable(question.getSessionKeyword(), question.get_id()); + this.publisher.publishEvent(new DeleteAnswerEvent(this, question, session)); } @Override @@ -490,12 +494,30 @@ public class QuestionService implements IQuestionService, ApplicationEventPublis @Override @PreAuthorize("isAuthenticated()") public int countLectureQuestionAnswers(final String sessionkey) { + return this.countLectureQuestionAnswersInternal(sessionkey); + } + + /* + * The "internal" suffix means it is called by internal services that have no authentication! + * TODO: Find a better way of doing this... + */ + @Override + public int countLectureQuestionAnswersInternal(final String sessionkey) { return databaseDao.countLectureQuestionAnswers(getSession(sessionkey)); } @Override @PreAuthorize("isAuthenticated()") public int countPreparationQuestionAnswers(final String sessionkey) { + return this.countLectureQuestionAnswersInternal(sessionkey); + } + + /* + * The "internal" suffix means it is called by internal services that have no authentication! + * TODO: Find a better way of doing this... + */ + @Override + public int countPreparationQuestionAnswersInternal(final String sessionkey) { return databaseDao.countPreparationQuestionAnswers(getSession(sessionkey)); } @@ -524,6 +546,11 @@ public class QuestionService implements IQuestionService, ApplicationEventPublis @PreAuthorize("isAuthenticated()") public List<String> getUnAnsweredLectureQuestionIds(final String sessionkey) { final User user = getCurrentUser(); + return this.getUnAnsweredLectureQuestionIds(sessionkey, user); + } + + @Override + public List<String> getUnAnsweredLectureQuestionIds(final String sessionkey, final User user) { final Session session = getSession(sessionkey); return databaseDao.getUnAnsweredLectureQuestionIds(session, user); } @@ -532,6 +559,11 @@ public class QuestionService implements IQuestionService, ApplicationEventPublis @PreAuthorize("isAuthenticated()") public List<String> getUnAnsweredPreparationQuestionIds(final String sessionkey) { final User user = getCurrentUser(); + return this.getUnAnsweredPreparationQuestionIds(sessionkey, user); + } + + @Override + public List<String> getUnAnsweredPreparationQuestionIds(final String sessionkey, final User user) { final Session session = getSession(sessionkey); return databaseDao.getUnAnsweredPreparationQuestionIds(session, user); } diff --git a/src/main/java/de/thm/arsnova/socket/ARSnovaSocketIOServer.java b/src/main/java/de/thm/arsnova/socket/ARSnovaSocketIOServer.java index c663b351..254ddbc9 100644 --- a/src/main/java/de/thm/arsnova/socket/ARSnovaSocketIOServer.java +++ b/src/main/java/de/thm/arsnova/socket/ARSnovaSocketIOServer.java @@ -30,17 +30,20 @@ import com.corundumstudio.socketio.protocol.Packet; import com.corundumstudio.socketio.protocol.PacketType; import de.thm.arsnova.entities.InterposedQuestion; -import de.thm.arsnova.entities.Question; import de.thm.arsnova.entities.User; +import de.thm.arsnova.events.DeleteAnswerEvent; +import de.thm.arsnova.events.NewAnswerEvent; import de.thm.arsnova.events.NewInterposedQuestionEvent; import de.thm.arsnova.events.NewQuestionEvent; import de.thm.arsnova.events.NovaEvent; import de.thm.arsnova.events.NovaEventVisitor; import de.thm.arsnova.exceptions.NoContentException; import de.thm.arsnova.services.IFeedbackService; +import de.thm.arsnova.services.IQuestionService; import de.thm.arsnova.services.ISessionService; import de.thm.arsnova.services.IUserService; import de.thm.arsnova.socket.message.Feedback; +import de.thm.arsnova.socket.message.Question; import de.thm.arsnova.socket.message.Session; @Component @@ -55,6 +58,9 @@ public class ARSnovaSocketIOServer implements ApplicationListener<NovaEvent>, No @Autowired private ISessionService sessionService; + @Autowired + private IQuestionService questionService; + private static final Logger LOGGER = LoggerFactory.getLogger(ARSnovaSocketIOServer.class); private int portNumber; @@ -140,7 +146,7 @@ public class ARSnovaSocketIOServer implements ApplicationListener<NovaEvent>, No /* active user count has to be sent to the client since the broadcast is * not always sent as long as the polling solution is active simultaneously */ reportActiveUserCountForSession(session.getKeyword()); - reportSessionDataToClient(session.getKeyword(), client); + reportSessionDataToClient(session.getKeyword(), u, client); } } }); @@ -233,22 +239,11 @@ public class ARSnovaSocketIOServer implements ApplicationListener<NovaEvent>, No } public void reportDeletedFeedback(final User user, final Set<de.thm.arsnova.entities.Session> arsSessions) { - final List<UUID> connectionIds = findConnectionIdForUser(user); - if (connectionIds.isEmpty()) { - return; - } final List<String> keywords = new ArrayList<String>(); for (final de.thm.arsnova.entities.Session session : arsSessions) { keywords.add(session.getKeyword()); } - - for (final SocketIOClient client : server.getAllClients()) { - // Find the client whose feedback has been deleted and send a - // message. - if (connectionIds.contains(client.getSessionId())) { - client.sendEvent("feedbackReset", keywords); - } - } + this.sendToUser(user, "feedbackReset", keywords); } private List<UUID> findConnectionIdForUser(final User user) { @@ -263,14 +258,31 @@ public class ARSnovaSocketIOServer implements ApplicationListener<NovaEvent>, No return result; } + private void sendToUser(final User user, final String event, Object data) { + final List<UUID> connectionIds = findConnectionIdForUser(user); + if (connectionIds.isEmpty()) { + return; + } + for (final SocketIOClient client : server.getAllClients()) { + if (connectionIds.contains(client.getSessionId())) { + client.sendEvent(event, data); + } + } + } + /** * Currently only sends the feedback data to the client. Should be used for all * relevant Socket.IO data, the client needs to know after joining a session. * * @param sessionKey + * @param user * @param client */ - public void reportSessionDataToClient(final String sessionKey, final SocketIOClient client) { + public void reportSessionDataToClient(final String sessionKey, final User user, final SocketIOClient client) { + client.sendEvent("unansweredLecturerQuestions", questionService.getUnAnsweredLectureQuestionIds(sessionKey, user)); + client.sendEvent("unansweredPreparationQuestions", questionService.getUnAnsweredPreparationQuestionIds(sessionKey, user)); + client.sendEvent("countLectureQuestionAnswers", questionService.countLectureQuestionAnswersInternal(sessionKey)); + client.sendEvent("countPreparationQuestionAnswers", questionService.countPreparationQuestionAnswersInternal(sessionKey)); client.sendEvent("activeUserCountData", sessionService.activeUsers(sessionKey)); final de.thm.arsnova.entities.Feedback fb = feedbackService.getFeedback(sessionKey); client.sendEvent("feedbackData", fb.getValues()); @@ -322,8 +334,8 @@ public class ARSnovaSocketIOServer implements ApplicationListener<NovaEvent>, No broadcastInSession(sessionKey, "activeUserCountData", count); } - public void reportAnswersToLecturerQuestionAvailable(final String sessionKey, final String lecturerQuestionId) { - broadcastInSession(sessionKey, "answersToLecQuestionAvail", lecturerQuestionId); + public void reportAnswersToLecturerQuestionAvailable(final de.thm.arsnova.entities.Session session, final Question lecturerQuestion) { + broadcastInSession(session.getKeyword(), "answersToLecQuestionAvail", lecturerQuestion.get_id()); } public void reportAudienceQuestionAvailable(final de.thm.arsnova.entities.Session session, final InterposedQuestion audienceQuestion) { @@ -333,7 +345,8 @@ public class ARSnovaSocketIOServer implements ApplicationListener<NovaEvent>, No public void reportLecturerQuestionAvailable(final de.thm.arsnova.entities.Session session, final Question lecturerQuestion) { /* TODO role handling implementation, send this only to users with role audience */ - broadcastInSession(session.getKeyword(), "lecQuestionAvail", lecturerQuestion.get_id()); + broadcastInSession(session.getKeyword(), "lecQuestionAvail", lecturerQuestion.get_id()); // deprecated! + broadcastInSession(session.getKeyword(), "lecturerQuestionAvailable", lecturerQuestion); } public void reportSessionStatus(final String sessionKey, final boolean active) { @@ -358,7 +371,7 @@ public class ARSnovaSocketIOServer implements ApplicationListener<NovaEvent>, No @Override public void visit(NewQuestionEvent event) { - this.reportLecturerQuestionAvailable(event.getSession(), event.getQuestion()); + this.reportLecturerQuestionAvailable(event.getSession(), new Question(event.getQuestion())); } @Override @@ -366,6 +379,25 @@ public class ARSnovaSocketIOServer implements ApplicationListener<NovaEvent>, No this.reportAudienceQuestionAvailable(event.getSession(), event.getQuestion()); } + @Override + public void visit(NewAnswerEvent event) { + final String sessionKey = event.getSession().getKeyword(); + this.reportAnswersToLecturerQuestionAvailable(event.getSession(), new Question(event.getQuestion())); + broadcastInSession(sessionKey, "countLectureQuestionAnswers", questionService.countLectureQuestionAnswersInternal(sessionKey)); + broadcastInSession(sessionKey, "countPreparationQuestionAnswers", questionService.countPreparationQuestionAnswersInternal(sessionKey)); + sendToUser(event.getUser(), "unansweredLecturerQuestions", questionService.getUnAnsweredLectureQuestionIds(sessionKey, event.getUser())); + sendToUser(event.getUser(), "unansweredPreparationQuestions", questionService.getUnAnsweredPreparationQuestionIds(sessionKey, event.getUser())); + } + + @Override + public void visit(DeleteAnswerEvent event) { + final String sessionKey = event.getSession().getKeyword(); + this.reportAnswersToLecturerQuestionAvailable(event.getSession(), new Question(event.getQuestion())); + // We do not know which user's answer was deleted, so we can't update his 'unanswered' list of questions... + broadcastInSession(sessionKey, "countLectureQuestionAnswers", questionService.countLectureQuestionAnswersInternal(sessionKey)); + broadcastInSession(sessionKey, "countPreparationQuestionAnswers", questionService.countPreparationQuestionAnswersInternal(sessionKey)); + } + @Override public void onApplicationEvent(NovaEvent event) { event.accept(this); diff --git a/src/main/java/de/thm/arsnova/socket/message/Question.java b/src/main/java/de/thm/arsnova/socket/message/Question.java new file mode 100644 index 00000000..8b6947a1 --- /dev/null +++ b/src/main/java/de/thm/arsnova/socket/message/Question.java @@ -0,0 +1,20 @@ +package de.thm.arsnova.socket.message; + +public class Question { + + private final String _id; + private final String variant; + + public Question(de.thm.arsnova.entities.Question question) { + this._id = question.get_id(); + this.variant = question.getQuestionVariant(); + } + + public String get_id() { + return _id; + } + + public String getVariant() { + return variant; + } +} -- GitLab