Index: lams_tool_scratchie/.classpath =================================================================== diff -u -r0fdf00ad8ffebc0cc6d79de96a216c08ce0d4cdf -r63ff5aaa19e3fb53810266ade4fc2e9433c09d44 --- lams_tool_scratchie/.classpath (.../.classpath) (revision 0fdf00ad8ffebc0cc6d79de96a216c08ce0d4cdf) +++ lams_tool_scratchie/.classpath (.../.classpath) (revision 63ff5aaa19e3fb53810266ade4fc2e9433c09d44) @@ -23,5 +23,6 @@ + Index: lams_tool_scratchie/build.xml =================================================================== diff -u -rded32af47a9de99cb3319fdd7461906323f7e293 -r63ff5aaa19e3fb53810266ade4fc2e9433c09d44 --- lams_tool_scratchie/build.xml (.../build.xml) (revision ded32af47a9de99cb3319fdd7461906323f7e293) +++ lams_tool_scratchie/build.xml (.../build.xml) (revision 63ff5aaa19e3fb53810266ade4fc2e9433c09d44) @@ -2,5 +2,23 @@ - + + + + + + ${ant.project.name}: Copying additional Java classes to WAR + + \ No newline at end of file Index: lams_tool_scratchie/conf/xdoclet/struts-actions.xml =================================================================== diff -u -rf4345e17e20270c029b9a58e3efb2ac4071894de -r63ff5aaa19e3fb53810266ade4fc2e9433c09d44 --- lams_tool_scratchie/conf/xdoclet/struts-actions.xml (.../struts-actions.xml) (revision f4345e17e20270c029b9a58e3efb2ac4071894de) +++ lams_tool_scratchie/conf/xdoclet/struts-actions.xml (.../struts-actions.xml) (revision 63ff5aaa19e3fb53810266ade4fc2e9433c09d44) @@ -167,19 +167,6 @@ - - - - - - - Index: lams_tool_scratchie/src/java/org/lamsfoundation/lams/tool/scratchie/web/action/LearningAction.java =================================================================== diff -u -rf4345e17e20270c029b9a58e3efb2ac4071894de -r63ff5aaa19e3fb53810266ade4fc2e9433c09d44 --- lams_tool_scratchie/src/java/org/lamsfoundation/lams/tool/scratchie/web/action/LearningAction.java (.../LearningAction.java) (revision f4345e17e20270c029b9a58e3efb2ac4071894de) +++ lams_tool_scratchie/src/java/org/lamsfoundation/lams/tool/scratchie/web/action/LearningAction.java (.../LearningAction.java) (revision 63ff5aaa19e3fb53810266ade4fc2e9433c09d44) @@ -88,19 +88,13 @@ @Override public ActionForward execute(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) - throws IOException, ServletException, JSONException, ScratchieApplicationException { + throws IOException, ServletException, JSONException, ScratchieApplicationException { String param = mapping.getParameter(); // -----------------------Scratchie Learner function --------------------------- if (param.equals("start")) { return start(mapping, form, request, response); } - if (param.equals("refreshQuestionList")) { - return refreshQuestionList(mapping, form, request, response); - } - if (param.equals("checkLeaderSubmittedNotebook")) { - return checkLeaderSubmittedNotebook(mapping, form, request, response); - } if (param.equals("recordItemScratched")) { return recordItemScratched(mapping, form, request, response); } @@ -343,36 +337,6 @@ } /** - * Refresh - * - * @throws ScratchieApplicationException - */ - private ActionForward refreshQuestionList(ActionMapping mapping, ActionForm form, HttpServletRequest request, - HttpServletResponse response) throws ScratchieApplicationException { - initializeScratchieService(); - String sessionMapID = request.getParameter(ScratchieConstants.ATTR_SESSION_MAP_ID); - SessionMap sessionMap = (SessionMap) request.getSession().getAttribute(sessionMapID); - request.setAttribute(ScratchieConstants.ATTR_SESSION_MAP_ID, sessionMapID); - - Long toolSessionId = (Long) sessionMap.get(AttributeNames.PARAM_TOOL_SESSION_ID); - ScratchieSession toolSession = LearningAction.service.getScratchieSessionBySessionId(toolSessionId); - - // set scratched flag for display purpose - Collection items = LearningAction.service.getItemsWithIndicatedScratches(toolSessionId); - sessionMap.put(ScratchieConstants.ATTR_ITEM_LIST, items); - - // refresh leadership status - ScratchieUser user = this.getCurrentUser(toolSessionId); - boolean isUserLeader = toolSession.isUserGroupLeader(user.getUid()); - sessionMap.put(ScratchieConstants.ATTR_IS_USER_LEADER, isUserLeader); - - // refresh ScratchingFinished status - sessionMap.put(ScratchieConstants.ATTR_IS_SCRATCHING_FINISHED, toolSession.isScratchingFinished()); - - return mapping.findForward(ScratchieConstants.SUCCESS); - } - - /** * Return whether leader still needs submit notebook. */ private ActionForward checkLeaderSubmittedNotebook(ActionMapping mapping, ActionForm form, @@ -477,9 +441,11 @@ * @param response * @return * @throws ScratchieApplicationException + * @throws IOException + * @throws JSONException */ private ActionForward showResults(ActionMapping mapping, ActionForm form, HttpServletRequest request, - HttpServletResponse response) throws ScratchieApplicationException { + HttpServletResponse response) throws ScratchieApplicationException, JSONException, IOException { initializeScratchieService(); // get back SessionMap String sessionMapID = request.getParameter(ScratchieConstants.ATTR_SESSION_MAP_ID); @@ -498,6 +464,8 @@ // see Next Activity button if (toolSession.isUserGroupLeader(userUid) && !toolSession.isScratchingFinished()) { LearningAction.service.setScratchingFinished(toolSessionId); + + LearningWebsocketServer.sendCloseRequest(toolSessionId); } // get updated score from ScratchieSession @@ -509,7 +477,8 @@ // display other groups' BurningQuestions if (isBurningQuestionsEnabled) { Scratchie scratchie = toolSession.getScratchie(); - List burningQuestionItemDtos = LearningAction.service.getBurningQuestionDtos(scratchie, toolSessionId); + List burningQuestionItemDtos = LearningAction.service + .getBurningQuestionDtos(scratchie, toolSessionId); request.setAttribute(ScratchieConstants.ATTR_BURNING_QUESTION_ITEM_DTOS, burningQuestionItemDtos); } @@ -539,15 +508,16 @@ return mapping.findForward(ScratchieConstants.SUCCESS); } - + /** - * @throws ServletException - * @throws ScratchieApplicationException + * @throws ServletException + * @throws ScratchieApplicationException */ private synchronized ActionForward like(ActionMapping mapping, ActionForm form, HttpServletRequest request, - HttpServletResponse response) throws JSONException, IOException, ServletException, ScratchieApplicationException { + HttpServletResponse response) + throws JSONException, IOException, ServletException, ScratchieApplicationException { initializeScratchieService(); - + String sessionMapID = WebUtil.readStrParam(request, ScratchieConstants.ATTR_SESSION_MAP_ID); SessionMap sessionMap = (SessionMap) request.getSession() .getAttribute(sessionMapID); @@ -570,15 +540,16 @@ response.getWriter().print(JSONObject); return null; } - + /** - * @throws ServletException - * @throws ScratchieApplicationException + * @throws ServletException + * @throws ScratchieApplicationException */ private synchronized ActionForward removeLike(ActionMapping mapping, ActionForm form, HttpServletRequest request, - HttpServletResponse response) throws JSONException, IOException, ServletException, ScratchieApplicationException { + HttpServletResponse response) + throws JSONException, IOException, ServletException, ScratchieApplicationException { initializeScratchieService(); - + String sessionMapID = WebUtil.readStrParam(request, ScratchieConstants.ATTR_SESSION_MAP_ID); SessionMap sessionMap = (SessionMap) request.getSession() .getAttribute(sessionMapID); @@ -713,11 +684,11 @@ ActionRedirect redirect; if (scratchie.isReflectOnActivity() && !isNotebookSubmitted) { redirect = new ActionRedirect(mapping.findForwardConfig("newReflection")); - // show results page + // show results page } else { redirect = new ActionRedirect(mapping.findForwardConfig("showResults")); } - + redirect.addParameter(ScratchieConstants.ATTR_SESSION_MAP_ID, sessionMap.getSessionID()); return redirect; } @@ -804,7 +775,7 @@ LearningAction.service.updateEntry(entry); } sessionMap.put(ScratchieConstants.ATTR_REFLECTION_ENTRY, entryText); - + ActionRedirect redirect = new ActionRedirect(mapping.findForwardConfig("showResults")); redirect.addParameter(ScratchieConstants.ATTR_SESSION_MAP_ID, sessionMap.getSessionID()); return redirect; Index: lams_tool_scratchie/src/java/org/lamsfoundation/lams/tool/scratchie/web/action/LearningWebsocketServer.java =================================================================== diff -u --- lams_tool_scratchie/src/java/org/lamsfoundation/lams/tool/scratchie/web/action/LearningWebsocketServer.java (revision 0) +++ lams_tool_scratchie/src/java/org/lamsfoundation/lams/tool/scratchie/web/action/LearningWebsocketServer.java (revision 63ff5aaa19e3fb53810266ade4fc2e9433c09d44) @@ -0,0 +1,234 @@ +package org.lamsfoundation.lams.tool.scratchie.web.action; + +import java.io.IOException; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.TreeMap; + +import javax.websocket.CloseReason; +import javax.websocket.CloseReason.CloseCodes; +import javax.websocket.OnClose; +import javax.websocket.OnOpen; +import javax.websocket.Session; +import javax.websocket.server.ServerEndpoint; + +import org.apache.log4j.Logger; +import org.apache.tomcat.util.json.JSONException; +import org.apache.tomcat.util.json.JSONObject; +import org.lamsfoundation.lams.tool.scratchie.model.ScratchieAnswer; +import org.lamsfoundation.lams.tool.scratchie.model.ScratchieItem; +import org.lamsfoundation.lams.tool.scratchie.service.IScratchieService; +import org.lamsfoundation.lams.tool.scratchie.service.ScratchieServiceProxy; +import org.lamsfoundation.lams.util.hibernate.HibernateSessionManager; +import org.lamsfoundation.lams.web.session.SessionManager; +import org.lamsfoundation.lams.web.util.AttributeNames; + +/** + * Sends Scratchies actions to non-leaders. + * + * @author Marcin Cieslak + */ +@ServerEndpoint("/learningWebsocket") +public class LearningWebsocketServer { + + /** + * A singleton which updates Learners with reports and votes. + */ + private static class SendWorker extends Thread { + private boolean stopFlag = false; + // how ofter the thread runs + private static final long CHECK_INTERVAL = 3000; + + @Override + public void run() { + while (!stopFlag) { + try { + // synchronize websockets as a new Learner entering the activity could modify this collection + synchronized (LearningWebsocketServer.websockets) { + Iterator>> entryIterator = LearningWebsocketServer.websockets + .entrySet().iterator(); + // go through activities and update registered learners with reports and vote count + while (entryIterator.hasNext()) { + Entry> entry = entryIterator.next(); + Long toolSessionId = entry.getKey(); + try { + send(toolSessionId); + } catch (JSONException e) { + LearningWebsocketServer.log.error("Error while building Scratchie answer JSON", e); + } + // if all learners left the activity, remove the obsolete mapping + Set sessionWebsockets = entry.getValue(); + if (sessionWebsockets.isEmpty()) { + entryIterator.remove(); + LearningWebsocketServer.cache.remove(toolSessionId); + } + } + } + + Thread.sleep(SendWorker.CHECK_INTERVAL); + } catch (InterruptedException e) { + LearningWebsocketServer.log.warn("Stopping Scratchie worker thread"); + stopFlag = true; + } catch (Exception e) { + // error caught, but carry on + LearningWebsocketServer.log.error("Error in Scratchie worker thread", e); + } + } + } + + /** + * Feeds websockets with scratched answers. + */ + @SuppressWarnings("unchecked") + private void send(Long toolSessionId) throws JSONException, IOException { + JSONObject responseJSON = new JSONObject(); + // websocket communication bypasses standard HTTP filters, so Hibernate session needs to be initialised manually + // A new session needs to be created on each thread run as the session keeps stale Hibernate data (single transaction). + HibernateSessionManager.bindHibernateSessionToCurrentThread(true); + + Collection items = LearningWebsocketServer.getScratchieService() + .getItemsWithIndicatedScratches(toolSessionId); + Map> sessionCache = null; + for (ScratchieItem item : items) { + Long itemUid = item.getUid(); + // do not init variables below until it's really needed + Map itemCache = null; + JSONObject itemJSON = null; + for (ScratchieAnswer answer : (Set) item.getAnswers()) { + if (answer.isScratched()) { + // answer is scratched, check if it is present in cache + if (itemCache == null) { + // init required cache variables + if (sessionCache == null) { + sessionCache = LearningWebsocketServer.cache.get(toolSessionId); + if (sessionCache == null) { + sessionCache = new TreeMap>(); + LearningWebsocketServer.cache.put(toolSessionId, sessionCache); + } + } + itemCache = sessionCache.get(itemUid); + if (itemCache == null) { + itemCache = new TreeMap(); + sessionCache.put(itemUid, itemCache); + } + } + + Long answerUid = answer.getUid(); + Boolean answerStoredIsCorrect = answer.isCorrect(); + Boolean answerCache = itemCache.get(answerUid); + // check if the correct answer is stored in cache + if ((answerCache == null) || !answerCache.equals(answerStoredIsCorrect)) { + // send only updates, nothing Learners are already aware of + itemCache.put(answerUid, answerStoredIsCorrect); + if (itemJSON == null) { + itemJSON = new JSONObject(); + } + itemJSON.put(answerUid.toString(), answerStoredIsCorrect); + } + } + } + if (itemJSON != null) { + responseJSON.put(itemUid.toString(), itemJSON); + } + } + + // are there any updates to send? + if (responseJSON.length() == 0) { + return; + } + + String response = responseJSON.toString(); + // make a copy of the websocket collection so it does not get blocked while sending messages + Set sessionWebsockets = new HashSet( + LearningWebsocketServer.websockets.get(toolSessionId)); + for (Session websocket : sessionWebsockets) { + websocket.getBasicRemote().sendText(response); + } + } + } + + private static Logger log = Logger.getLogger(LearningWebsocketServer.class); + + private static IScratchieService scratchieService; + + private static final SendWorker sendWorker = new SendWorker(); + // maps toolSessionId -> itemUid -> answerUid -> isCorrect + private static final Map>> cache = Collections + .synchronizedMap(new TreeMap>>()); + private static final Map> websockets = Collections + .synchronizedMap(new TreeMap>()); + + static { + // run the singleton thread + LearningWebsocketServer.sendWorker.start(); + } + + /** + * Registeres the Learner for processing by SendWorker. + */ + @OnOpen + public void registerUser(Session websocket) throws JSONException, IOException { + Long toolSessionId = Long + .valueOf(websocket.getRequestParameterMap().get(AttributeNames.PARAM_TOOL_SESSION_ID).get(0)); + Set sessionWebsockets = LearningWebsocketServer.websockets.get(toolSessionId); + if (sessionWebsockets == null) { + sessionWebsockets = Collections.synchronizedSet(new HashSet()); + LearningWebsocketServer.websockets.put(toolSessionId, sessionWebsockets); + } + sessionWebsockets.add(websocket); + + if (LearningWebsocketServer.log.isDebugEnabled()) { + LearningWebsocketServer.log.debug("User " + websocket.getUserPrincipal().getName() + + " entered Scratchie with toolSessionId: " + toolSessionId); + } + } + + /** + * When user leaves the activity. + */ + @OnClose + public void unregisterUser(Session websocket, CloseReason reason) { + Long toolSessionId = Long + .valueOf(websocket.getRequestParameterMap().get(AttributeNames.PARAM_TOOL_SESSION_ID).get(0)); + LearningWebsocketServer.websockets.get(toolSessionId).remove(websocket); + + if (LearningWebsocketServer.log.isDebugEnabled()) { + // If there was something wrong with the connection, put it into logs. + LearningWebsocketServer.log.debug("User " + websocket.getUserPrincipal().getName() + + " left Scratchie with Tool Session ID: " + toolSessionId + + (!(reason.getCloseCode().equals(CloseCodes.GOING_AWAY) + || reason.getCloseCode().equals(CloseCodes.NORMAL_CLOSURE)) + ? ". Abnormal close. Code: " + reason.getCloseCode() + ". Reason: " + + reason.getReasonPhrase() + : "")); + } + } + + /** + * The leader finished the activity. Non-leaders will have Finish button displayed. + */ + static void sendCloseRequest(Long toolSessionId) throws JSONException, IOException { + JSONObject responseJSON = new JSONObject(); + responseJSON.put("close", true); + String response = responseJSON.toString(); + + // make a copy of the websocket collection so it does not get blocked while sending messages + Set sessionWebsockets = new HashSet(LearningWebsocketServer.websockets.get(toolSessionId)); + for (Session websocket : sessionWebsockets) { + websocket.getBasicRemote().sendText(response); + } + } + + private static IScratchieService getScratchieService() { + if (LearningWebsocketServer.scratchieService == null) { + LearningWebsocketServer.scratchieService = ScratchieServiceProxy + .getScratchieService(SessionManager.getServletContext()); + } + return LearningWebsocketServer.scratchieService; + } +} \ No newline at end of file Index: lams_tool_scratchie/web/pages/learning/learning.jsp =================================================================== diff -u -rc9759e81d08842b6202f62bc08a78cc13ca5bcf8 -r63ff5aaa19e3fb53810266ade4fc2e9433c09d44 --- lams_tool_scratchie/web/pages/learning/learning.jsp (.../learning.jsp) (revision c9759e81d08842b6202f62bc08a78cc13ca5bcf8) +++ lams_tool_scratchie/web/pages/learning/learning.jsp (.../learning.jsp) (revision 63ff5aaa19e3fb53810266ade4fc2e9433c09d44) @@ -35,12 +35,21 @@ } - + Index: lams_tool_scratchie/web/pages/learning/questionlist.jsp =================================================================== diff -u -rc9759e81d08842b6202f62bc08a78cc13ca5bcf8 -r63ff5aaa19e3fb53810266ade4fc2e9433c09d44 --- lams_tool_scratchie/web/pages/learning/questionlist.jsp (.../questionlist.jsp) (revision c9759e81d08842b6202f62bc08a78cc13ca5bcf8) +++ lams_tool_scratchie/web/pages/learning/questionlist.jsp (.../questionlist.jsp) (revision 63ff5aaa19e3fb53810266ade4fc2e9433c09d44) @@ -15,45 +15,40 @@ - + //init the connection with server using server URL but with different protocol + var websocket = new WebSocket(''.replace('http', 'ws') + + 'learningWebsocket?toolSessionID=' + ${toolSessionID}); + + // run when the server pushes new reports and vote statistics + websocket.onmessage = function(e) { + // create JSON object + var input = JSON.parse(e.data); + + if (input.close) { + // leader finished the activity + $('#finishButton').show(); + return; + } + + $.each(input, function(itemUid, answers) { + $.each(answers, function(answerUid, isCorrect){ + // only updates come via websockets + scratchImage(itemUid, answerUid, isCorrect); + }); + }); + }; + +
@@ -68,14 +63,16 @@ - + - + - + - + - +
\ No newline at end of file