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