diff --git a/src/main/java/de/thm/arsnova/services/IUserService.java b/src/main/java/de/thm/arsnova/services/IUserService.java index 4e94a08f74ac76e0c79f718622712be502efa29a..735815357b6cab781051a5c7da110ce0d9a65bc4 100644 --- a/src/main/java/de/thm/arsnova/services/IUserService.java +++ b/src/main/java/de/thm/arsnova/services/IUserService.java @@ -44,4 +44,8 @@ public interface IUserService { void addCurrentUserToSessionMap(String keyword); String getSessionForUser(String username); + + void addUserToSessionBySocketId(UUID socketId, String keyword); + + void removeUserFromSessionBySocketId(UUID socketId); } diff --git a/src/main/java/de/thm/arsnova/services/SessionService.java b/src/main/java/de/thm/arsnova/services/SessionService.java index a568c205f769714702e9f81420de2c623842fd9f..52343f340ec87dcba53cfbc308487bc8eabc4426 100644 --- a/src/main/java/de/thm/arsnova/services/SessionService.java +++ b/src/main/java/de/thm/arsnova/services/SessionService.java @@ -38,6 +38,7 @@ import de.thm.arsnova.entities.LoggedIn; import de.thm.arsnova.entities.Session; import de.thm.arsnova.entities.User; import de.thm.arsnova.exceptions.ForbiddenException; +import de.thm.arsnova.socket.ARSnovaSocketIOServer; @Service public class SessionService implements ISessionService { @@ -49,7 +50,10 @@ public class SessionService implements ISessionService { @Autowired private IUserService userService; - + + @Autowired + private ARSnovaSocketIOServer socketIoServer; + @Autowired(required=false) private ConnectorClient connectorClient; @@ -61,15 +65,16 @@ public class SessionService implements ISessionService { @Authenticated public final Session joinSession(final String keyword) { userService.addCurrentUserToSessionMap(keyword); + socketIoServer.reportActiveUserCountForSession(keyword); + Session session = databaseDao.getSession(keyword); - if (connectorClient != null && session.isCourseSession()) { String courseid = session.getCourseId(); if (!connectorClient.getMembership(userService.getCurrentUser().getUsername(), courseid).isMember()) { throw new ForbiddenException(); } } - + return session; } diff --git a/src/main/java/de/thm/arsnova/services/UserService.java b/src/main/java/de/thm/arsnova/services/UserService.java index d4f2660af9ca648e05d15a58e818977c52321396..b2e0dcdb31b035c8d970632ee23ab55cefafe9d3 100644 --- a/src/main/java/de/thm/arsnova/services/UserService.java +++ b/src/main/java/de/thm/arsnova/services/UserService.java @@ -41,8 +41,13 @@ public class UserService implements IUserService, InitializingBean, DisposableBe public static final Logger LOGGER = LoggerFactory.getLogger(UserService.class); private static final ConcurrentHashMap<UUID, User> socketid2user = new ConcurrentHashMap<UUID, User>(); + + /* used for Socket.IO online check solution (new) */ private static final ConcurrentHashMap<User, String> user2session = new ConcurrentHashMap<User, String>(); + /* used for HTTP polling online check solution (legacy) */ + private static final ConcurrentHashMap<User, String> user2sessionLegacy = new ConcurrentHashMap<User, String>(); + @Override public User getCurrentUser() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); @@ -106,7 +111,7 @@ public class UserService implements IUserService, InitializingBean, DisposableBe public boolean isUserInSession(User user, String keyword) { if (keyword == null) return false; - String session = user2session.get(user); + String session = user2sessionLegacy.get(user); if (session == null) return false; return keyword.equals(session); @@ -120,6 +125,12 @@ public class UserService implements IUserService, InitializingBean, DisposableBe result.add(e.getKey()); } } + for (Entry<User, String> e : user2sessionLegacy.entrySet()) { + if (e.getValue().equals(keyword)) { + result.add(e.getKey()); + } + } + return result; } @@ -129,14 +140,37 @@ public class UserService implements IUserService, InitializingBean, DisposableBe User user = getCurrentUser(); if (user == null) throw new UnauthorizedException(); + LOGGER.info("Mapping user " + user.getUsername() + " to session " + keyword + " [legacy]."); + user2sessionLegacy.put(user, keyword); + } + + @Override + @Transactional(isolation = Isolation.READ_COMMITTED) + public void addUserToSessionBySocketId(UUID socketId, String keyword) { + User user = socketid2user.get(socketId); + LOGGER.info("Mapping user " + user.getUsername() + " to session " + keyword + "."); user2session.put(user, keyword); } + @Override + @Transactional(isolation = Isolation.READ_COMMITTED) + public void removeUserFromSessionBySocketId(UUID socketId) { + user2session.remove(socketId); + } + @Override public String getSessionForUser(String username) { for (Entry<User, String> entry : user2session.entrySet()) { - if (entry.getKey().getUsername().equals(username)) return entry.getValue(); + if (entry.getKey().getUsername().equals(username)) { + return entry.getValue(); + } + } + for (Entry<User, String> entry : user2sessionLegacy.entrySet()) { + if (entry.getKey().getUsername().equals(username)) { + return entry.getValue(); + } } + return null; } @@ -157,7 +191,7 @@ public class UserService implements IUserService, InitializingBean, DisposableBe LOGGER.info("load from store: {}", map); socketid2user.putAll(s2u); - user2session.putAll(u2s); + user2sessionLegacy.putAll(u2s); } catch (IOException e) { LOGGER.error("IOException during restoring UserService", e); @@ -170,7 +204,7 @@ public class UserService implements IUserService, InitializingBean, DisposableBe public void destroy() { Hashtable<String, Map<?, ?>> map = new Hashtable<String, Map<?, ?>>(); map.put("socketid2user", socketid2user); - map.put("user2session", user2session); + map.put("user2session", user2sessionLegacy); try { File tmpDir = new File(System.getProperty("java.io.tmpdir")); diff --git a/src/main/java/de/thm/arsnova/socket/ARSnovaSocketIOServer.java b/src/main/java/de/thm/arsnova/socket/ARSnovaSocketIOServer.java index 6c8ad2c9c36973e53bb84efe75c0dd519888e985..9ce63e7fe3568f80417b3307ec483663fe420afc 100644 --- a/src/main/java/de/thm/arsnova/socket/ARSnovaSocketIOServer.java +++ b/src/main/java/de/thm/arsnova/socket/ARSnovaSocketIOServer.java @@ -26,8 +26,10 @@ import de.thm.arsnova.entities.Question; import de.thm.arsnova.entities.User; 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.Session; public class ARSnovaSocketIOServer { @@ -40,6 +42,9 @@ public class ARSnovaSocketIOServer { @Autowired private IUserService userService; + @Autowired + private ISessionService sessionService; + private final Logger logger = LoggerFactory.getLogger(getClass()); private int portNumber; @@ -49,6 +54,8 @@ public class ARSnovaSocketIOServer { private String storepass; private final Configuration config; private SocketIOServer server; + + private int lastActiveUserCount = 0; public ARSnovaSocketIOServer() { config = new Configuration(); @@ -93,6 +100,15 @@ public class ARSnovaSocketIOServer { } }); + server.addEventListener("setSession", Session.class, new DataListener<Session>() { + @Override + public void onData(SocketIOClient client, Session session, AckRequest ackSender) { + userService.addUserToSessionBySocketId(client.getSessionId(), session.getKeyword()); + reportActiveUserCountForSession(session.getKeyword()); + reportSessionDataToClient(session.getKeyword(), client); + } + }); + server.addEventListener("arsnova/question/create", Question.class, new DataListener<Question>() { @Override public void onData(SocketIOClient client, Question question, AckRequest ackSender) { @@ -111,6 +127,11 @@ public class ARSnovaSocketIOServer { @Override public void onDisconnect(SocketIOClient client) { logger.info("addDisconnectListener.onDisconnect: Client: {}", new Object[] { client }); + String sessionKey = userService.getSessionForUser( + userService.getUser2SocketId(client.getSessionId()).getUsername() + ); + reportActiveUserCountForSession(sessionKey); + userService.removeUserFromSessionBySocketId(client.getSessionId()); userService.removeUser2SocketId(client.getSessionId()); } }); @@ -196,21 +217,51 @@ public class ARSnovaSocketIOServer { return result; } - public void reportUpdatedFeedbackForSession(String session) { + /** + * 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 client + */ + public void reportSessionDataToClient(String sessionKey, SocketIOClient client) { + de.thm.arsnova.entities.Feedback fb = feedbackService.getFeedback(sessionKey); + client.sendEvent("updateFeedback", fb.getValues()); + + /* updateActiveUserCount does not need to be send since it is broadcasted + * after the client joined the session */ + } + + public void reportUpdatedFeedbackForSession(String sessionKey) { + de.thm.arsnova.entities.Feedback fb = feedbackService.getFeedback(sessionKey); + broadcastInSession(sessionKey, "updateFeedback", fb.getValues()); + } + + public void reportActiveUserCountForSession(String sessionKey) { + int count = sessionService.countActiveUsers(sessionKey); + if (count == lastActiveUserCount) { + return; + } + lastActiveUserCount = count; + broadcastInSession(sessionKey, "updateActiveUserCount", count); + } + + public void broadcastInSession(String sessionKey, String eventName, Object data) { + logger.info("Broadcasting " + eventName + " for session " + sessionKey + "."); + /** * collect a list of users which are in the current session iterate over * all connected clients and if send feedback, if user is in current * session */ - List<User> users = userService.getUsersInSession(session); - de.thm.arsnova.entities.Feedback fb = feedbackService.getFeedback(session); + List<User> users = userService.getUsersInSession(sessionKey); for (SocketIOClient c : server.getAllClients()) { User u = userService.getUser2SocketId(c.getSessionId()); if (u != null && users.contains(u)) { logger.info("sending out to client {}, username is: {}, current session is: {}", - new Object[] { c.getSessionId(), u.getUsername(), session }); - c.sendEvent("updateFeedback", fb.getValues()); + new Object[] { c.getSessionId(), u.getUsername(), sessionKey }); + c.sendEvent(eventName, data); } } } diff --git a/src/main/java/de/thm/arsnova/socket/message/Session.java b/src/main/java/de/thm/arsnova/socket/message/Session.java new file mode 100644 index 0000000000000000000000000000000000000000..3b56fe50c035f2c77d19c9d8db581755e249fc01 --- /dev/null +++ b/src/main/java/de/thm/arsnova/socket/message/Session.java @@ -0,0 +1,13 @@ +package de.thm.arsnova.socket.message; + +public class Session { + private String keyword; + + public String getKeyword() { + return keyword; + } + + public void setKeyword(String keyword) { + this.keyword = keyword; + } +}