SEBSERV-73 finished SEB Exam config update for running exams

This commit is contained in:
anhefti 2019-10-30 14:14:58 +01:00
parent af6ebf7666
commit 9b9aa4625d
24 changed files with 607 additions and 206 deletions

View file

@ -17,6 +17,7 @@ import javax.validation.constraints.NotNull;
import org.apache.commons.lang3.StringUtils;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnore;
@ -47,7 +48,8 @@ public final class Exam implements GrantEntity {
null,
null,
ExamStatus.FINISHED,
Boolean.FALSE);
Boolean.FALSE,
null);
public static final String FILTER_ATTR_TYPE = "type";
public static final String FILTER_ATTR_STATUS = "status";
@ -115,6 +117,9 @@ public final class Exam implements GrantEntity {
@JsonProperty(EXAM.ATTR_ACTIVE)
public final Boolean active;
@JsonProperty(EXAM.ATTR_LASTUPDATE)
public final String lastUpdate;
@JsonCreator
public Exam(
@JsonProperty(EXAM.ATTR_ID) final Long id,
@ -131,7 +136,8 @@ public final class Exam implements GrantEntity {
@JsonProperty(EXAM.ATTR_OWNER) final String owner,
@JsonProperty(EXAM.ATTR_SUPPORTER) final Collection<String> supporter,
@JsonProperty(EXAM.ATTR_STATUS) final ExamStatus status,
@JsonProperty(EXAM.ATTR_ACTIVE) final Boolean active) {
@JsonProperty(EXAM.ATTR_ACTIVE) final Boolean active,
@JsonProperty(EXAM.ATTR_LASTUPDATE) final String lastUpdate) {
this.id = id;
this.institutionId = institutionId;
@ -145,8 +151,9 @@ public final class Exam implements GrantEntity {
this.type = type;
this.quitPassword = quitPassword;
this.owner = owner;
this.status = (status != null) ? status : ExamStatus.UP_COMING;
this.status = (status != null) ? status : getStatusFromDate(startTime, endTime);
this.active = (active != null) ? active : Boolean.FALSE;
this.lastUpdate = lastUpdate;
this.supporter = (supporter != null)
? Collections.unmodifiableCollection(supporter)
@ -167,9 +174,13 @@ public final class Exam implements GrantEntity {
this.type = mapper.getEnum(EXAM.ATTR_TYPE, ExamType.class, ExamType.UNDEFINED);
this.quitPassword = mapper.getString(EXAM.ATTR_QUIT_PASSWORD);
this.owner = mapper.getString(EXAM.ATTR_OWNER);
this.status = mapper.getEnum(EXAM.ATTR_STATUS, ExamStatus.class);
this.status = mapper.getEnum(
EXAM.ATTR_STATUS,
ExamStatus.class,
getStatusFromDate(this.startTime, this.endTime));
this.active = mapper.getBoolean(EXAM.ATTR_ACTIVE);
this.supporter = mapper.getStringSet(EXAM.ATTR_SUPPORTER);
this.lastUpdate = null;
}
public Exam(final QuizData quizzData) {
@ -189,9 +200,10 @@ public final class Exam implements GrantEntity {
this.type = null;
this.quitPassword = null;
this.owner = null;
this.status = (status != null) ? status : ExamStatus.UP_COMING;
this.status = (status != null) ? status : getStatusFromDate(this.startTime, this.endTime);
this.active = null;
this.supporter = null;
this.lastUpdate = null;
}
@Override
@ -326,4 +338,17 @@ public final class Exam implements GrantEntity {
return builder.toString();
}
public static ExamStatus getStatusFromDate(final DateTime startTime, final DateTime endTime) {
final DateTime now = DateTime.now(DateTimeZone.UTC);
if (startTime != null && now.isBefore(startTime)) {
return ExamStatus.UP_COMING;
} else if (startTime != null && now.isAfter(startTime) && (endTime == null || now.isBefore(endTime))) {
return ExamStatus.RUNNING;
} else if (endTime != null && now.isAfter(endTime)) {
return ExamStatus.FINISHED;
} else {
return ExamStatus.UP_COMING;
}
}
}

View file

@ -23,6 +23,8 @@ import org.springframework.stereotype.Component;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.api.API;
import ch.ethz.seb.sebserver.gbl.api.APIMessage;
import ch.ethz.seb.sebserver.gbl.api.APIMessageError;
import ch.ethz.seb.sebserver.gbl.api.EntityType;
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.Configuration;
@ -37,6 +39,7 @@ import ch.ethz.seb.sebserver.gui.service.examconfig.impl.AttributeMapping;
import ch.ethz.seb.sebserver.gui.service.examconfig.impl.ViewContext;
import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey;
import ch.ethz.seb.sebserver.gui.service.page.PageContext;
import ch.ethz.seb.sebserver.gui.service.page.PageMessageException;
import ch.ethz.seb.sebserver.gui.service.page.PageService;
import ch.ethz.seb.sebserver.gui.service.page.TemplateComposer;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestService;
@ -61,10 +64,12 @@ public class SebExamConfigSettingsForm implements TemplateComposer {
"sebserver.examconfig.action.saveToHistory.success";
private static final String KEY_UNDO_SUCCESS =
"sebserver.examconfig.action.undo.success";
private static final LocTextKey TITLE_TEXT_KEY =
new LocTextKey("sebserver.examconfig.props.from.title");
private static final LocTextKey MESSAGE_SAVE_INTEGRITY_VIOLATION =
new LocTextKey("sebserver.examconfig.action.saveToHistory.integrity-violation");
private final PageService pageService;
private final RestService restService;
private final CurrentUser currentUser;
@ -150,8 +155,7 @@ public class SebExamConfigSettingsForm implements TemplateComposer {
this.restService.getBuilder(SaveExamConfigHistory.class)
.withURIVariable(API.PARAM_MODEL_ID, configuration.getModelId())
.call()
.onError(pageContext::notifyError)
.getOrThrow();
.onError(t -> notifyErrorOnSave(t, pageContext));
return action;
})
.withSuccess(KEY_SAVE_TO_HISTORY_SUCCESS)
@ -164,7 +168,6 @@ public class SebExamConfigSettingsForm implements TemplateComposer {
this.restService.getBuilder(SebExamConfigUndo.class)
.withURIVariable(API.PARAM_MODEL_ID, configuration.getModelId())
.call()
.onError(pageContext::notifyError)
.getOrThrow();
return action;
})
@ -175,9 +178,7 @@ public class SebExamConfigSettingsForm implements TemplateComposer {
.newAction(ActionDefinition.SEB_EXAM_CONFIG_VIEW_PROP)
.withEntityKey(entityKey)
.ignoreMoveAwayFromEdit()
.publish()
;
.publish();
} catch (final RuntimeException e) {
log.error("Unexpected error while trying to fetch exam configuration data and create views", e);
@ -186,7 +187,24 @@ public class SebExamConfigSettingsForm implements TemplateComposer {
log.error("Unexpected error while trying to fetch exam configuration data and create views", e);
pageContext.notifyError(e);
}
}
private void notifyErrorOnSave(final Throwable error, final PageContext context) {
if (error instanceof APIMessageError) {
try {
final List<APIMessage> errorMessages = ((APIMessageError) error).getErrorMessages();
final APIMessage apiMessage = errorMessages.get(0);
if (APIMessage.ErrorMessage.INTEGRITY_VALIDATION.isOf(apiMessage)) {
throw new PageMessageException(MESSAGE_SAVE_INTEGRITY_VIOLATION);
} else {
throw error;
}
} catch (final PageMessageException e) {
throw e;
} catch (final Throwable e) {
throw new RuntimeException(error);
}
}
}
}

View file

@ -303,7 +303,6 @@ public class PageContextImpl implements PageContext {
@Override
public <T> T notifyError(final Throwable error) {
log.error("Unexpected error: ", error);
notifyError(error.getMessage(), error);
return null;
}

View file

@ -38,7 +38,7 @@ public interface ExamConfigurationMapDAO extends
* @param examId The Exam identifier
* @return ConfigurationNode identifier of the default Exam Configuration of
* the Exam with specified identifier */
Result<Long> getDefaultConfigurationForExam(Long examId);
Result<Long> getDefaultConfigurationNode(Long examId);
/** Get the ConfigurationNode identifier of the Exam Configuration of
* the Exam for a specified user identifier.
@ -47,7 +47,7 @@ public interface ExamConfigurationMapDAO extends
* @param userId the user identifier
* @return ConfigurationNode identifier of the Exam Configuration of
* the Exam for a specified user identifier */
Result<Long> getUserConfigurationIdForExam(final Long examId, final String userId);
Result<Long> getUserConfigurationNodeId(final Long examId, final String userId);
/** Get all id of Exams that has a relation to the given configuration id.
*

View file

@ -44,11 +44,13 @@ public interface ExamDAO extends ActivatableEntityDAO<Exam, Exam>, BulkActionSup
Result<Collection<Exam>> allForEndCheck();
Result<Exam> startUpdate(Long examId, String update);
Result<Exam> placeLock(Long examId, String update);
Result<Exam> endUpdate(Long examId, String update);
Result<Exam> releaseLock(Long examId, String update);
Result<Boolean> isUpdating(Long examId);
Result<Long> forceUnlock(Long examId);
Result<Boolean> isLocked(Long examId);
Result<Boolean> upToDate(Long examId, String lastUpdate);

View file

@ -21,6 +21,7 @@ import java.util.stream.Collectors;
import org.apache.commons.lang3.BooleanUtils;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import ch.ethz.seb.sebserver.gbl.api.EntityType;
@ -148,7 +149,7 @@ public class ConfigurationDAOImpl implements ConfigurationDAO {
.execute();
Collections.sort(
configs,
(c1, c2) -> c1.getVersionDate().compareTo(c2.getVersionDate()));
(c1, c2) -> c1.getVersionDate().compareTo(c2.getVersionDate()) * -1);
final ConfigurationRecord configurationRecord = configs.get(0);
return configurationRecord;
}).flatMap(ConfigurationDAOImpl::toDomainModel);
@ -177,7 +178,7 @@ public class ConfigurationDAOImpl implements ConfigurationDAO {
}
@Override
@Transactional
@Transactional(propagation = Propagation.REQUIRES_NEW)
public Result<Configuration> saveToHistory(final Long configurationNodeId) {
return this.configurationDAOBatchService
.saveToHistory(configurationNodeId)

View file

@ -173,7 +173,7 @@ public class ExamConfigurationMapDAOImpl implements ExamConfigurationMapDAO {
@Override
@Transactional(readOnly = true)
public Result<Long> getDefaultConfigurationForExam(final Long examId) {
public Result<Long> getDefaultConfigurationNode(final Long examId) {
return Result.tryCatch(() -> this.examConfigurationMapRecordMapper
.selectByExample()
.where(
@ -190,7 +190,7 @@ public class ExamConfigurationMapDAOImpl implements ExamConfigurationMapDAO {
}
@Override
public Result<Long> getUserConfigurationIdForExam(final Long examId, final String userId) {
public Result<Long> getUserConfigurationNodeId(final Long examId, final String userId) {
return Result.tryCatch(() -> this.examConfigurationMapRecordMapper
.selectByExample()
.where(

View file

@ -27,6 +27,7 @@ import org.apache.commons.lang3.StringUtils;
import org.joda.time.DateTime;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import ch.ethz.seb.sebserver.gbl.Constants;
@ -172,13 +173,14 @@ public class ExamDAOImpl implements ExamDAO {
(exam.supporter != null)
? StringUtils.join(exam.supporter, Constants.LIST_SEPARATOR_CHAR)
: null,
(exam.type != null) ? exam.type.name() : ExamType.UNDEFINED.name(),
(exam.type != null) ? exam.type.name() : null,
exam.quitPassword,
null, // browser keys
null, // status
(exam.status != null) ? exam.status.name() : null,
null, // updating
null, // lastUpdate
BooleanUtils.toIntegerObject(exam.active));
null // active
);
this.examRecordMapper.updateByPrimaryKeySelective(examRecord);
return this.examRecordMapper.selectByPrimaryKey(exam.id);
@ -230,8 +232,8 @@ public class ExamDAOImpl implements ExamDAO {
(exam.type != null) ? exam.type.name() : ExamType.UNDEFINED.name(),
null, // quitPassword
null, // browser keys
null, // status
null, // updating
(exam.status != null) ? exam.status.name() : ExamStatus.UP_COMING.name(),
BooleanUtils.toInteger(false),
null, // lastUpdate
BooleanUtils.toInteger(true));
@ -329,8 +331,8 @@ public class ExamDAOImpl implements ExamDAO {
}
@Override
@Transactional
public Result<Exam> startUpdate(final Long examId, final String update) {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public Result<Exam> placeLock(final Long examId, final String update) {
return Result.tryCatch(() -> {
final ExamRecord examRec = this.recordById(examId)
@ -357,8 +359,8 @@ public class ExamDAOImpl implements ExamDAO {
}
@Override
@Transactional
public Result<Exam> endUpdate(final Long examId, final String update) {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public Result<Exam> releaseLock(final Long examId, final String update) {
return Result.tryCatch(() -> {
final ExamRecord examRec = this.recordById(examId)
@ -386,9 +388,29 @@ public class ExamDAOImpl implements ExamDAO {
.onError(TransactionHandler::rollback);
}
@Override
@Transactional
public Result<Long> forceUnlock(final Long examId) {
log.info("forceUnlock for exam: {}", examId);
return Result.tryCatch(() -> {
final ExamRecord examRecord = new ExamRecord(
examId,
null, null, null, null, null, null, null, null, null,
BooleanUtils.toInteger(false),
null, null);
this.examRecordMapper.updateByPrimaryKeySelective(examRecord);
return examRecord.getId();
})
.onError(TransactionHandler::rollback);
}
@Override
@Transactional(readOnly = true)
public Result<Boolean> isUpdating(final Long examId) {
public Result<Boolean> isLocked(final Long examId) {
return this.recordById(examId)
.map(rec -> BooleanUtils.toBooleanObject(rec.getUpdating()));
}
@ -397,7 +419,13 @@ public class ExamDAOImpl implements ExamDAO {
@Transactional(readOnly = true)
public Result<Boolean> upToDate(final Long examId, final String lastUpdate) {
return this.recordById(examId)
.map(rec -> lastUpdate.equals(rec.getLastupdate()));
.map(rec -> {
if (lastUpdate == null) {
return rec.getLastupdate() == null;
} else {
return lastUpdate.equals(rec.getLastupdate());
}
});
}
@Override
@ -588,7 +616,8 @@ public class ExamDAOImpl implements ExamDAO {
record.getOwner(),
supporter,
status,
BooleanUtils.toBooleanObject((quizData != null) ? record.getActive() : 0));
BooleanUtils.toBooleanObject((quizData != null) ? record.getActive() : 0),
record.getLastupdate());
});
}

View file

@ -15,9 +15,12 @@ import java.util.Set;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.api.APIMessage;
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
@ -53,26 +56,35 @@ final class MockupLmsAPITemplate implements LmsAPITemplate {
final LmsType lmsType = lmsSetup.getLmsType();
this.mockups = new ArrayList<>();
this.mockups.add(new QuizData(
"quiz1", institutionId, lmsSetupId, lmsType, "Demo Quiz 1", "Demo Quit Mockup",
"quiz1", institutionId, lmsSetupId, lmsType, "Demo Quiz 1", "Demo Quiz Mockup",
"2020-01-01T09:00:00Z", "2021-01-01T09:00:00Z", "http://lms.mockup.com/api/"));
this.mockups.add(new QuizData(
"quiz2", institutionId, lmsSetupId, lmsType, "Demo Quiz 2", "Demo Quit Mockup",
"quiz2", institutionId, lmsSetupId, lmsType, "Demo Quiz 2", "Demo Quiz Mockup",
"2020-01-01T09:00:00Z", "2021-01-01T09:00:00Z", "http://lms.mockup.com/api/"));
this.mockups.add(new QuizData(
"quiz3", institutionId, lmsSetupId, lmsType, "Demo Quiz 3", "Demo Quit Mockup",
"quiz3", institutionId, lmsSetupId, lmsType, "Demo Quiz 3", "Demo Quiz Mockup",
"2018-07-30T09:00:00Z", "2018-08-01T00:00:00Z", "http://lms.mockup.com/api/"));
this.mockups.add(new QuizData(
"quiz4", institutionId, lmsSetupId, lmsType, "Demo Quiz 4", "Demo Quit Mockup",
"quiz4", institutionId, lmsSetupId, lmsType, "Demo Quiz 4", "Demo Quiz Mockup",
"2018-01-01T00:00:00Z", "2019-01-01T00:00:00Z", "http://lms.mockup.com/api/"));
this.mockups.add(new QuizData(
"quiz5", institutionId, lmsSetupId, lmsType, "Demo Quiz 5", "Demo Quit Mockup",
"quiz5", institutionId, lmsSetupId, lmsType, "Demo Quiz 5", "Demo Quiz Mockup",
"2018-01-01T09:00:00Z", "2021-01-01T09:00:00Z", "http://lms.mockup.com/api/"));
this.mockups.add(new QuizData(
"quiz6", institutionId, lmsSetupId, lmsType, "Demo Quiz 6", "Demo Quit Mockup",
"quiz6", institutionId, lmsSetupId, lmsType, "Demo Quiz 6", "Demo Quiz Mockup",
"2019-01-01T09:00:00Z", "2021-01-01T09:00:00Z", "http://lms.mockup.com/api/"));
this.mockups.add(new QuizData(
"quiz7", institutionId, lmsSetupId, lmsType, "Demo Quiz 7", "Demo Quit Mockup",
"quiz7", institutionId, lmsSetupId, lmsType, "Demo Quiz 7", "Demo Quiz Mockup",
"2018-01-01T09:00:00Z", "2021-01-01T09:00:00Z", "http://lms.mockup.com/api/"));
this.mockups.add(new QuizData(
"quiz10", institutionId, lmsSetupId, lmsType, "Demo Quiz 10",
"Starts in a minute and ends after five minutes",
DateTime.now(DateTimeZone.UTC).plus(Constants.MINUTE_IN_MILLIS)
.toString(Constants.DEFAULT_DATE_TIME_FORMAT),
DateTime.now(DateTimeZone.UTC).plus(6 * Constants.MINUTE_IN_MILLIS)
.toString(Constants.DEFAULT_DATE_TIME_FORMAT),
"http://lms.mockup.com/api/"));
}
@Override

View file

@ -1,19 +0,0 @@
/*
* Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig;
public class ConfigurationChangedEvent {
public final Long configurationId;
public ConfigurationChangedEvent(final Long configurationId) {
this.configurationId = configurationId;
}
}

View file

@ -147,11 +147,11 @@ public class SebExamConfigServiceImpl implements SebExamConfigService {
}
public Result<Long> getDefaultConfigurationIdForExam(final Long examId) {
return this.examConfigurationMapDAO.getDefaultConfigurationForExam(examId);
return this.examConfigurationMapDAO.getDefaultConfigurationNode(examId);
}
public Result<Long> getUserConfigurationIdForExam(final Long examId, final String userId) {
return this.examConfigurationMapDAO.getUserConfigurationIdForExam(examId, userId);
return this.examConfigurationMapDAO.getUserConfigurationNodeId(examId, userId);
}
@Override

View file

@ -0,0 +1,49 @@
/*
* Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package ch.ethz.seb.sebserver.webservice.servicelayer.session;
import java.util.Collection;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.transaction.annotation.Transactional;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.Configuration;
import ch.ethz.seb.sebserver.gbl.util.Result;
public interface ExamConfigUpdateService {
Logger log = LoggerFactory.getLogger(ExamConfigUpdateService.class);
Result<Collection<Long>> processSEBExamConfigurationChange(Long configurationNodeId);
@Transactional
default Result<Configuration> processSEBExamConfigurationChange(final Configuration config) {
if (config == null) {
return Result.ofError(new NullPointerException("Configuration has null reference"));
}
return Result.tryCatch(() -> {
processSEBExamConfigurationChange(config.configurationNodeId)
.map(ids -> {
log.info("Successfully updated SEB Configuration for exams: {}", ids);
return ids;
})
.getOrThrow();
return config;
});
}
void forceReleaseUpdateLocks(Long configurationId);
Collection<Result<Long>> forceReleaseUpdateLocks(Collection<Long> examIds);
Result<Collection<Long>> checkRunningExamIntegrity(final Long configurationNodeId);
}

View file

@ -12,15 +12,12 @@ import java.io.OutputStream;
import java.util.Collection;
import java.util.function.Predicate;
import org.springframework.context.event.EventListener;
import ch.ethz.seb.sebserver.gbl.api.APIMessage;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ConfigurationChangedEvent;
/** A Service to handle running exam sessions */
public interface ExamSessionService {
@ -47,6 +44,12 @@ public interface ExamSessionService {
* @return true if an Exam is currently running */
boolean isExamRunning(Long examId);
/** Indicates if the Exam with specified Id is currently locked for new SEB Client connection attempts.
*
* @param examId The Exam identifier
* @return true if the specified Exam is currently locked for new SEB Client connections. */
boolean isExamLocked(Long examId);
/** Use this to get currently running exams by exam identifier.
* This test first if the Exam with the given identifier is currently/still
* running. If true the Exam is returned within the result. Otherwise the
@ -99,7 +102,17 @@ public interface ExamSessionService {
* of a running exam */
Result<Collection<ClientConnectionData>> getConnectionData(Long examId);
@EventListener(ConfigurationChangedEvent.class)
void updateExamConfigCache(ConfigurationChangedEvent configChanged);
/** Use this to check if the current cached running exam is up to date
* and if not to flush the cache.
*
* @param examId the Exam identifier
* @return Result with updated Exam instance or refer to an error if happened */
Result<Exam> updateExamCache(Long examId);
/** Flush all the caches for an specified Exam.
*
* @param exam The Exam instance
* @return Result with reference to the given Exam or to an error if happened */
Result<Exam> flushCache(final Exam exam);
}

View file

@ -0,0 +1,302 @@
/*
* Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package ch.ethz.seb.sebserver.webservice.servicelayer.session.impl;
import java.util.Collection;
import java.util.Collections;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.api.APIMessage;
import ch.ethz.seb.sebserver.gbl.api.APIMessage.ErrorMessage;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.Configuration;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionStatus;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ConfigurationDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamConfigurationMapDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.TransactionHandler;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamConfigUpdateService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService;
@Lazy
@Service
@WebServiceProfile
public class ExamConfigUpdateServiceImpl implements ExamConfigUpdateService {
private static final Logger log = LoggerFactory.getLogger(ExamConfigUpdateServiceImpl.class);
private final ExamDAO examDAO;
private final ConfigurationDAO configurationDAO;
private final ExamConfigurationMapDAO examConfigurationMapDAO;
private final ExamSessionService examSessionService;
private final ExamSessionControlTask examSessionControlTask;
protected ExamConfigUpdateServiceImpl(
final ExamDAO examDAO,
final ConfigurationDAO configurationDAO,
final ExamConfigurationMapDAO examConfigurationMapDAO,
final ExamSessionService examSessionService,
final ExamSessionControlTask examSessionControlTask) {
this.examDAO = examDAO;
this.configurationDAO = configurationDAO;
this.examConfigurationMapDAO = examConfigurationMapDAO;
this.examSessionService = examSessionService;
this.examSessionControlTask = examSessionControlTask;
}
// processing:
// check running exam integrity (No running exam with active SEB client-connection available)
// if OK, create an update-id and for each exam, create an update-lock on DB (This also prevents new SEB client connection attempts during update)
// check running exam integrity again after lock to ensure there where no SEB Client connection attempts in the meantime
// store the new configuration values (into history) so that they take effect
// generate the new Config Key and update the Config Key within the LMSSetup API for each exam (delete old Key and add new Key)
// evict each Exam from cache and release the update-lock on DB
@Override
@Transactional
public Result<Collection<Long>> processSEBExamConfigurationChange(final Long configurationNodeId) {
final String updateId = this.examSessionControlTask.createUpdateId();
if (log.isDebugEnabled()) {
log.debug("Process SEB Exam Configuration update for: {} with update-id {}",
configurationNodeId,
updateId);
}
return Result.tryCatch(() -> {
// check running exam integrity (No running exam with active SEB client-connection available)
final Collection<Long> examIdsFirstCheck = checkRunningExamIntegrity(configurationNodeId)
.getOrThrow();
if (log.isDebugEnabled()) {
log.debug("Involved exams on fist integrity check: {}", examIdsFirstCheck);
}
// if OK, create an update-lock on DB (This also prevents new SEB client connection attempts during update)
final Collection<Exam> exams = lockForUpdate(examIdsFirstCheck, updateId)
.stream()
.map(Result::getOrThrow)
.collect(Collectors.toList());
final Collection<Long> examsIds = exams
.stream()
.map(Exam::getId)
.collect(Collectors.toList());
if (log.isDebugEnabled()) {
log.debug("Update-Lock successfully placed for all involved exams: {}", examsIds);
}
// check running exam integrity again after lock to ensure there where no SEB Client connection attempts in the meantime
final Collection<Long> examIdsSecondCheck = checkRunningExamIntegrity(configurationNodeId)
.getOrThrow();
checkIntegrityDoubleCheck(
examIdsFirstCheck,
examIdsSecondCheck);
if (log.isDebugEnabled()) {
log.debug("Involved exams on second integrity check: {}", examIdsSecondCheck);
}
// store the new configuration values (into history) so that they take effect
final Configuration configuration = this.configurationDAO
.saveToHistory(configurationNodeId)
.getOrThrow();
if (log.isDebugEnabled()) {
log.debug("Successfully save SEB Exam Configuration: {}", configuration);
}
// generate the new Config Key and update the Config Key within the LMSSetup API for each exam (delete old Key and add new Key)
final Collection<Long> updatedExams = updateConfigKey(exams)
.stream()
.map(Result::getOrThrow)
.map(Exam::getId)
.collect(Collectors.toList());
if (log.isDebugEnabled()) {
log.debug("Successfully updated ConfigKey for Exams: {}", updatedExams);
}
// evict each Exam from cache and release the update-lock on DB
final Collection<Long> evictedExams = evictFromCache(exams)
.stream()
.filter(Result::hasValue)
.map(Result::get)
.map(Exam::getId)
.collect(Collectors.toList());
if (log.isDebugEnabled()) {
log.debug("Successfully evicted Exams from cache: {}", evictedExams);
}
// release the update-locks on involved exams
final Collection<Long> releasedLocks = releaseUpdateLocks(examIdsFirstCheck, updateId)
.stream()
.map(Result::getOrThrow)
.map(Exam::getId)
.collect(Collectors.toList());
if (log.isDebugEnabled()) {
log.debug("Successfully released update-locks on Exams: {}", releasedLocks);
}
return examIdsFirstCheck;
})
.onError(TransactionHandler::rollback);
}
@Override
public void forceReleaseUpdateLocks(final Long configurationId) {
log.warn(" **** Force release of update-locks for all exams that are related to configuration: {}",
configurationId);
try {
final Configuration config = this.configurationDAO.byPK(configurationId)
.getOrThrow();
final Collection<Long> involvedExams = this.examConfigurationMapDAO
.getExamIdsForConfigNodeId(config.configurationNodeId)
.getOrThrow();
final Collection<Long> examsIds = forceReleaseUpdateLocks(involvedExams)
.stream()
.map(Result::getOrThrow)
.collect(Collectors.toList());
log.info("Successfully released update-locks for exams: {}", examsIds);
} catch (final Exception e) {
log.error("Failed to release update-locks for exam(s)", e);
}
}
@Override
public Collection<Result<Long>> forceReleaseUpdateLocks(final Collection<Long> examIds) {
return examIds.stream()
.map(this.examDAO::forceUnlock)
.collect(Collectors.toList());
}
private void checkIntegrityDoubleCheck(
final Collection<Long> examIdsFirstCheck,
final Collection<Long> examIdsSecondCheck) {
if (examIdsFirstCheck.size() != examIdsSecondCheck.size()) {
throw new IllegalStateException("Running Exam integrity check missmatch. examIdsFirstCheck: "
+ examIdsFirstCheck + " examIdsSecondCheck: " + examIdsSecondCheck);
}
}
private Collection<Result<Exam>> lockForUpdate(final Collection<Long> examIds, final String update) {
return examIds.stream()
.map(id -> this.examDAO.placeLock(id, update))
.collect(Collectors.toList());
}
private Collection<Result<Exam>> releaseUpdateLocks(final Collection<Long> examIds, final String update) {
return examIds.stream()
.map(id -> this.examDAO.releaseLock(id, update))
.collect(Collectors.toList());
}
private Collection<Result<Exam>> updateConfigKey(final Collection<Exam> exams) {
return exams
.stream()
.map(this::updateConfigKey)
.collect(Collectors.toList());
}
private Result<Exam> updateConfigKey(final Exam exam) {
return Result.tryCatch(() -> {
if (log.isDebugEnabled()) {
log.debug("update Config Key for Exam {}", exam.externalId);
}
// TODO
return exam;
});
}
private Collection<Result<Exam>> evictFromCache(final Collection<Exam> exams) {
return exams
.stream()
.map(this.examSessionService::flushCache)
.collect(Collectors.toList());
}
@Override
public Result<Collection<Long>> checkRunningExamIntegrity(final Long configurationNodeId) {
final Collection<Long> involvedExams = this.examConfigurationMapDAO
.getExamIdsForConfigNodeId(configurationNodeId)
.getOrThrow();
if (involvedExams == null || involvedExams.isEmpty()) {
return Result.of(Collections.emptyList());
}
// check if the configuration is attached to a running exams with active client connections
final long activeConnections = involvedExams
.stream()
.flatMap(examId -> {
return this.examSessionService.getConnectionData(examId)
.getOrThrow()
.stream();
})
.filter(ExamConfigUpdateServiceImpl::isActiveConnection)
.count();
// if we have active SEB client connection on any running exam that
// is involved within the specified configuration change, the change is denied
if (activeConnections > 0) {
return Result.ofError(new APIMessage.APIMessageException(
ErrorMessage.INTEGRITY_VALIDATION,
"Integrity violation: There are currently active SEB Client connection."));
} else {
// otherwise we return the involved identifiers exams to further processing
return Result.of(involvedExams);
}
}
private static boolean isActiveConnection(final ClientConnectionData connection) {
if (connection.clientConnection.status == ConnectionStatus.ESTABLISHED
|| connection.clientConnection.status == ConnectionStatus.AUTHENTICATED) {
return true;
}
if (connection.clientConnection.status == ConnectionStatus.CONNECTION_REQUESTED) {
final Long creationTime = connection.clientConnection.getCreationTime();
final long millisecondsNow = Utils.getMillisecondsNow();
if (millisecondsNow - creationTime < 30 * Constants.SECOND_IN_MILLIS) {
return true;
}
}
return false;
}
}

View file

@ -100,8 +100,7 @@ public class ExamSessionCacheService {
@CacheEvict(
cacheNames = CACHE_NAME_RUNNING_EXAM,
key = "#exam.id",
condition = "#target.isRunning(#result)")
key = "#exam.id")
public Exam evict(final Exam exam) {
if (log.isDebugEnabled()) {
@ -111,7 +110,7 @@ public class ExamSessionCacheService {
return exam;
}
boolean isRunning(final Exam exam) {
public boolean isRunning(final Exam exam) {
if (exam == null) {
return false;
}
@ -131,7 +130,6 @@ public class ExamSessionCacheService {
default: {
return false;
}
}
}
@ -166,33 +164,31 @@ public class ExamSessionCacheService {
@Cacheable(
cacheNames = CACHE_NAME_SEB_CONFIG_EXAM,
key = "#examId",
key = "#exam.id",
unless = "#result == null")
public InMemorySebConfig getDefaultSebConfigForExam(final Long examId) {
final Exam runningExam = this.getRunningExam(examId);
public InMemorySebConfig getDefaultSebConfigForExam(final Exam exam) {
try {
final ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
final Long configId = this.sebExamConfigService.exportForExam(
byteOut,
runningExam.institutionId,
examId);
exam.institutionId,
exam.id);
return new InMemorySebConfig(configId, runningExam.id, byteOut.toByteArray());
return new InMemorySebConfig(configId, exam.id, byteOut.toByteArray());
} catch (final Exception e) {
log.error("Unexpected error while getting default exam configuration for running exam; {}", runningExam, e);
log.error("Unexpected error while getting default exam configuration for running exam; {}", exam, e);
return null;
}
}
@CacheEvict(
cacheNames = CACHE_NAME_SEB_CONFIG_EXAM,
key = "#examId")
public void evictDefaultSebConfig(final Long examId) {
key = "#exam.id")
public void evictDefaultSebConfig(final Exam exam) {
if (log.isDebugEnabled()) {
log.debug("Eviction of default SEB Configuration from cache for exam: {}", examId);
log.debug("Eviction of default SEB Configuration from cache for exam: {}", exam.id);
}
}

View file

@ -29,7 +29,7 @@ import ch.ethz.seb.sebserver.webservice.WebserviceInfo;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamDAO;
@Service
public class ExamSessionControlTask {
class ExamSessionControlTask {
private static final Logger log = LoggerFactory.getLogger(ExamSessionControlTask.class);
@ -47,20 +47,19 @@ public class ExamSessionControlTask {
this.examDAO = examDAO;
this.examTimePrefix = examTimePrefix;
this.examTimeSuffix = examTimeSuffix;
this.updatePrefix = webserviceInfo.getHostAddress()
+ "_" + webserviceInfo.getServerPort() + "_";
}
@Async
@Scheduled(cron = "1 * * * * *")
@Scheduled(cron = "${sebserver.webservice.api.exam.update-interval:1 * * * * *}")
@Transactional
public void execTask() {
final String updateId = createUpdateId();
final String updateId = this.createUpdateId();
if (log.isDebugEnabled()) {
log.debug("Run ExamControlTask. Update Id: {}", updateId);
log.debug("Run exam runtime update task with Id: {}", updateId);
}
controlStart(updateId);
@ -84,7 +83,9 @@ public class ExamSessionControlTask {
.map(exam -> this.setRunning(exam, updateId))
.collect(Collectors.toMap(Exam::getId, Exam::getName));
log.info("Updated exams to running state: {}", updated);
if (!updated.isEmpty()) {
log.info("Updated exams to running state: {}", updated);
}
} catch (final Exception e) {
log.error("Unexpected error while trying to update exams: ", e);
@ -97,15 +98,15 @@ public class ExamSessionControlTask {
final DateTime now = DateTime.now(DateTimeZone.UTC);
if (exam.getStatus() == ExamStatus.UP_COMING
&& exam.endTime.plus(this.examTimeSuffix).isBefore(now)) {
return setRunning(exam, createUpdateId());
return setRunning(exam, this.createUpdateId());
} else {
return exam;
}
});
}
public Result<Exam> setRunning(final Exam exam) {
return Result.tryCatch(() -> setRunning(exam, createUpdateId()));
public String createUpdateId() {
return this.updatePrefix + Utils.getMillisecondsNow();
}
private Exam setRunning(final Exam exam, final String updateId) {
@ -114,11 +115,11 @@ public class ExamSessionControlTask {
}
return this.examDAO
.startUpdate(exam.id, updateId)
.placeLock(exam.id, updateId)
.flatMap(e -> this.examDAO.save(new Exam(
exam.id,
ExamStatus.RUNNING)))
.flatMap(e -> this.examDAO.endUpdate(e.id, updateId))
.flatMap(e -> this.examDAO.releaseLock(e.id, updateId))
.getOrThrow();
}
@ -138,7 +139,9 @@ public class ExamSessionControlTask {
.map(exam -> this.setFinished(exam, updateId))
.collect(Collectors.toMap(Exam::getId, Exam::getName));
log.info("Updated exams to finished state: {}", updated);
if (!updated.isEmpty()) {
log.info("Updated exams to finished state: {}", updated);
}
} catch (final Exception e) {
log.error("Unexpected error while trying to update exams: ", e);
@ -151,16 +154,12 @@ public class ExamSessionControlTask {
}
return this.examDAO
.startUpdate(exam.id, updateId)
.placeLock(exam.id, updateId)
.flatMap(e -> this.examDAO.save(new Exam(
exam.id,
ExamStatus.FINISHED)))
.flatMap(e -> this.examDAO.endUpdate(e.id, updateId))
.flatMap(e -> this.examDAO.releaseLock(e.id, updateId))
.getOrThrow();
}
private String createUpdateId() {
return this.updatePrefix + Utils.getMillisecondsNow();
}
}

View file

@ -17,12 +17,12 @@ import java.util.NoSuchElementException;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import org.apache.commons.lang3.BooleanUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.event.EventListener;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.stereotype.Service;
@ -39,7 +39,6 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamConfigurationMapDAO
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.IndicatorDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ConfigurationChangedEvent;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService;
@Lazy
@ -92,7 +91,7 @@ public class ExamSessionServiceImpl implements ExamSessionService {
}
// check SEB configuration
this.examConfigurationMapDAO.getDefaultConfigurationForExam(examId)
this.examConfigurationMapDAO.getDefaultConfigurationNode(examId)
.get(t -> {
result.add(ErrorMessage.EXAM_CONSISTANCY_VALIDATION_CONFIG.of(exam.getModelId()));
return null;
@ -116,6 +115,17 @@ public class ExamSessionServiceImpl implements ExamSessionService {
return !getRunningExam(examId).hasError();
}
@Override
public boolean isExamLocked(final Long examId) {
final Result<Boolean> locked = this.examDAO.isLocked(examId);
if (locked.hasError()) {
log.error("Unexpected Error while trying to verify lock for Exam: {}", examId);
}
return locked.hasError() || BooleanUtils.toBoolean(locked.get());
}
@Override
public Result<Exam> getRunningExam(final Long examId) {
if (log.isTraceEnabled()) {
@ -123,6 +133,7 @@ public class ExamSessionServiceImpl implements ExamSessionService {
}
final Exam exam = this.examSessionCacheService.getRunningExam(examId);
if (this.examSessionCacheService.isRunning(exam)) {
if (log.isTraceEnabled()) {
log.trace("Exam {} is running and cached", examId);
@ -194,8 +205,11 @@ public class ExamSessionServiceImpl implements ExamSessionService {
log.debug("Trying to get exam from InMemorySebConfig");
}
final Exam exam = this.getRunningExam(connection.examId)
.getOrThrow();
final InMemorySebConfig sebConfigForExam = this.examSessionCacheService
.getDefaultSebConfigForExam(connection.examId);
.getDefaultSebConfigForExam(exam);
if (sebConfigForExam == null) {
log.error("Failed to get and cache InMemorySebConfig for connection: {}", connection);
@ -241,23 +255,24 @@ public class ExamSessionServiceImpl implements ExamSessionService {
}
@Override
@EventListener(ConfigurationChangedEvent.class)
public void updateExamConfigCache(final ConfigurationChangedEvent configChanged) {
public Result<Exam> updateExamCache(final Long examId) {
final Exam exam = this.examSessionCacheService.getRunningExam(examId);
final Boolean isUpToDate = this.examDAO.upToDate(examId, exam.lastUpdate)
.onError(t -> log.error("Failed to verify if cached exam is up to date: {}", exam, t))
.getOr(false);
if (log.isDebugEnabled()) {
log.debug("Flush exam config cache for configuration: {}", configChanged.configurationId);
if (!BooleanUtils.toBoolean(isUpToDate)) {
return flushCache(exam);
} else {
return Result.of(exam);
}
this.examConfigurationMapDAO
.getExamIdsForConfigId(configChanged.configurationId)
.getOrElse(() -> Collections.emptyList())
.forEach(this.examSessionCacheService::evictDefaultSebConfig);
}
private void flushCache(final Exam exam) {
try {
@Override
public Result<Exam> flushCache(final Exam exam) {
return Result.tryCatch(() -> {
this.examSessionCacheService.evict(exam);
this.examSessionCacheService.evictDefaultSebConfig(exam.id);
this.examSessionCacheService.evictDefaultSebConfig(exam);
this.clientConnectionDAO
.getConnectionTokens(exam.id)
.getOrElse(() -> Collections.emptyList())
@ -267,9 +282,9 @@ public class ExamSessionServiceImpl implements ExamSessionService {
// evict also cached ping record
this.examSessionCacheService.evictPingRecord(token);
});
} catch (final Exception e) {
log.error("Unexpected error while trying to flush cache for exam: ", exam, e);
}
return exam;
});
}
}

View file

@ -18,6 +18,7 @@ import org.springframework.boot.logging.LogLevel;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam.ExamType;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionStatus;
@ -101,7 +102,9 @@ public class SebClientConnectionServiceImpl implements SebClientConnectionServic
clientAddress);
}
checkExamRunning(examId);
if (examId != null) {
checkExamIntegrity(examId);
}
// Create ClientConnection in status CONNECTION_REQUESTED for further processing
final String connectionToken = createToken();
@ -133,6 +136,20 @@ public class SebClientConnectionServiceImpl implements SebClientConnectionServic
});
}
private void checkExamIntegrity(final Long examId) {
// check Exam is running and not locked
checkExamRunning(examId);
if (this.examSessionService.isExamLocked(examId)) {
throw new APIConstraintViolationException(
"Exam is currently on update and locked for new SEB Client connections");
}
// if the cached Exam is not up to date anymore, we have to update the cache first
final Result<Exam> updateExamCache = this.examSessionService.updateExamCache(examId);
if (updateExamCache.hasError()) {
log.warn("Failed to update Exam-Cache for Exam: {}", examId);
}
}
@Override
public Result<ClientConnection> updateClientConnection(
final String connectionToken,
@ -170,6 +187,10 @@ public class SebClientConnectionServiceImpl implements SebClientConnectionServic
"ClientConnection integrity violation: client connection is not in expected state");
}
if (examId != null) {
checkExamIntegrity(examId);
}
// userSessionId integrity check
if (userSessionId != null &&
clientConnection.userSessionId != null &&
@ -294,6 +315,8 @@ public class SebClientConnectionServiceImpl implements SebClientConnectionServic
.save(establishedClientConnection)
.getOrThrow();
checkExamIntegrity(updatedClientConnection.examId);
// evict cached ClientConnection
this.examSessionCacheService.evictClientConnection(connectionToken);
// and load updated ClientConnection into cache

View file

@ -11,7 +11,6 @@ package ch.ethz.seb.sebserver.webservice.weblayer.api;
import java.util.Collection;
import org.mybatis.dynamic.sql.SqlTable;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
@ -19,27 +18,18 @@ import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.api.API;
import ch.ethz.seb.sebserver.gbl.api.API.BulkActionType;
import ch.ethz.seb.sebserver.gbl.api.APIMessage;
import ch.ethz.seb.sebserver.gbl.api.APIMessage.ErrorMessage;
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.Configuration;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionStatus;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ConfigurationRecordDynamicSqlSupport;
import ch.ethz.seb.sebserver.webservice.servicelayer.PaginationService;
import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.AuthorizationService;
import ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction.BulkActionService;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ConfigurationDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamConfigurationMapDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserActivityLogDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ConfigurationChangedEvent;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamConfigUpdateService;
import ch.ethz.seb.sebserver.webservice.servicelayer.validation.BeanValidationService;
@WebServiceProfile
@ -48,9 +38,7 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.validation.BeanValidationSe
public class ConfigurationController extends ReadonlyEntityController<Configuration, Configuration> {
private final ConfigurationDAO configurationDAO;
private final ExamConfigurationMapDAO examConfigurationMapDAO;
private final ApplicationEventPublisher applicationEventPublisher;
private final ExamSessionService examSessionService;
private final ExamConfigUpdateService examConfigUpdateService;
protected ConfigurationController(
final AuthorizationService authorization,
@ -59,9 +47,7 @@ public class ConfigurationController extends ReadonlyEntityController<Configurat
final UserActivityLogDAO userActivityLogDAO,
final PaginationService paginationService,
final BeanValidationService beanValidationService,
final ApplicationEventPublisher applicationEventPublisher,
final ExamSessionService examSessionService,
final ExamConfigurationMapDAO examConfigurationMapDAO) {
final ExamConfigUpdateService examConfigUpdateService) {
super(authorization,
bulkActionService,
@ -71,9 +57,7 @@ public class ConfigurationController extends ReadonlyEntityController<Configurat
beanValidationService);
this.configurationDAO = entityDAO;
this.applicationEventPublisher = applicationEventPublisher;
this.examSessionService = examSessionService;
this.examConfigurationMapDAO = examConfigurationMapDAO;
this.examConfigUpdateService = examConfigUpdateService;
}
@RequestMapping(
@ -81,14 +65,13 @@ public class ConfigurationController extends ReadonlyEntityController<Configurat
method = RequestMethod.POST,
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE,
produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public Configuration saveToHistory(@PathVariable final String modelId) {
public Configuration saveToHistory(@PathVariable final Long modelId) {
return this.entityDAO.byModelId(modelId)
return this.entityDAO.byPK(modelId)
.flatMap(this.authorization::checkModify)
.flatMap(this::checkRunningExamIntegrity)
.flatMap(config -> this.configurationDAO.saveToHistory(config.configurationNodeId))
.flatMap(this.examConfigUpdateService::processSEBExamConfigurationChange)
.onError(t -> this.examConfigUpdateService.forceReleaseUpdateLocks(modelId))
.flatMap(this.userActivityLogDAO::logSaveToHistory)
.flatMap(this::publishConfigChanged)
.getOrThrow();
}
@ -103,7 +86,6 @@ public class ConfigurationController extends ReadonlyEntityController<Configurat
.flatMap(this.authorization::checkModify)
.flatMap(config -> this.configurationDAO.undo(config.configurationNodeId))
.flatMap(this.userActivityLogDAO::logUndo)
.flatMap(this::publishConfigChanged)
.getOrThrow();
}
@ -119,7 +101,6 @@ public class ConfigurationController extends ReadonlyEntityController<Configurat
return this.entityDAO.byModelId(modelId)
.flatMap(this.authorization::checkModify)
.flatMap(config -> this.configurationDAO.restoreToVersion(configurationNodeId, config.getId()))
.flatMap(this::publishConfigChanged)
.getOrThrow();
}
@ -133,55 +114,4 @@ public class ConfigurationController extends ReadonlyEntityController<Configurat
return ConfigurationRecordDynamicSqlSupport.configurationRecord;
}
// NOTE: This will not properly work within a distributed setup since other instances
// are not notified about the configuration change
// TODO: find a way to manage the notification of a changed configuration that works
// also on distributed environments. For example use the database to store configuration
// changed information and check before getting a configuration from cache if it is still valid
private Result<Configuration> publishConfigChanged(final Configuration config) {
this.applicationEventPublisher.publishEvent(new ConfigurationChangedEvent(config.id));
return Result.of(config);
}
private Result<Configuration> checkRunningExamIntegrity(final Configuration config) {
// check if the configuration is attached to an exam
final long activeConnections = this.examConfigurationMapDAO
.getExamIdsForConfigNodeId(config.configurationNodeId)
.getOrThrow()
.stream()
.flatMap(examId -> {
return this.examSessionService
.getConnectionData(examId)
.getOrThrow()
.stream();
})
.filter(this::isActiveConnection)
.count();
if (activeConnections > 0) {
throw new APIMessage.APIMessageException(
ErrorMessage.INTEGRITY_VALIDATION,
"Integrity violation: There are currently active SEB Client connection.");
} else {
return Result.of(config);
}
}
private boolean isActiveConnection(final ClientConnectionData connection) {
if (connection.clientConnection.status == ConnectionStatus.ESTABLISHED
|| connection.clientConnection.status == ConnectionStatus.AUTHENTICATED) {
return true;
}
if (connection.clientConnection.status == ConnectionStatus.CONNECTION_REQUESTED) {
final Long creationTime = connection.clientConnection.getCreationTime();
final long millisecondsNow = Utils.getMillisecondsNow();
if (millisecondsNow - creationTime < 30 * Constants.SECOND_IN_MILLIS) {
return true;
}
}
return false;
}
}

View file

@ -27,8 +27,9 @@ sebserver.webservice.http.redirect.gui=/gui
sebserver.webservice.api.admin.endpoint=/admin-api/v1
sebserver.webservice.api.admin.accessTokenValiditySeconds=3600
sebserver.webservice.api.admin.refreshTokenValiditySeconds=-1
sebserver.webservice.api.exam.time-prefix=3600000
sebserver.webservice.api.exam.time-suffix=3600000
sebserver.webservice.api.exam.update-interval=1 * * * * *
sebserver.webservice.api.exam.time-prefix=0
sebserver.webservice.api.exam.time-suffix=0
sebserver.webservice.api.exam.endpoint=/exam-api
sebserver.webservice.api.exam.endpoint.discovery=${sebserver.webservice.api.exam.endpoint}/discovery
sebserver.webservice.api.exam.endpoint.v1=${sebserver.webservice.api.exam.endpoint}/v1

View file

@ -448,6 +448,7 @@ sebserver.examconfig.action.modify.properties=Edit Configuration
sebserver.examconfig.action.save=Save
sebserver.examconfig.action.saveToHistory=Save / Publish
sebserver.examconfig.action.saveToHistory.success=Successfully saved in history
sebserver.examconfig.action.saveToHistory.integrity-violation=There is currently at least one running Exam with active SEB client connections that uses this Configuration.<br/>Modify of a configuration that is currently in use would lead to inconsistency and is therefore not allowed.<br/><br/>Please make sure that the configuration is not in use before applying changes.
sebserver.examconfig.action.undo=Undo
sebserver.examconfig.action.undo.success=Successfully reverted to last saved state
sebserver.examconfig.action.copy=Copy Configuration
@ -486,6 +487,8 @@ sebserver.examconfig.props.form.views.security=Security
sebserver.examconfig.props.form.views.registry=Registry
sebserver.examconfig.props.form.views.hooked_keys=Hooked Keys
sebserver.examconfig.props.label.hashedAdminPassword=Administrator password
sebserver.examconfig.props.label.hashedAdminPassword.confirm=Confirm password
sebserver.examconfig.props.label.allowQuit=Allow user to quit SEB

View file

@ -706,7 +706,8 @@ public class UseCasesIntegrationTest extends GuiIntegrationTest {
null, null,
Utils.immutableCollectionOf(userId),
ExamStatus.RUNNING,
true);
true,
null);
final Result<Exam> savedExamResult = restService
.getBuilder(SaveExam.class)

View file

@ -65,7 +65,8 @@ public class ExamAPITest extends AdministrationAPIIntegrationTester {
exam.owner,
Arrays.asList("user5"),
null,
true))
true,
null))
.withExpectedStatus(HttpStatus.OK)
.getAsObject(new TypeReference<Exam>() {
});
@ -94,7 +95,8 @@ public class ExamAPITest extends AdministrationAPIIntegrationTester {
exam.owner,
Arrays.asList("user2"),
null,
true))
true,
null))
.withExpectedStatus(HttpStatus.BAD_REQUEST)
.getAsObject(new TypeReference<List<APIMessage>>() {
});

View file

@ -59,7 +59,7 @@ public class QuizDataTest extends AdministrationAPIIntegrationTester {
});
assertNotNull(quizzes);
assertTrue(quizzes.content.size() == 7);
assertTrue(quizzes.content.size() == 8);
// for the inactive LmsSetup we should'nt get any quizzes
quizzes = new RestAPITestHelper()
@ -109,7 +109,7 @@ public class QuizDataTest extends AdministrationAPIIntegrationTester {
});
assertNotNull(quizzes);
assertTrue(quizzes.content.size() == 7);
assertTrue(quizzes.content.size() == 8);
// but for the now active lmsSetup2 we should get the quizzes
quizzes = new RestAPITestHelper()
@ -120,7 +120,7 @@ public class QuizDataTest extends AdministrationAPIIntegrationTester {
});
assertNotNull(quizzes);
assertTrue(quizzes.content.size() == 7);
assertTrue(quizzes.content.size() == 8);
}
@Test