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