Index: lams_common/.classpath
===================================================================
diff -u -ra5b247dd91cb3ffabf9de46cba029e5537fad087 -r7b189d6353baf83e0218dd064ef543d53b555a99
--- lams_common/.classpath (.../.classpath) (revision a5b247dd91cb3ffabf9de46cba029e5537fad087)
+++ lams_common/.classpath (.../.classpath) (revision 7b189d6353baf83e0218dd064ef543d53b555a99)
@@ -47,6 +47,7 @@
+
Index: lams_common/src/java/org/lamsfoundation/lams/web/controller/AbstractTimeLimitWebsocketServer.java
===================================================================
diff -u
--- lams_common/src/java/org/lamsfoundation/lams/web/controller/AbstractTimeLimitWebsocketServer.java (revision 0)
+++ lams_common/src/java/org/lamsfoundation/lams/web/controller/AbstractTimeLimitWebsocketServer.java (revision 7b189d6353baf83e0218dd064ef543d53b555a99)
@@ -0,0 +1,350 @@
+package org.lamsfoundation.lams.web.controller;
+
+import java.io.IOException;
+import java.lang.reflect.Method;
+import java.time.Duration;
+import java.time.LocalDateTime;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+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.ServerEndpointConfig;
+
+import org.apache.log4j.Logger;
+import org.lamsfoundation.lams.usermanagement.User;
+import org.lamsfoundation.lams.usermanagement.service.IUserManagementService;
+import org.lamsfoundation.lams.util.hibernate.HibernateSessionManager;
+import org.lamsfoundation.lams.web.session.SessionManager;
+import org.lamsfoundation.lams.web.util.AttributeNames;
+import org.springframework.web.context.WebApplicationContext;
+import org.springframework.web.context.support.WebApplicationContextUtils;
+
+import com.fasterxml.jackson.databind.node.JsonNodeFactory;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+
+/**
+ * Controls activity time limits.
+ * Can be used in tools to set relative and absolute time limits for learners.
+ *
+ * @author Marcin Cieslak
+ */
+public abstract class AbstractTimeLimitWebsocketServer extends ServerEndpointConfig.Configurator {
+
+ /**
+ * By default Endpoint instances are created for each request.
+ * We need persistent data to cache time settings.
+ * We can not use static fields as each subclass needs to have own set of fields.
+ * This class registers and retrieves Endpoints as singletons.
+ */
+ public static class EndpointConfigurator extends ServerEndpointConfig.Configurator {
+ private static final Map TIME_LIMIT_ENDPOINT_INSTANCES = new ConcurrentHashMap<>();
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public T getEndpointInstance(Class endpointClass) throws InstantiationException {
+
+ // is there an instance already
+ AbstractTimeLimitWebsocketServer instance = AbstractTimeLimitWebsocketServer
+ .getInstance(endpointClass.getName());
+ if (instance == null) {
+ // every subclass must have a static getInstance() method
+ try {
+ Method getInstanceMethod = endpointClass.getMethod("getInstance");
+ instance = (AbstractTimeLimitWebsocketServer) getInstanceMethod.invoke(endpointClass);
+ } catch (Exception e) {
+ throw new InstantiationException(
+ "Error while instantinating time limit websocket server " + endpointClass.getName());
+ }
+ }
+
+ return (T) instance;
+ }
+ }
+
+ protected static class TimeCache {
+ public int relativeTimeLimit;
+ public LocalDateTime absoluteTimeLimit;
+ // mapping of user ID (not UID) and when the learner entered the activity
+ public final Map timeLimitLaunchedDate = new ConcurrentHashMap<>();
+ public Map timeLimitAdjustment = new HashMap<>();
+
+ public TimeCache() {
+ }
+ }
+
+ /**
+ * A singleton which updates Learners with their time limit
+ */
+ protected Runnable sendWorker = new Runnable() {
+ @Override
+ public void run() {
+ try {
+ // websocket communication bypasses standard HTTP filters, so Hibernate session needs to be initialised manually
+ HibernateSessionManager.openSession();
+
+ Iterator>> entryIterator = websockets.entrySet().iterator();
+ // go through activities and update registered learners with time if needed
+ while (entryIterator.hasNext()) {
+ Entry> entry = entryIterator.next();
+ // processActivity method can be overwritten if needed
+ boolean processed = processActivity(entry.getKey(), entry.getValue());
+ if (!processed) {
+ entryIterator.remove();
+ }
+ }
+ } catch (IllegalStateException e) {
+ // do nothing as server is probably shutting down and we could not obtain Hibernate session
+ } catch (Exception e) {
+ // error caught, but carry on
+ log.error("Error in Assessment worker thread", e);
+ } finally {
+ HibernateSessionManager.closeSession();
+ }
+ }
+ };
+
+ protected static IUserManagementService userManagementService;
+
+ protected Logger log = getLog();
+
+ // how ofter the thread runs in seconds
+ protected long CHECK_INTERVAL = 3;
+ protected ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
+ protected Map> websockets = new ConcurrentHashMap<>();
+ protected Map timeCaches = new ConcurrentHashMap<>();
+
+ // each concrete subclass has to implement these methods
+
+ protected abstract Logger getLog();
+
+ protected abstract TimeCache getExistingTimeSettings(long toolContentId, Collection userIds);
+
+ protected abstract LocalDateTime launchUserTimeLimit(long toolContentId, int userId);
+
+ // methods for server instantination
+
+ protected AbstractTimeLimitWebsocketServer() {
+ // run the singleton thread in given periods
+ executor.scheduleAtFixedRate(sendWorker, 0, CHECK_INTERVAL, TimeUnit.SECONDS);
+
+ if (userManagementService == null) {
+ WebApplicationContext wac = WebApplicationContextUtils
+ .getRequiredWebApplicationContext(SessionManager.getServletContext());
+ userManagementService = (IUserManagementService) wac.getBean("userManagementService");
+ }
+ }
+
+ /**
+ * Gets Endpoint singleton for concrete subclass
+ */
+ protected static AbstractTimeLimitWebsocketServer getInstance(String className) {
+ return EndpointConfigurator.TIME_LIMIT_ENDPOINT_INSTANCES.get(className);
+ }
+
+ /**
+ * Stores given Endpoint instance in singleton register
+ */
+ protected static void registerInstance(AbstractTimeLimitWebsocketServer instance) {
+ String endpointClassName = instance.getClass().getName();
+ AbstractTimeLimitWebsocketServer existingInstance = EndpointConfigurator.TIME_LIMIT_ENDPOINT_INSTANCES
+ .get(endpointClassName);
+ if (existingInstance != null) {
+ throw new IllegalStateException("Endpoint " + endpointClassName + " already existing in the pool.");
+ }
+ EndpointConfigurator.TIME_LIMIT_ENDPOINT_INSTANCES.put(endpointClassName, instance);
+ }
+
+ /**
+ * Main logic method
+ */
+ protected boolean processActivity(long toolContentId, Collection websockets) throws IOException {
+ // if all learners left the activity, remove the obsolete mapping
+ if (websockets.isEmpty()) {
+ timeCaches.remove(toolContentId);
+ return false;
+ }
+
+ TimeCache timeCache = timeCaches.get(toolContentId);
+ // first time a learner entered the activity, so there is not cache yet
+ if (timeCache == null) {
+ timeCache = new TimeCache();
+ timeCaches.put(toolContentId, timeCache);
+ }
+
+ // get only currently active users, not all activity participants
+ Collection userIds = websockets.stream().filter(w -> w.getUserProperties() != null)
+ .collect(Collectors.mapping(w -> (Integer) w.getUserProperties().get("userId"), Collectors.toSet()));
+ // get activity data from DB
+ TimeCache existingTimeSettings = getExistingTimeSettings(toolContentId, userIds);
+
+ boolean updateAllUsers = false;
+ // compare relative and absolute time limits with cache
+ // it they changed, update all learners
+ if (timeCache.relativeTimeLimit != existingTimeSettings.relativeTimeLimit) {
+ timeCache.relativeTimeLimit = existingTimeSettings.relativeTimeLimit;
+ updateAllUsers = true;
+ }
+ if (timeCache.absoluteTimeLimit == null ? existingTimeSettings.absoluteTimeLimit != null
+ : !timeCache.absoluteTimeLimit.equals(existingTimeSettings.absoluteTimeLimit)) {
+ timeCache.absoluteTimeLimit = existingTimeSettings.absoluteTimeLimit;
+ updateAllUsers = true;
+ }
+
+ if (!existingTimeSettings.timeLimitAdjustment.equals(timeCache.timeLimitAdjustment)) {
+ timeCache.timeLimitAdjustment = existingTimeSettings.timeLimitAdjustment;
+ updateAllUsers = true;
+ }
+
+ for (Session websocket : websockets) {
+ Integer userId = (Integer) websocket.getUserProperties().get("userId");
+
+ if (userId == null) {
+ // apparently onClose() for this websocket has been run and the map is not available anymore
+ continue;
+ }
+
+ boolean updateUser = updateAllUsers;
+
+ // check if there is a point in updating learner launch date
+ if (timeCache.relativeTimeLimit > 0 || timeCache.absoluteTimeLimit != null) {
+ LocalDateTime existingLaunchDate = existingTimeSettings.timeLimitLaunchedDate.get(userId);
+
+ if (existingLaunchDate == null) {
+ // learner entered the activity, so store his launch date in cache and DB
+ existingLaunchDate = launchUserTimeLimit(toolContentId, userId);
+ }
+
+ LocalDateTime launchedDate = timeCache.timeLimitLaunchedDate.get(userId);
+ // user (re)entered the activity, so update him with time limit
+ if (launchedDate == null || !launchedDate.equals(existingLaunchDate)) {
+ updateUser = true;
+ timeCache.timeLimitLaunchedDate.put(userId, existingLaunchDate);
+ }
+ }
+
+ if (updateUser) {
+ Long secondsLeft = getSecondsLeft(timeCache, userId);
+ sendUpdate(websocket, secondsLeft);
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Registers a learner for processing.
+ */
+ @OnOpen
+ public void registerUser(Session websocket) throws IOException {
+ Long toolContentId = Long
+ .valueOf(websocket.getRequestParameterMap().get(AttributeNames.PARAM_TOOL_CONTENT_ID).get(0));
+ String login = websocket.getUserPrincipal().getName();
+ Integer userId = getUserId(login);
+ if (userId == null) {
+ throw new SecurityException(
+ "User \"" + login + "\" is not a participant in activity with tool content ID " + toolContentId);
+ }
+
+ websocket.getUserProperties().put("userId", userId);
+
+ Set toolContentWebsockets = websockets.get(toolContentId);
+ if (toolContentWebsockets == null) {
+ toolContentWebsockets = ConcurrentHashMap.newKeySet();
+ websockets.put(toolContentId, toolContentWebsockets);
+ }
+ toolContentWebsockets.add(websocket);
+
+ TimeCache timeCache = timeCaches.get(toolContentId);
+ if (timeCache != null) {
+ // clear cached learner data, so he gets updated with current time limit via websocket
+ timeCache.timeLimitLaunchedDate.remove(userId);
+ }
+
+ if (log.isDebugEnabled()) {
+ log.debug("User " + login + " entered activity with tool content ID: " + toolContentId);
+ }
+ }
+
+ /**
+ * When user leaves the activity.
+ */
+ @OnClose
+ public void unregisterUser(Session websocket, CloseReason reason) {
+ Long toolContentID = Long
+ .valueOf(websocket.getRequestParameterMap().get(AttributeNames.PARAM_TOOL_CONTENT_ID).get(0));
+ websockets.get(toolContentID).remove(websocket);
+
+ if (log.isDebugEnabled()) {
+ // If there was something wrong with the connection, put it into logs.
+ log.debug("User " + websocket.getUserPrincipal().getName() + " left activity with tool content ID: "
+ + toolContentID
+ + (!(reason.getCloseCode().equals(CloseCodes.GOING_AWAY)
+ || reason.getCloseCode().equals(CloseCodes.NORMAL_CLOSURE))
+ ? ". Abnormal close. Code: " + reason.getCloseCode() + ". Reason: "
+ + reason.getReasonPhrase()
+ : ""));
+ }
+ }
+
+ protected Long getSecondsLeft(TimeCache timeCache, int userId) {
+ if (timeCache.relativeTimeLimit == 0 && timeCache.absoluteTimeLimit == null) {
+ // no time limit is set at all
+ return null;
+ }
+
+ // when user entered the activity
+ LocalDateTime launchedDate = timeCache.timeLimitLaunchedDate.get(userId);
+ // what is the time limit for him
+ LocalDateTime finish = null;
+ if (timeCache.absoluteTimeLimit != null) {
+ // the limit is same for everyone
+ finish = timeCache.absoluteTimeLimit;
+ } else {
+ // the limit is his entry plus relative time limit
+ finish = launchedDate.plusSeconds(timeCache.relativeTimeLimit);
+ }
+
+ LocalDateTime now = LocalDateTime.now();
+ long secondsLeft = Duration.between(now, finish).toSeconds();
+
+ Integer adjustment = timeCache.timeLimitAdjustment.get(userId);
+ if (adjustment != null) {
+ secondsLeft += adjustment * 60;
+ }
+
+ return Math.max(0, secondsLeft);
+ }
+
+ protected void sendUpdate(Session websocket, Long secondsLeft) throws IOException {
+ ObjectNode responseJSON = JsonNodeFactory.instance.objectNode();
+ if (secondsLeft == null) {
+ // time limit feature was disabled, so destroy counter on learner screen
+ responseJSON.put("clearTimer", true);
+ } else {
+ responseJSON.put("secondsLeft", secondsLeft);
+ }
+ String response = responseJSON.toString();
+
+ if (websocket.isOpen()) {
+ websocket.getBasicRemote().sendText(response);
+ }
+ }
+
+ protected Integer getUserId(String login) {
+ User user = userManagementService.getUserByLogin(login);
+ return user == null ? null : user.getUserId();
+ }
+}
\ No newline at end of file
Index: lams_tool_assessment/src/java/org/lamsfoundation/lams/tool/assessment/service/AssessmentServiceImpl.java
===================================================================
diff -u -rf8c00dda6ee14dc3ce8dfaa448ff283d39ca779d -r7b189d6353baf83e0218dd064ef543d53b555a99
--- lams_tool_assessment/src/java/org/lamsfoundation/lams/tool/assessment/service/AssessmentServiceImpl.java (.../AssessmentServiceImpl.java) (revision f8c00dda6ee14dc3ce8dfaa448ff283d39ca779d)
+++ lams_tool_assessment/src/java/org/lamsfoundation/lams/tool/assessment/service/AssessmentServiceImpl.java (.../AssessmentServiceImpl.java) (revision 7b189d6353baf83e0218dd064ef543d53b555a99)
@@ -316,17 +316,18 @@
}
@Override
- public LocalDateTime launchTimeLimit(Long assessmentUid, Long userId) {
- AssessmentResult lastResult = getLastAssessmentResult(assessmentUid, userId);
+ public LocalDateTime launchTimeLimit(long toolContentId, int userId) {
+ Assessment assessment = getAssessmentByContentId(toolContentId);
+ AssessmentResult lastResult = getLastAssessmentResult(assessment.getUid(), Long.valueOf(userId));
LocalDateTime launchedDate = LocalDateTime.now();
lastResult.setTimeLimitLaunchedDate(launchedDate);
assessmentResultDao.saveObject(lastResult);
return launchedDate;
}
@Override
- public boolean checkTimeLimitExceeded(long assessmentUid, long userId) {
- Long secondsLeft = LearningWebsocketServer.getSecondsLeft(assessmentUid, userId);
+ public boolean checkTimeLimitExceeded(long toolContentId, int userId) {
+ Long secondsLeft = LearningWebsocketServer.getSecondsLeft(toolContentId, userId);
return secondsLeft != null && secondsLeft.equals(0);
}
Index: lams_tool_assessment/src/java/org/lamsfoundation/lams/tool/assessment/service/IAssessmentService.java
===================================================================
diff -u -r8d440baa0d18441b56553aabcc56e1981a84b206 -r7b189d6353baf83e0218dd064ef543d53b555a99
--- lams_tool_assessment/src/java/org/lamsfoundation/lams/tool/assessment/service/IAssessmentService.java (.../IAssessmentService.java) (revision 8d440baa0d18441b56553aabcc56e1981a84b206)
+++ lams_tool_assessment/src/java/org/lamsfoundation/lams/tool/assessment/service/IAssessmentService.java (.../IAssessmentService.java) (revision 7b189d6353baf83e0218dd064ef543d53b555a99)
@@ -87,18 +87,15 @@
/**
* Stores date when user has started activity with time limit.
- *
- * @param assessmentUid
- * @param userId
*/
- LocalDateTime launchTimeLimit(Long assessmentUid, Long userId);
+ LocalDateTime launchTimeLimit(long toolContentId, int userId);
/**
* @param assessment
* @param groupLeader
* @return whether the time limit is exceeded already
*/
- boolean checkTimeLimitExceeded(long assessmentUid, long userId);
+ boolean checkTimeLimitExceeded(long assessmentUid, int userId);
/**
* Get users by given toolSessionID.
Index: lams_tool_assessment/src/java/org/lamsfoundation/lams/tool/assessment/web/controller/LearningController.java
===================================================================
diff -u -r7eab6b4371036949ed61a387bc7706473895f567 -r7b189d6353baf83e0218dd064ef543d53b555a99
--- lams_tool_assessment/src/java/org/lamsfoundation/lams/tool/assessment/web/controller/LearningController.java (.../LearningController.java) (revision 7eab6b4371036949ed61a387bc7706473895f567)
+++ lams_tool_assessment/src/java/org/lamsfoundation/lams/tool/assessment/web/controller/LearningController.java (.../LearningController.java) (revision 7b189d6353baf83e0218dd064ef543d53b555a99)
@@ -204,8 +204,8 @@
}
//if the time is up and leader hasn't submitted response - show waitForLeaderFinish page
- boolean isTimeLimitExceeded = service.checkTimeLimitExceeded(assessment.getUid(),
- groupLeader.getUserId());
+ boolean isTimeLimitExceeded = service.checkTimeLimitExceeded(assessment.getContentId(),
+ groupLeader.getUserId().intValue());
if (isTimeLimitExceeded) {
request.setAttribute(AssessmentConstants.PARAM_WAITING_MESSAGE_KEY,
"label.waiting.for.leader.finish");
@@ -446,8 +446,8 @@
AssessmentUser leader = session.getGroupLeader();
//in case of time limit - prevent user from seeing questions page longer than time limit allows
- boolean isTimeLimitExceeded = service.checkTimeLimitExceeded(session.getAssessment().getUid(),
- leader.getUserId());
+ boolean isTimeLimitExceeded = service.checkTimeLimitExceeded(session.getAssessment().getContentId(),
+ leader.getUserId().intValue());
boolean isLeaderResponseFinalized = service.isLastAttemptFinishedByUser(leader);
ObjectNode responseJSON = JsonNodeFactory.instance.objectNode();
Index: lams_tool_assessment/src/java/org/lamsfoundation/lams/tool/assessment/web/controller/LearningWebsocketServer.java
===================================================================
diff -u -r50e336123ddaab4628f5f94f795340c8e845c6df -r7b189d6353baf83e0218dd064ef543d53b555a99
--- lams_tool_assessment/src/java/org/lamsfoundation/lams/tool/assessment/web/controller/LearningWebsocketServer.java (.../LearningWebsocketServer.java) (revision 50e336123ddaab4628f5f94f795340c8e845c6df)
+++ lams_tool_assessment/src/java/org/lamsfoundation/lams/tool/assessment/web/controller/LearningWebsocketServer.java (.../LearningWebsocketServer.java) (revision 7b189d6353baf83e0218dd064ef543d53b555a99)
@@ -1,281 +1,84 @@
package org.lamsfoundation.lams.tool.assessment.web.controller;
-import java.io.IOException;
-import java.time.Duration;
import java.time.LocalDateTime;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.Set;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.Executors;
-import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.TimeUnit;
+import java.util.Collection;
-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.lamsfoundation.lams.tool.assessment.AssessmentConstants;
import org.lamsfoundation.lams.tool.assessment.model.Assessment;
import org.lamsfoundation.lams.tool.assessment.model.AssessmentResult;
-import org.lamsfoundation.lams.tool.assessment.model.AssessmentUser;
import org.lamsfoundation.lams.tool.assessment.service.IAssessmentService;
-import org.lamsfoundation.lams.util.hibernate.HibernateSessionManager;
+import org.lamsfoundation.lams.web.controller.AbstractTimeLimitWebsocketServer;
+import org.lamsfoundation.lams.web.controller.AbstractTimeLimitWebsocketServer.EndpointConfigurator;
import org.lamsfoundation.lams.web.session.SessionManager;
-import org.lamsfoundation.lams.web.util.AttributeNames;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;
-import com.fasterxml.jackson.databind.node.JsonNodeFactory;
-import com.fasterxml.jackson.databind.node.ObjectNode;
-
/**
* Controls Assessment time limits
*
* @author Marcin Cieslak
*/
-@ServerEndpoint("/learningWebsocket")
-public class LearningWebsocketServer {
+@ServerEndpoint(value = "/learningWebsocket", configurator = EndpointConfigurator.class)
+public class LearningWebsocketServer extends AbstractTimeLimitWebsocketServer {
- private static class TimeCache {
- private int relativeTimeLimit;
- private LocalDateTime absoluteTimeLimit;
- // mapping of user ID (not UID) and when the learner entered the activity
- private final Map timeLimitLaunchedDate = new ConcurrentHashMap<>();
- private Map timeLimitAdjustment = new HashMap<>();
- }
-
- /**
- * A singleton which updates Learners with their time limit
- */
- private static final Runnable sendWorker = new Runnable() {
- @Override
- public void run() {
- try {
- // websocket communication bypasses standard HTTP filters, so Hibernate session needs to be initialised manually
- HibernateSessionManager.openSession();
-
- Iterator>> entryIterator = LearningWebsocketServer.websockets.entrySet()
- .iterator();
- // go through activities and update registered learners with time if needed
- while (entryIterator.hasNext()) {
- Entry> entry = entryIterator.next();
- Long toolContentId = entry.getKey();
- // if all learners left the activity, remove the obsolete mapping
- Set websockets = entry.getValue();
- if (websockets.isEmpty()) {
- entryIterator.remove();
- timeCaches.remove(toolContentId);
- continue;
- }
-
- Assessment assessment = LearningWebsocketServer.getAssessmentService()
- .getAssessmentByContentId(toolContentId);
- long assessmentUid = assessment.getUid();
- TimeCache timeCache = timeCaches.get(toolContentId);
- // first time a learner entered the activity, so there is not cache yet
- if (timeCache == null) {
- timeCache = new TimeCache();
- timeCaches.put(toolContentId, timeCache);
- }
-
- boolean updateAllUsers = false;
- // compare relative and absolute time limits with cache
- // it they changed, update all learners
- int existingRelativeTimeLimit = assessment.getRelativeTimeLimit() * 60;
- if (timeCache.relativeTimeLimit != existingRelativeTimeLimit) {
- timeCache.relativeTimeLimit = existingRelativeTimeLimit;
- updateAllUsers = true;
- }
- LocalDateTime existingAbsoluteTimeLimit = assessment.getAbsoluteTimeLimit();
- if (timeCache.absoluteTimeLimit == null ? existingAbsoluteTimeLimit != null
- : !timeCache.absoluteTimeLimit.equals(existingAbsoluteTimeLimit)) {
- timeCache.absoluteTimeLimit = existingAbsoluteTimeLimit;
- updateAllUsers = true;
- }
-
- Map existingTimeLimitAdjustment = assessment.getTimeLimitAdjustments();
- if (!existingTimeLimitAdjustment.equals(timeCache.timeLimitAdjustment)) {
- timeCache.timeLimitAdjustment = existingTimeLimitAdjustment;
- updateAllUsers = true;
- }
-
- for (Session websocket : entry.getValue()) {
- String login = websocket.getUserPrincipal().getName();
- AssessmentUser user = LearningWebsocketServer.getAssessmentService()
- .getUserByLoginAndContent(login, toolContentId);
- long userId = user.getUserId();
- boolean updateUser = updateAllUsers;
-
- // check if there is a point in updating learner launch date
- if (timeCache.relativeTimeLimit > 0 || timeCache.absoluteTimeLimit != null) {
- AssessmentResult result = LearningWebsocketServer.getAssessmentService()
- .getLastAssessmentResult(assessmentUid, userId);
- if (result == null) {
- continue;
- }
- LocalDateTime existingLaunchDate = result.getTimeLimitLaunchedDate();
- if (existingLaunchDate == null) {
- // learner entered the activity, so store his launch date in cache and DB
- existingLaunchDate = assessmentService.launchTimeLimit(assessmentUid, userId);
- }
-
- LocalDateTime launchedDate = timeCache.timeLimitLaunchedDate.get(userId);
- // user (re)entered the activity, so update him with time limit
- if (launchedDate == null || !launchedDate.equals(existingLaunchDate)) {
- updateUser = true;
- timeCache.timeLimitLaunchedDate.put(userId, existingLaunchDate);
- }
- }
-
- if (updateUser) {
- Long secondsLeft = LearningWebsocketServer.getSecondsLeft(timeCache, userId);
- LearningWebsocketServer.sendUpdate(websocket, secondsLeft);
- }
- }
- }
- } catch (IllegalStateException e) {
- // do nothing as server is probably shutting down and we could not obtain Hibernate session
- } catch (Exception e) {
- // error caught, but carry on
- LearningWebsocketServer.log.error("Error in Assessment worker thread", e);
- } finally {
- HibernateSessionManager.closeSession();
- }
- }
- };
-
- // how ofter the thread runs in seconds
- private static final long CHECK_INTERVAL = 3;
-
private static final Logger log = Logger.getLogger(LearningWebsocketServer.class);
- private static final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
- private static final Map> websockets = new ConcurrentHashMap<>();
- private static final Map timeCaches = new ConcurrentHashMap<>();
-
private static IAssessmentService assessmentService;
- static {
- // run the singleton thread in given periods
- executor.scheduleAtFixedRate(sendWorker, 0, CHECK_INTERVAL, TimeUnit.SECONDS);
- }
-
- /**
- * Registers the Learner for processing.
- */
- @OnOpen
- public void registerUser(Session websocket) throws IOException {
- Long toolContentID = Long
- .valueOf(websocket.getRequestParameterMap().get(AttributeNames.PARAM_TOOL_CONTENT_ID).get(0));
- String login = websocket.getUserPrincipal().getName();
- AssessmentUser user = LearningWebsocketServer.getAssessmentService().getUserByLoginAndContent(login,
- toolContentID);
- if (user == null) {
- throw new SecurityException("User \"" + login
- + "\" is not a participant in Assessment activity with tool content ID " + toolContentID);
+ public LearningWebsocketServer() {
+ if (assessmentService == null) {
+ WebApplicationContext wac = WebApplicationContextUtils
+ .getRequiredWebApplicationContext(SessionManager.getServletContext());
+ assessmentService = (IAssessmentService) wac.getBean(AssessmentConstants.ASSESSMENT_SERVICE);
}
-
- Set toolContentWebsockets = websockets.get(toolContentID);
- if (toolContentWebsockets == null) {
- toolContentWebsockets = ConcurrentHashMap.newKeySet();
- websockets.put(toolContentID, toolContentWebsockets);
- }
- toolContentWebsockets.add(websocket);
-
- TimeCache timeCache = timeCaches.get(toolContentID);
- if (timeCache != null) {
- // clear cached learner data, so he gets updated with current time limit via websocket
- timeCache.timeLimitLaunchedDate.remove(user.getUserId());
- }
-
- if (log.isDebugEnabled()) {
- log.debug("User " + login + " entered Assessment with toolContentId: " + toolContentID);
- }
}
- /**
- * When user leaves the activity.
- */
- @OnClose
- public void unregisterUser(Session websocket, CloseReason reason) {
- Long toolContentID = Long
- .valueOf(websocket.getRequestParameterMap().get(AttributeNames.PARAM_TOOL_CONTENT_ID).get(0));
- websockets.get(toolContentID).remove(websocket);
-
- if (log.isDebugEnabled()) {
- // If there was something wrong with the connection, put it into logs.
- log.debug("User " + websocket.getUserPrincipal().getName() + " left Assessment with Tool Content ID: "
- + toolContentID
- + (!(reason.getCloseCode().equals(CloseCodes.GOING_AWAY)
- || reason.getCloseCode().equals(CloseCodes.NORMAL_CLOSURE))
- ? ". Abnormal close. Code: " + reason.getCloseCode() + ". Reason: "
- + reason.getReasonPhrase()
- : ""));
- }
+ @Override
+ protected Logger getLog() {
+ return log;
}
- public static Long getSecondsLeft(long assessmentUid, long userId) {
- TimeCache timeCache = timeCaches.get(assessmentUid);
- return timeCache == null ? null : LearningWebsocketServer.getSecondsLeft(timeCache, userId);
- }
+ @Override
+ protected TimeCache getExistingTimeSettings(long toolContentId, Collection userIds) {
+ Assessment assessment = assessmentService.getAssessmentByContentId(toolContentId);
+ TimeCache existingTimeSettings = new TimeCache();
- private static Long getSecondsLeft(TimeCache timeCache, Long userId) {
- if (timeCache.relativeTimeLimit == 0 && timeCache.absoluteTimeLimit == null) {
- // no time limit is set at all
- return null;
- }
+ existingTimeSettings.absoluteTimeLimit = assessment.getAbsoluteTimeLimit();
+ existingTimeSettings.relativeTimeLimit = assessment.getRelativeTimeLimit() * 60;
+ existingTimeSettings.timeLimitAdjustment = assessment.getTimeLimitAdjustments();
- // when user entered the activity
- LocalDateTime launchedDate = timeCache.timeLimitLaunchedDate.get(userId);
- // what is the time limit for him
- LocalDateTime finish = null;
- if (timeCache.absoluteTimeLimit != null) {
- // the limit is same for everyone
- finish = timeCache.absoluteTimeLimit;
- } else {
- // the limit is his entry plus relative time limit
- finish = launchedDate.plusSeconds(timeCache.relativeTimeLimit);
+ for (Integer userId : userIds) {
+ AssessmentResult result = assessmentService.getLastAssessmentResult(assessment.getUid(),
+ userId.longValue());
+ if (result != null && result.getTimeLimitLaunchedDate() != null) {
+ existingTimeSettings.timeLimitLaunchedDate.put(userId, result.getTimeLimitLaunchedDate());
+ }
}
- LocalDateTime now = LocalDateTime.now();
- long secondsLeft = Duration.between(now, finish).toSeconds();
+ return existingTimeSettings;
+ }
- Integer adjustment = timeCache.timeLimitAdjustment.get(userId.intValue());
- if (adjustment != null) {
- secondsLeft += adjustment * 60;
- }
-
- return Math.max(0, secondsLeft);
+ @Override
+ protected LocalDateTime launchUserTimeLimit(long toolContentId, int userId) {
+ return assessmentService.launchTimeLimit(toolContentId, userId);
}
- private static void sendUpdate(Session websocket, Long secondsLeft) throws IOException {
- ObjectNode responseJSON = JsonNodeFactory.instance.objectNode();
- if (secondsLeft == null) {
- // time limit feature was disabled, so destroy counter on learner screen
- responseJSON.put("clearTimer", true);
- } else {
- responseJSON.put("secondsLeft", secondsLeft);
+ public static LearningWebsocketServer getInstance() {
+ LearningWebsocketServer result = (LearningWebsocketServer) AbstractTimeLimitWebsocketServer
+ .getInstance(LearningWebsocketServer.class.getName());
+ if (result == null) {
+ result = new LearningWebsocketServer();
+ AbstractTimeLimitWebsocketServer.registerInstance(result);
}
- String response = responseJSON.toString();
-
- if (websocket.isOpen()) {
- websocket.getBasicRemote().sendText(response);
- }
+ return result;
}
- private static IAssessmentService getAssessmentService() {
- if (assessmentService == null) {
- WebApplicationContext wac = WebApplicationContextUtils
- .getRequiredWebApplicationContext(SessionManager.getServletContext());
- assessmentService = (IAssessmentService) wac.getBean(AssessmentConstants.ASSESSMENT_SERVICE);
- }
- return assessmentService;
+ public static Long getSecondsLeft(long toolContentId, int userId) {
+ LearningWebsocketServer instance = LearningWebsocketServer.getInstance();
+ TimeCache timeCache = instance.timeCaches.get(toolContentId);
+ return timeCache == null ? null : instance.getSecondsLeft(timeCache, userId);
}
}
\ No newline at end of file