From 810b6dc8c24a9947cde186d3f17e872dc8e313b9 Mon Sep 17 00:00:00 2001 From: anhefti Date: Fri, 1 Nov 2019 20:50:42 +0100 Subject: [PATCH] SEBSERV-73 seb exam config add/remove on running exam --- .../session/ExamConfigUpdateService.java | 8 +- .../impl/ExamConfigUpdateServiceImpl.java | 77 ++++++++++++++++--- .../weblayer/api/ConfigurationController.java | 2 +- .../weblayer/api/EntityController.java | 22 +++--- .../ExamConfigurationMappingController.java | 58 ++++++++++---- 5 files changed, 130 insertions(+), 37 deletions(-) diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ExamConfigUpdateService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ExamConfigUpdateService.java index 1124b046..aba6a0e8 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ExamConfigUpdateService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ExamConfigUpdateService.java @@ -9,8 +9,10 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.session; import java.util.Collection; +import java.util.function.Function; import ch.ethz.seb.sebserver.gbl.model.exam.Exam; +import ch.ethz.seb.sebserver.gbl.model.exam.ExamConfigurationMap; import ch.ethz.seb.sebserver.gbl.util.Result; public interface ExamConfigUpdateService { @@ -48,9 +50,11 @@ public interface ExamConfigUpdateService { * * @param configurationNodeId the SEB Configuration node identifier * @return Result refer to a list of involved and updated Exam identifiers */ - Result> processSEBExamConfigurationChange(Long configurationNodeId); + Result> processExamConfigurationChange(Long configurationNodeId); - Result processSEBExamConfigurationAttachmentChange(Long examId); + Result processExamConfigurationMappingChange( + ExamConfigurationMap mapping, + Function> changeAction); /** Use this to force a release of update-locks for all Exams that has the specified * SEB Exam Configuration attached. diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamConfigUpdateServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamConfigUpdateServiceImpl.java index 1e0f13f1..9566b5fd 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamConfigUpdateServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamConfigUpdateServiceImpl.java @@ -11,6 +11,7 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.session.impl; import java.util.Collection; import java.util.Collections; import java.util.Objects; +import java.util.function.Function; import java.util.stream.Collectors; import org.slf4j.Logger; @@ -24,6 +25,7 @@ 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.exam.Exam.ExamStatus; +import ch.ethz.seb.sebserver.gbl.model.exam.ExamConfigurationMap; 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; @@ -73,7 +75,7 @@ public class ExamConfigUpdateServiceImpl implements ExamConfigUpdateService { // evict each Exam from cache and release the update-lock on DB @Override @Transactional - public Result> processSEBExamConfigurationChange(final Long configurationNodeId) { + public Result> processExamConfigurationChange(final Long configurationNodeId) { final String updateId = this.examUpdateHandler.createUpdateId(); @@ -170,21 +172,76 @@ public class ExamConfigUpdateServiceImpl implements ExamConfigUpdateService { } @Override - public Result processSEBExamConfigurationAttachmentChange(final Long examId) { - return this.examDAO.byPK(examId) + @Transactional + public Result processExamConfigurationMappingChange( + final ExamConfigurationMap mapping, + final Function> changeAction) { + + return this.examDAO.byPK(mapping.examId) .map(exam -> { + + // if the exam is not currently running just apply the action if (exam.status != ExamStatus.RUNNING) { - return examId; + return changeAction + .apply(mapping) + .getOrThrow(); } - // TODO Lock?? - // TODO flush cache - // TODO update seb restriction if on - // TODO unlock? + // if the exam is running... + final String updateId = this.examUpdateHandler.createUpdateId(); + if (log.isDebugEnabled()) { + log.debug("Process SEB Exam Configuration mapping update for: {} with update-id {}", + mapping, + updateId); + } - return examId; - }); + // check if there are no active client connections for this exam + checkActiveClientConnections(exam); + // lock the exam + this.examDAO.placeLock(exam.id, updateId) + .getOrThrow(); + + // check again if there are no new active client connections in the meantime + checkActiveClientConnections(exam); + + // apply the referenced change action + final T result = changeAction.apply(mapping) + .getOrThrow(); + + // flush the exam cache + this.examSessionService.flushCache(exam) + .getOrThrow(); + + // update seb client restriction if the feature is activated + if (exam.lmsSebRestriction) { + final Result updateSebClientRestriction = this.updateSebClientRestriction(exam); + if (updateSebClientRestriction.hasError()) { + log.error("Failed to update SEB Client restriction on LMS for exam: {}", exam); + } + } + + // release the lock + this.examDAO.releaseLock(exam.id, updateId) + .getOrThrow(); + + return result; + }) + .onError(TransactionHandler::rollback); + + } + + private void checkActiveClientConnections(final Exam exam) { + if (this.examSessionService.getConnectionData(exam.id) + .getOrThrow() + .stream() + .filter(ExamConfigUpdateServiceImpl::isActiveConnection) + .count() > 0) { + + throw new APIMessage.APIMessageException( + ErrorMessage.INTEGRITY_VALIDATION, + "Integrity violation: There are currently active SEB Client connection."); + } } @Override diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ConfigurationController.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ConfigurationController.java index 2bfd3afe..b92d9566 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ConfigurationController.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ConfigurationController.java @@ -124,7 +124,7 @@ public class ConfigurationController extends ReadonlyEntityController { log.info("Successfully updated SEB Configuration for exams: {}", ids); return config; diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/EntityController.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/EntityController.java index e18e2e94..c8b18989 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/EntityController.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/EntityController.java @@ -316,12 +316,22 @@ public abstract class EntityController { return this.entityDAO.byModelId(modelId) .flatMap(this::checkWriteAccess) .flatMap(this::validForDelete) - .map(this::bulkDelete) + .flatMap(this::bulkDelete) .flatMap(this::notifyDeleted) .flatMap(pair -> this.logBulkAction(pair.b)) .getOrThrow(); } + protected Result> bulkDelete(final T entity) { + return Result.tryCatch(() -> new Pair<>( + entity, + this.bulkActionService.createReport(new BulkAction( + BulkActionType.HARD_DELETE, + entity.entityType(), + new EntityName(entity.getModelId(), entity.entityType(), entity.getName()))) + .getOrThrow())); + } + protected void checkReadPrivilege(final Long institutionId) { this.authorization.check( PrivilegeType.READ, @@ -500,14 +510,4 @@ public abstract class EntityController { * @return the MyBatis SqlTable for the concrete Entity */ protected abstract SqlTable getSQLTableOfEntity(); - private Pair bulkDelete(final T entity) { - return new Pair<>( - entity, - this.bulkActionService.createReport(new BulkAction( - BulkActionType.HARD_DELETE, - entity.entityType(), - new EntityName(entity.getModelId(), entity.entityType(), entity.getName()))) - .getOrThrow()); - } - } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamConfigurationMappingController.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamConfigurationMappingController.java index 0db4d843..5a17d279 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamConfigurationMappingController.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamConfigurationMappingController.java @@ -9,8 +9,13 @@ package ch.ethz.seb.sebserver.webservice.weblayer.api; import org.mybatis.dynamic.sql.SqlTable; +import org.springframework.http.MediaType; +import org.springframework.util.MultiValueMap; import org.springframework.validation.FieldError; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; +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.api.API; @@ -32,6 +37,7 @@ import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ExamConfigurationMapRecordDynamicSqlSupport; import ch.ethz.seb.sebserver.webservice.servicelayer.PaginationService; import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.AuthorizationService; +import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.UserService; import ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction.BulkActionService; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ConfigurationNodeDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.EntityDAO; @@ -116,21 +122,53 @@ public class ExamConfigurationMappingController extends EntityController validForSave(final ExamConfigurationMap entity) { - return super.validForSave(entity) - .map(this::checkPasswordMatch); + @RequestMapping( + method = RequestMethod.POST, + consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, + produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + public ExamConfigurationMap create( + @RequestParam final MultiValueMap allRequestParams, + @RequestParam( + name = API.PARAM_INSTITUTION_ID, + required = true, + defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId) { + + // check modify privilege for requested institution and concrete entityType + this.checkModifyPrivilege(institutionId); + final POSTMapper postMap = new POSTMapper(allRequestParams) + .putIfAbsent(API.PARAM_INSTITUTION_ID, String.valueOf(institutionId)); + + final ExamConfigurationMap requestModel = this.createNew(postMap); + return this.checkCreateAccess(requestModel) + .map(this::checkPasswordMatch) + .flatMap(entity -> this.examConfigUpdateService.processExamConfigurationMappingChange( + entity, + this.entityDAO::createNew)) + .flatMap(this::logCreate) + .flatMap(this::notifyCreated) + .getOrThrow(); } @Override - protected Result validForDelete(final ExamConfigurationMap entity) { - return super.validForDelete(entity) - .map(this::checkNoActiveClientConnections); + @RequestMapping( + path = API.MODEL_ID_VAR_PATH_SEGMENT, + method = RequestMethod.DELETE, + produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + public EntityProcessingReport hardDelete(@PathVariable final String modelId) { + + return this.entityDAO.byModelId(modelId) + .flatMap(this::checkWriteAccess) + .flatMap(entity -> this.examConfigUpdateService.processExamConfigurationMappingChange( + entity, + this::bulkDelete)) + .flatMap(this::notifyDeleted) + .flatMap(pair -> this.logBulkAction(pair.b)) + .getOrThrow(); } @Override protected Result notifyCreated(final ExamConfigurationMap entity) { // update the attached configurations state to "In Use" - // and apply change to involved Exam return this.configurationNodeDAO.save(new ConfigurationNode( entity.configurationNodeId, null, @@ -140,8 +178,6 @@ public class ExamConfigurationMappingController extends EntityController this.examConfigUpdateService - .processSEBExamConfigurationAttachmentChange(entity.examId)) .map(id -> entity); } @@ -149,9 +185,7 @@ public class ExamConfigurationMappingController extends EntityController> notifyDeleted( final Pair pair) { - // update the attached configurations state to "Ready" - // and apply change to involved Exam return this.configurationNodeDAO.save(new ConfigurationNode( pair.a.configurationNodeId, null, @@ -161,8 +195,6 @@ public class ExamConfigurationMappingController extends EntityController this.examConfigUpdateService - .processSEBExamConfigurationAttachmentChange(pair.a.examId)) .map(id -> pair); }