Index: lams_common/src/java/org/lamsfoundation/lams/learning/service/ILearnerService.java =================================================================== diff -u -r471b903caa3365758fbdec0a22440b1b0b3f2947 -r673e64304c12d78aa1b4ba819a39ae14f394ca42 --- lams_common/src/java/org/lamsfoundation/lams/learning/service/ILearnerService.java (.../ILearnerService.java) (revision 471b903caa3365758fbdec0a22440b1b0b3f2947) +++ lams_common/src/java/org/lamsfoundation/lams/learning/service/ILearnerService.java (.../ILearnerService.java) (revision 673e64304c12d78aa1b4ba819a39ae14f394ca42) @@ -36,12 +36,11 @@ import org.lamsfoundation.lams.lesson.LearnerProgress; import org.lamsfoundation.lams.lesson.Lesson; import org.lamsfoundation.lams.lesson.dto.LessonDTO; -import org.lamsfoundation.lams.tool.ToolOutput; import org.lamsfoundation.lams.tool.exception.LamsToolServiceException; import org.lamsfoundation.lams.usermanagement.User; /** - * All Learner service methods that are available within the core. + * All Learner service methods that are available within the core. */ public interface ILearnerService { @@ -73,7 +72,7 @@ * @throws LamsToolServiceException */ void createToolSessionsIfNecessary(Activity activity, LearnerProgress learnerProgress); - + /** * Marks an tool session as complete and calculates the next activity against the learning design. This method is * for tools to redirect the client on complete. @@ -167,12 +166,14 @@ * will return null. */ Lesson getLessonByActivity(Activity activity); - + boolean isKumaliveDisabledForOrganisation(Integer organisationId); ActivityPositionDTO getActivityPositionByToolSessionId(Long toolSessionId); void createCommandForLearner(Long lessonId, String userName, String jsonCommand); void createCommandForLearners(Long toolContentId, Collection userIds, String jsonCommand); -} + + boolean triggerCommandCheckAndSend(); +} \ No newline at end of file Index: lams_learning/src/java/org/lamsfoundation/lams/learning/command/CommandWebsocketServer.java =================================================================== diff -u -rea80430beb4497f12c92db2580341f21750a5a43 -r673e64304c12d78aa1b4ba819a39ae14f394ca42 --- lams_learning/src/java/org/lamsfoundation/lams/learning/command/CommandWebsocketServer.java (.../CommandWebsocketServer.java) (revision ea80430beb4497f12c92db2580341f21750a5a43) +++ lams_learning/src/java/org/lamsfoundation/lams/learning/command/CommandWebsocketServer.java (.../CommandWebsocketServer.java) (revision 673e64304c12d78aa1b4ba819a39ae14f394ca42) @@ -39,7 +39,7 @@ */ private static class SendWorker extends Thread { private boolean stopFlag = false; - // how ofter the thread runs + // how often the thread runs private static final long CHECK_INTERVAL = 5000; // mapping lessonId -> timestamp when the check was last performed, so the thread does not run too often private final Map lastSendTimes = new TreeMap<>(); @@ -50,31 +50,7 @@ try { // websocket communication bypasses standard HTTP filters, so Hibernate session needs to be initialised manually HibernateSessionManager.openSession(); - - Iterator>> entryIterator = CommandWebsocketServer.websockets - .entrySet().iterator(); - - Entry> entry = null; - // go through lessons and update registered learners with messages - do { - entry = entryIterator.hasNext() ? entryIterator.next() : null; - if (entry != null) { - Long lessonId = entry.getKey(); - Long lastSendTime = lastSendTimes.get(lessonId); - if ((lastSendTime == null) - || ((System.currentTimeMillis() - lastSendTime) >= SendWorker.CHECK_INTERVAL)) { - send(lessonId); - } - - // if all learners left the lesson, remove the obsolete mapping - Map lessonWebsockets = entry.getValue(); - if (lessonWebsockets.isEmpty()) { - entryIterator.remove(); - lastSendTimes.remove(lessonId); - } - } - } while (entry != null); - + checkAndSend(); Thread.sleep(SendWorker.CHECK_INTERVAL); } catch (IllegalStateException e) { // do nothing as server is probably shutting down and we could not obtain Hibernate session @@ -93,10 +69,41 @@ } } + public boolean checkAndSend() throws IOException { + boolean sentAnything = false; + + Iterator>> entryIterator = CommandWebsocketServer.websockets.entrySet() + .iterator(); + + Entry> entry = null; + // go through lessons and update registered learners with messages + do { + entry = entryIterator.hasNext() ? entryIterator.next() : null; + if (entry != null) { + Long lessonId = entry.getKey(); + Long lastSendTime = lastSendTimes.get(lessonId); + if ((lastSendTime == null) + || ((System.currentTimeMillis() - lastSendTime) >= SendWorker.CHECK_INTERVAL)) { + sentAnything |= send(lessonId); + } + + // if all learners left the lesson, remove the obsolete mapping + Map lessonWebsockets = entry.getValue(); + if (lessonWebsockets.isEmpty()) { + entryIterator.remove(); + lastSendTimes.remove(lessonId); + } + } + } while (entry != null); + + return sentAnything; + } + /** * Feeds opened websockets with commands. */ - private void send(Long lessonId) throws IOException { + private boolean send(Long lessonId) throws IOException { + boolean sentAnything = false; Long lastSendTime = lastSendTimes.get(lessonId); if (lastSendTime == null) { lastSendTime = System.currentTimeMillis() - CHECK_INTERVAL; @@ -110,8 +117,10 @@ Session websocket = lessonWebsockets.get(command.getUserName()); if (websocket != null && websocket.isOpen()) { websocket.getBasicRemote().sendText(command.getCommandText()); + sentAnything = true; } } + return sentAnything; } } @@ -160,6 +169,24 @@ lessonWebsockets.remove(login); } + /** + * Manually trigger sending all waiting commands. + * This methods has to be run in a transactional environment, + * for example is a service method! + * Otherwise manually open and close Hibernate session, + * same as in {@link SendWorker#run()} + */ + public static boolean triggerCheckAndSend() { + try { + return CommandWebsocketServer.sendWorker.checkAndSend(); + } catch (Exception e) { + CommandWebsocketServer.log.error("Error in Command Websocket Server", e); + } + // it is safer to assume that something was sent, + // so the calling method can act accordingly + return true; + } + private static ILearnerFullService getLearnerService() { if (learnerService == null) { WebApplicationContext ctx = WebApplicationContextUtils Index: lams_learning/src/java/org/lamsfoundation/lams/learning/service/LearnerService.java =================================================================== diff -u -rf4e0bfe05f14ecc966f26ecb6eed46c9a8c59e1e -r673e64304c12d78aa1b4ba819a39ae14f394ca42 --- lams_learning/src/java/org/lamsfoundation/lams/learning/service/LearnerService.java (.../LearnerService.java) (revision f4e0bfe05f14ecc966f26ecb6eed46c9a8c59e1e) +++ lams_learning/src/java/org/lamsfoundation/lams/learning/service/LearnerService.java (.../LearnerService.java) (revision 673e64304c12d78aa1b4ba819a39ae14f394ca42) @@ -38,6 +38,7 @@ import org.apache.log4j.Logger; import org.lamsfoundation.lams.gradebook.service.IGradebookService; +import org.lamsfoundation.lams.learning.command.CommandWebsocketServer; import org.lamsfoundation.lams.learning.command.dao.ICommandDAO; import org.lamsfoundation.lams.learning.command.model.Command; import org.lamsfoundation.lams.learning.kumalive.model.Kumalive; @@ -116,7 +117,7 @@ private ActivityMapping activityMapping; private IUserManagementService userManagementService; private ILessonService lessonService; - private static HashMap syncMap = new HashMap(); + private static HashMap syncMap = new HashMap<>(); private IGradebookService gradebookService; private ILogEventService logEventService; private IKumaliveService kumaliveService; @@ -767,7 +768,7 @@ private boolean forceGrouping(Lesson lesson, Grouping grouping, Group group, User learner) { boolean groupingDone = false; if (lesson.isPreviewLesson()) { - ArrayList learnerList = new ArrayList(); + ArrayList learnerList = new ArrayList<>(); learnerList.add(learner); if (group != null) { if (group.getGroupId() != null) { @@ -862,7 +863,7 @@ @Override public Set getGroupsForGate(GateActivity gate) { Lesson lesson = getLessonByActivity(gate); - Set result = new HashSet(); + Set result = new HashSet<>(); Activity branchActivity = gate.getParentBranch(); while ((branchActivity != null) && !(branchActivity.getParentActivity().isChosenBranchingActivity() @@ -914,7 +915,7 @@ * @return the lesson dto array. */ private LessonDTO[] getLessonDataFor(List lessons) { - List lessonDTOList = new ArrayList(); + List lessonDTOList = new ArrayList<>(); for (Iterator i = lessons.iterator(); i.hasNext();) { Lesson currentLesson = (Lesson) i.next(); lessonDTOList.add(new LessonDTO(currentLesson)); @@ -974,7 +975,7 @@ if (toolSession != null) { // Get all the conditions for this branching activity, ordered by order id. - Map conditionsMap = new TreeMap(); + Map conditionsMap = new TreeMap<>(); Iterator branchIterator = branchingActivity.getActivities().iterator(); while (branchIterator.hasNext()) { Activity branchActivity = (Activity) branchIterator.next(); @@ -991,7 +992,7 @@ // Go through each condition until we find one that passes and that is the required branch. // Cache the tool output so that we aren't calling it over an over again. - Map toolOutputMap = new HashMap(); + Map toolOutputMap = new HashMap<>(); Iterator conditionIterator = conditionsMap.keySet().iterator(); while ((matchedBranch == null) && conditionIterator.hasNext()) { @@ -1106,7 +1107,7 @@ // Go through each condition until we find one that passes and that opens the gate. // Cache the tool output so that we aren't calling it over an over again. - Map toolOutputMap = new HashMap(); + Map toolOutputMap = new HashMap<>(); for (BranchActivityEntry entry : conditionGate.getBranchActivityEntries()) { BranchCondition condition = entry.getCondition(); String conditionName = condition.getName(); @@ -1602,6 +1603,11 @@ } @Override + public boolean triggerCommandCheckAndSend() { + return CommandWebsocketServer.triggerCheckAndSend(); + } + + @Override public IActivityDAO getActivityDAO() { return activityDAO; } Index: lams_monitoring/src/java/org/lamsfoundation/lams/monitoring/service/MonitoringService.java =================================================================== diff -u -rd471fb4d4ad60b6568b9f3cb4ec9cd82c6fbe495 -r673e64304c12d78aa1b4ba819a39ae14f394ca42 --- lams_monitoring/src/java/org/lamsfoundation/lams/monitoring/service/MonitoringService.java (.../MonitoringService.java) (revision d471fb4d4ad60b6568b9f3cb4ec9cd82c6fbe495) +++ lams_monitoring/src/java/org/lamsfoundation/lams/monitoring/service/MonitoringService.java (.../MonitoringService.java) (revision 673e64304c12d78aa1b4ba819a39ae14f394ca42) @@ -33,7 +33,6 @@ import java.util.Date; import java.util.HashSet; import java.util.Iterator; -import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Locale; @@ -114,7 +113,6 @@ import org.lamsfoundation.lams.util.FileUtil; import org.lamsfoundation.lams.util.MessageService; import org.lamsfoundation.lams.util.NumberUtil; -import org.lamsfoundation.lams.util.excel.ExcelCell; import org.lamsfoundation.lams.util.excel.ExcelRow; import org.lamsfoundation.lams.util.excel.ExcelSheet; import org.lamsfoundation.lams.web.session.SessionManager; @@ -127,6 +125,9 @@ import org.quartz.TriggerBuilder; import org.quartz.TriggerKey; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.fasterxml.jackson.databind.node.ObjectNode; + /** *

* This is the major service facade for all monitoring functionalities. It is configured as a Spring factory bean so as @@ -1306,10 +1307,34 @@ } else { securityService.isLessonMonitor(lessonId, requesterId, "force complete", true); } - Lesson lesson = lessonDAO.getLesson(new Long(lessonId)); + Lesson lesson = lessonDAO.getLesson(Long.valueOf(lessonId)); User learner = (User) baseDAO.find(User.class, learnerId); LearnerProgress learnerProgress = learnerService.getProgress(learnerId, lessonId); + + // trigger autosave on current activity so learner's progress is not lost + Activity currentActivity = learnerProgress == null ? null : learnerProgress.getCurrentActivity(); + if (!removeLearnerContent && currentActivity != null) { + ObjectNode jsonCommand = JsonNodeFactory.instance.objectNode(); + // command websocket on Page.tag understands this message + jsonCommand.put("message", "autosave"); + learnerService.createCommandForLearner(lessonId, learner.getLogin(), jsonCommand.toString()); + // manually trigger sending of messages + boolean sentAnything = learnerService.triggerCommandCheckAndSend(); + + // if the learner does not have lesson window open, probably nothing was sent + if (sentAnything) { + try { + // allow autosave to finish + // a primitive, but way more simple solution than synchronous call to websocket and processing reply + Thread.sleep(2000); + } catch (InterruptedException e) { + log.warn( + "Monitoring service thread interrupted while giving Command Websocket Server time to send autosave message"); + } + } + } + Activity stopActivity = null; if (activityId != null) { @@ -1349,7 +1374,6 @@ baseDAO.insert(learnerProgress); } - Activity currentActivity = learnerProgress.getCurrentActivity(); Activity stopPreviousActivity = null; if (stopActivity != null) { Activity firstActivity = lesson.getLearningDesign().getFirstActivity(); @@ -1926,7 +1950,7 @@ EmailNotificationArchive notification = (EmailNotificationArchive) baseDAO.find(EmailNotificationArchive.class, emailNotificationUid); - List sheets = new LinkedList(); + List sheets = new LinkedList<>(); ExcelSheet sheet = new ExcelSheet(messageService.getMessage("email.notifications.archived.export.sheet.name")); sheets.add(sheet); Index: lams_tool_assessment/web/WEB-INF/tags/Page.tag =================================================================== diff -u -r1abddf98cd1cef4c2bd78bedfa702e0908932ccd -r673e64304c12d78aa1b4ba819a39ae14f394ca42 --- lams_tool_assessment/web/WEB-INF/tags/Page.tag (.../Page.tag) (revision 1abddf98cd1cef4c2bd78bedfa702e0908932ccd) +++ lams_tool_assessment/web/WEB-INF/tags/Page.tag (.../Page.tag) (revision 673e64304c12d78aa1b4ba819a39ae14f394ca42) @@ -207,7 +207,17 @@ // read JSON object var command = JSON.parse(e.data); if (command.message) { - alert(command.message); + // some tools implement autosave feature + // if it is such a tool, trigger it + if (command.message === 'autosave') { + // the name of this function is same in all tools + if (typeof learnerAutosave == 'function') { + learnerAutosave(); + } + } else { + alert(command.message); + } + } // if learner's currently displayed page has hookTrigger same as in the JSON // then a function also defined on that page will run Index: lams_tool_assessment/web/pages/learning/learning.jsp =================================================================== diff -u -r595c3aded62df8b73335d4c71df63d4b33691793 -r673e64304c12d78aa1b4ba819a39ae14f394ca42 --- lams_tool_assessment/web/pages/learning/learning.jsp (.../learning.jsp) (revision 595c3aded62df8b73335d4c71df63d4b33691793) +++ lams_tool_assessment/web/pages/learning/learning.jsp (.../learning.jsp) (revision 673e64304c12d78aa1b4ba819a39ae14f394ca42) @@ -188,7 +188,7 @@ //autosave feature - function autosave(){ + function learnerAutosave(){ if (isWaitingForConfirmation) return; //copy value from CKEditor (only available in essay type of questions) to textarea before ajax submit @@ -211,7 +211,7 @@ } var autosaveInterval = "30000"; // 30 seconds interval - window.setInterval(autosave, autosaveInterval); + window.setInterval(learnerAutosave, autosaveInterval); //check if we came back due to failed answers' validation (missing required question's answer or min words limit not reached)