Index: lams_learning/src/java/org/lamsfoundation/lams/learning/presence/PresenceWebsocketServer.java =================================================================== diff -u -r7475d08afc280b5e2e5ddf04e8bf35e3166aaf80 -rc0afa34ef8004a73b35f396bfb30285043ebce31 --- lams_learning/src/java/org/lamsfoundation/lams/learning/presence/PresenceWebsocketServer.java (.../PresenceWebsocketServer.java) (revision 7475d08afc280b5e2e5ddf04e8bf35e3166aaf80) +++ lams_learning/src/java/org/lamsfoundation/lams/learning/presence/PresenceWebsocketServer.java (.../PresenceWebsocketServer.java) (revision c0afa34ef8004a73b35f396bfb30285043ebce31) @@ -24,6 +24,11 @@ import org.lamsfoundation.lams.learning.presence.model.PresenceChatMessage; import org.lamsfoundation.lams.learning.presence.model.PresenceChatUser; import org.lamsfoundation.lams.learning.presence.service.IPresenceChatService; +import org.lamsfoundation.lams.lesson.Lesson; +import org.lamsfoundation.lams.lesson.service.ILessonService; +import org.lamsfoundation.lams.security.ISecurityService; +import org.lamsfoundation.lams.usermanagement.User; +import org.lamsfoundation.lams.usermanagement.service.IUserManagementService; import org.lamsfoundation.lams.util.JsonUtil; import org.lamsfoundation.lams.util.hibernate.HibernateSessionManager; import org.lamsfoundation.lams.web.session.SessionManager; @@ -45,20 +50,6 @@ public class PresenceWebsocketServer { /** - * Identifies a single connection. There can be more than one connection for the same user: multiple windows open or - * the same user in an another role. - */ - private static class Websocket { - private final Session session; - private final String nickName; - - private Websocket(Session session) { - this.session = session; - this.nickName = session.getRequestParameterMap().get("nickname").get(0); - } - } - - /** * A singleton which updates Learners with messages and roster. */ private static class SendWorker extends Thread { @@ -74,11 +65,11 @@ try { // websocket communication bypasses standard HTTP filters, so Hibernate session needs to be initialised manually HibernateSessionManager.openSession(); - Iterator>> entryIterator = PresenceWebsocketServer.websockets.entrySet() + Iterator>> lessonIterator = PresenceWebsocketServer.websockets.entrySet() .iterator(); // go through lessons and update registered learners with messages and roster - while (entryIterator.hasNext()) { - Entry> entry = entryIterator.next(); + while (lessonIterator.hasNext()) { + Entry> entry = lessonIterator.next(); Long lessonId = entry.getKey(); Long lastSendTime = lastSendTimes.get(lessonId); if ((lastSendTime == null) @@ -87,9 +78,9 @@ } // if all learners left the chat, remove the obsolete mapping - Set lessonWebsockets = entry.getValue(); + Set lessonWebsockets = entry.getValue(); if (lessonWebsockets.isEmpty()) { - entryIterator.remove(); + lessonIterator.remove(); PresenceWebsocketServer.rosters.remove(lessonId); lastSendTimes.remove(lessonId); } @@ -127,13 +118,14 @@ lastSendTimes.put(lessonId, System.currentTimeMillis()); } - Set lessonWebsockets = PresenceWebsocketServer.websockets.get(lessonId); + Set lessonWebsockets = PresenceWebsocketServer.websockets.get(lessonId); Roster roster = PresenceWebsocketServer.rosters.get(lessonId); try { ArrayNode rosterJSON = roster.getRosterJSON(); - for (Websocket websocket : lessonWebsockets) { + for (Session websocket : lessonWebsockets) { // if this run is meant only for one learner, skip the others - if ((nickName != null) && !nickName.equals(websocket.nickName)) { + String websocketNickName = (String) websocket.getUserProperties().get(PARAM_NICKNAME); + if ((nickName != null) && !nickName.equals(websocketNickName)) { continue; } @@ -142,14 +134,14 @@ // if it is just roster, skip messages if (roster.imEnabled) { - ArrayNode messagesJSON = PresenceWebsocketServer.filterMessages(messages, websocket.nickName); + ArrayNode messagesJSON = PresenceWebsocketServer.filterMessages(messages, websocketNickName); responseJSON.set("messages", messagesJSON); } responseJSON.set("roster", rosterJSON); // send the payload to the Learner's browser - if (websocket.session.isOpen()) { - websocket.session.getBasicRemote().sendText(responseJSON.toString()); + if (websocket.isOpen()) { + websocket.getBasicRemote().sendText(responseJSON.toString()); } } @@ -185,10 +177,10 @@ */ private ArrayNode getRosterJSON() throws JsonProcessingException, IOException { Set localActiveUsers = new TreeSet<>(); - Set sessionWebsockets = PresenceWebsocketServer.websockets.get(lessonId); + Set lessonWebsockets = PresenceWebsocketServer.websockets.get(lessonId); // find out who is active locally - for (Websocket websocket : sessionWebsockets) { - localActiveUsers.add(websocket.nickName); + for (Session websocket : lessonWebsockets) { + localActiveUsers.add((String) websocket.getUserProperties().get(PARAM_NICKNAME)); } // is it time to sync with the DB yet? @@ -218,11 +210,16 @@ private static Logger log = Logger.getLogger(PresenceWebsocketServer.class); + private static final String PARAM_NICKNAME = "nickname"; + private static IPresenceChatService presenceChatService; + private static ISecurityService securityService; + private static ILessonService lessonService; + private static IUserManagementService userManagementService; private static final SendWorker sendWorker = new SendWorker(); private static final Map rosters = new ConcurrentHashMap<>(); - private static final Map> websockets = new ConcurrentHashMap<>(); + private static final Map> websockets = new ConcurrentHashMap<>(); static { // run the singleton thread @@ -233,64 +230,74 @@ * Registeres the Learner for processing by SendWorker. */ @OnOpen - public void registerUser(Session session) throws IOException { - Long lessonId = Long.valueOf(session.getRequestParameterMap().get(AttributeNames.PARAM_LESSON_ID).get(0)); - Set sessionWebsockets = PresenceWebsocketServer.websockets.get(lessonId); - if (sessionWebsockets == null) { - sessionWebsockets = ConcurrentHashMap.newKeySet(); - PresenceWebsocketServer.websockets.put(lessonId, sessionWebsockets); + public void registerUser(Session websocket) throws IOException { + Long lessonId = Long.valueOf(websocket.getRequestParameterMap().get(AttributeNames.PARAM_LESSON_ID).get(0)); + + String login = websocket.getUserPrincipal().getName(); + User user = PresenceWebsocketServer.getUserManagementService().getUserByLogin(login); + + String nickname = user.getFirstName() + " " + user.getLastName(); + websocket.getUserProperties().put(PARAM_NICKNAME, nickname); + websocket.getUserProperties().put(AttributeNames.PARAM_LESSON_ID, lessonId); + + PresenceWebsocketServer.getSecurityService().isLessonParticipant(lessonId, user.getUserId(), "join lesson chat", + true); + + Set lessonWebsockets = PresenceWebsocketServer.websockets.get(lessonId); + if (lessonWebsockets == null) { + lessonWebsockets = ConcurrentHashMap.newKeySet(); + PresenceWebsocketServer.websockets.put(lessonId, lessonWebsockets); } - Websocket websocket = new Websocket(session); - sessionWebsockets.add(websocket); + lessonWebsockets.add(websocket); Roster roster = PresenceWebsocketServer.rosters.get(lessonId); if (roster == null) { - boolean imEnabled = Boolean.valueOf(session.getRequestParameterMap().get("imEnabled").get(0)); + Lesson lesson = PresenceWebsocketServer.getLessonService().getLesson(lessonId); // build a new roster object - roster = new Roster(lessonId, imEnabled); + roster = new Roster(lessonId, lesson.getLearnerImAvailable()); PresenceWebsocketServer.rosters.put(lessonId, roster); } new Thread(() -> { try { // websocket communication bypasses standard HTTP filters, so Hibernate session needs to be initialised manually HibernateSessionManager.openSession(); - SendWorker.send(lessonId, websocket.nickName); + SendWorker.send(lessonId, nickname); } finally { HibernateSessionManager.closeSession(); } }).start(); if (PresenceWebsocketServer.log.isDebugEnabled()) { PresenceWebsocketServer.log - .debug("User " + websocket.nickName + " entered Presence Chat with lesson ID: " + lessonId); + .debug("User " + nickname + " entered Presence Chat with lesson ID: " + lessonId); } } /** * If there was something wrong with the connection, put it into logs. */ @OnClose - public void unregisterUser(Session session, CloseReason reason) { - Long lessonId = Long.valueOf(session.getRequestParameterMap().get(AttributeNames.PARAM_LESSON_ID).get(0)); - Set lessonWebsockets = PresenceWebsocketServer.websockets.get(lessonId); - Iterator websocketIterator = lessonWebsockets.iterator(); - while (websocketIterator.hasNext()) { - Websocket websocket = websocketIterator.next(); - if (websocket.session.equals(session)) { - websocketIterator.remove(); + public void unregisterUser(Session websocket, CloseReason reason) { + Long lessonId = (Long) websocket.getUserProperties().get(AttributeNames.PARAM_LESSON_ID); + Set lessonWebsockets = PresenceWebsocketServer.websockets.get(lessonId); + Iterator sessionIterator = lessonWebsockets.iterator(); + while (sessionIterator.hasNext()) { + Session storedSession = sessionIterator.next(); + if (storedSession.equals(websocket)) { + sessionIterator.remove(); break; } } if (PresenceWebsocketServer.log.isDebugEnabled()) { - PresenceWebsocketServer.log.debug( - "User " + session.getUserPrincipal().getName() + " left Presence Chat with lessonId: " + lessonId - + (!(reason.getCloseCode().equals(CloseCodes.GOING_AWAY) - || reason.getCloseCode().equals(CloseCodes.NORMAL_CLOSURE)) - ? ". Abnormal close. Code: " + reason.getCloseCode() + ". Reason: " - + reason.getReasonPhrase() - : "")); + PresenceWebsocketServer.log.debug("User " + websocket.getUserProperties().get(PARAM_NICKNAME) + + " left Presence Chat with lessonId: " + lessonId + + (!(reason.getCloseCode().equals(CloseCodes.GOING_AWAY) + || reason.getCloseCode().equals(CloseCodes.NORMAL_CLOSURE)) + ? ". Abnormal close. Code: " + reason.getCloseCode() + ". Reason: " + + reason.getReasonPhrase() + : "(unknown)")); } } @@ -300,7 +307,7 @@ * @throws IOException */ @OnMessage - public void receiveRequest(String input, Session session) throws IOException { + public void receiveRequest(String input, Session websocket) throws IOException { if (StringUtils.isBlank(input)) { return; } @@ -312,27 +319,27 @@ ObjectNode requestJSON = JsonUtil.readObject(input); switch (JsonUtil.optString(requestJSON, "type")) { case "message": - PresenceWebsocketServer.storeMessage(requestJSON, session); + PresenceWebsocketServer.storeMessage(requestJSON, websocket); break; case "fetchConversation": - PresenceWebsocketServer.sendConversation(requestJSON, session); + PresenceWebsocketServer.sendConversation(requestJSON, websocket); break; } } /** * Stores a message sent by a Learner. */ - private static void storeMessage(ObjectNode requestJSON, Session session) { + private static void storeMessage(ObjectNode requestJSON, Session websocket) { String message = JsonUtil.optString(requestJSON, "message"); if (StringUtils.isBlank(message)) { return; } - Long lessonId = JsonUtil.optLong(requestJSON, "lessonID"); + Long lessonId = (Long) websocket.getUserProperties().get(AttributeNames.PARAM_LESSON_ID); // websocket communication bypasses standard HTTP filters, so Hibernate session needs to be initialised manually - String from = session.getRequestParameterMap().get("nickname").get(0); + String from = (String) websocket.getUserProperties().get(PARAM_NICKNAME); String to = JsonUtil.optString(requestJSON, "to"); if (StringUtils.isBlank(to)) { to = null; @@ -353,11 +360,11 @@ /** * Sends the whole message history between the current and the chosen learner. */ - private static void sendConversation(ObjectNode requestJSON, Session session) throws IOException { - Long lessonId = JsonUtil.optLong(requestJSON, "lessonID"); + private static void sendConversation(ObjectNode requestJSON, Session websocket) throws IOException { + Long lessonId = (Long) websocket.getUserProperties().get(AttributeNames.PARAM_LESSON_ID); String to = JsonUtil.optString(requestJSON, "to"); - String from = session.getRequestParameterMap().get("nickname").get(0); + String from = (String) websocket.getUserProperties().get(PARAM_NICKNAME); // websocket communication bypasses standard HTTP filters, so Hibernate session needs to be initialised manually new Thread(() -> { @@ -375,7 +382,7 @@ ObjectNode responseJSON = JsonNodeFactory.instance.objectNode(); responseJSON.set("messages", messagesJSON); // send the payload to the Learner's browser - session.getBasicRemote().sendText(responseJSON.toString()); + websocket.getBasicRemote().sendText(responseJSON.toString()); } catch (Exception e) { log.error("Error while seding conversation", e); } finally { @@ -412,14 +419,14 @@ } public static int getActiveUserCount(long lessonId) { - Set lessonWebsockets = PresenceWebsocketServer.websockets.get(lessonId); + Set lessonWebsockets = PresenceWebsocketServer.websockets.get(lessonId); if (lessonWebsockets == null) { return 0; } // there can be few websockets (browser windows) for a single learner Set activeNicknames = new TreeSet<>(); - for (Websocket websocket : lessonWebsockets) { - activeNicknames.add(websocket.nickName); + for (Session websocket : lessonWebsockets) { + activeNicknames.add((String) websocket.getUserProperties().get(PARAM_NICKNAME)); } return activeNicknames.size(); } @@ -432,4 +439,32 @@ } return PresenceWebsocketServer.presenceChatService; } + + private static ISecurityService getSecurityService() { + if (PresenceWebsocketServer.securityService == null) { + WebApplicationContext ctx = WebApplicationContextUtils + .getWebApplicationContext(SessionManager.getServletContext()); + PresenceWebsocketServer.securityService = (ISecurityService) ctx.getBean("securityService"); + } + return PresenceWebsocketServer.securityService; + } + + private static ILessonService getLessonService() { + if (PresenceWebsocketServer.lessonService == null) { + WebApplicationContext ctx = WebApplicationContextUtils + .getWebApplicationContext(SessionManager.getServletContext()); + PresenceWebsocketServer.lessonService = (ILessonService) ctx.getBean("lessonService"); + } + return PresenceWebsocketServer.lessonService; + } + + private static IUserManagementService getUserManagementService() { + if (PresenceWebsocketServer.userManagementService == null) { + WebApplicationContext ctx = WebApplicationContextUtils + .getWebApplicationContext(SessionManager.getServletContext()); + PresenceWebsocketServer.userManagementService = (IUserManagementService) ctx + .getBean("userManagementService"); + } + return PresenceWebsocketServer.userManagementService; + } } \ No newline at end of file Index: lams_learning/src/java/org/lamsfoundation/lams/learning/presence/dao/IPresenceChatDAO.java =================================================================== diff -u -r7475d08afc280b5e2e5ddf04e8bf35e3166aaf80 -rc0afa34ef8004a73b35f396bfb30285043ebce31 --- lams_learning/src/java/org/lamsfoundation/lams/learning/presence/dao/IPresenceChatDAO.java (.../IPresenceChatDAO.java) (revision 7475d08afc280b5e2e5ddf04e8bf35e3166aaf80) +++ lams_learning/src/java/org/lamsfoundation/lams/learning/presence/dao/IPresenceChatDAO.java (.../IPresenceChatDAO.java) (revision c0afa34ef8004a73b35f396bfb30285043ebce31) @@ -21,18 +21,18 @@ * **************************************************************** */ - - package org.lamsfoundation.lams.learning.presence.dao; import java.util.Date; import java.util.List; +import org.lamsfoundation.lams.dao.IBaseDAO; import org.lamsfoundation.lams.learning.presence.model.PresenceChatMessage; import org.lamsfoundation.lams.learning.presence.model.PresenceChatUser; -public interface IPresenceChatDAO { +public interface IPresenceChatDAO extends IBaseDAO { + @Override void insertOrUpdate(Object object); List getNewMessages(Long lessonId, Date lastCheck); Index: lams_learning/web/includes/javascript/presence.js =================================================================== diff -u -r7475d08afc280b5e2e5ddf04e8bf35e3166aaf80 -rc0afa34ef8004a73b35f396bfb30285043ebce31 --- lams_learning/web/includes/javascript/presence.js (.../presence.js) (revision 7475d08afc280b5e2e5ddf04e8bf35e3166aaf80) +++ lams_learning/web/includes/javascript/presence.js (.../presence.js) (revision c0afa34ef8004a73b35f396bfb30285043ebce31) @@ -92,9 +92,7 @@ presenceWebsocketInitTime = Date.now(), // init the connection with server using server URL but with different protocol - presenceWebsocket = new WebSocket(APP_URL.replace('http', 'ws') + 'presenceChatWebsocket?lessonID=' + lessonId - + '&imEnabled=' + presenceImEnabled - + '&nickname=' + encodeURIComponent(nickname)), + presenceWebsocket = new WebSocket(APP_URL.replace('http', 'ws') + 'presenceChatWebsocket?lessonID=' + lessonId), presenceWebsocketPingTimeout = null, presenceWebsocketPingFunc = null; @@ -242,7 +240,6 @@ roster.lastMessageUids[nick] = null; var data = { 'type' : 'fetchConversation', - 'lessonID' : lessonId, 'to' : nick }; @@ -303,7 +300,6 @@ var data = { 'type' : 'message', - 'lessonID' : lessonId, 'to' : tag == groupChatInfo.tag ? '' : receiver, 'message' : message }; Index: lams_learning/web/presenceChat.jsp =================================================================== diff -u -r7475d08afc280b5e2e5ddf04e8bf35e3166aaf80 -rc0afa34ef8004a73b35f396bfb30285043ebce31 --- lams_learning/web/presenceChat.jsp (.../presenceChat.jsp) (revision 7475d08afc280b5e2e5ddf04e8bf35e3166aaf80) +++ lams_learning/web/presenceChat.jsp (.../presenceChat.jsp) (revision c0afa34ef8004a73b35f396bfb30285043ebce31) @@ -28,8 +28,7 @@ <%-- Learner interface uses attribues, Monitor uses parameters --%> presenceEnabled = ${param.presenceEnabledPatch eq 'true' or presenceEnabledPatch}, presenceShown = ${param.presenceShown eq 'true' or presenceShown}, - presenceImEnabled = ${param.presenceImEnabled eq 'true' or presenceImEnabled}, - nickname = ' ', + nickname = '' + ' ' + '', // labels used in JS file labelSend = '', labelUsers = '',