From 3d651b72cc5fdad4d5fe6125e326937f66de9c53 Mon Sep 17 00:00:00 2001 From: anhefti Date: Wed, 10 Nov 2021 14:34:45 +0100 Subject: [PATCH 1/4] fixed doc build --- docs/requirements.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 docs/requirements.txt diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 00000000..c896d4df --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1 @@ +docutils<0.18 \ No newline at end of file From 8f57c556a27230258b3771df23afbc4ab6764b6e Mon Sep 17 00:00:00 2001 From: anhefti Date: Wed, 10 Nov 2021 15:05:24 +0100 Subject: [PATCH 2/4] fixed documentation link --- src/main/resources/messages.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties index ff7e222a..9a11c799 100644 --- a/src/main/resources/messages.properties +++ b/src/main/resources/messages.properties @@ -6,7 +6,7 @@ sebserver.overall.version=SEB Server Version : {0} sebserver.overall.about=About sebserver.overall.about.markup=SEB Server About

1. Installation.

This is the SEB Server development setup. sebserver.overall.help=Documentation -sebserver.overall.help.link=https://www.safeexambrowser.org/news_en.html +sebserver.overall.help.link=https://seb-server.readthedocs.io/en/latest/index.html sebserver.overall.message.leave.without.save=You have unsaved changes!
Are you sure you want to leave the page? The changes will be lost. sebserver.overall.upload=Please select a file From 7de512d7fed935cd17f86bdd9b866c995c7ff79a Mon Sep 17 00:00:00 2001 From: anhefti Date: Thu, 11 Nov 2021 16:01:35 +0100 Subject: [PATCH 3/4] synchronized running exam cache load to prevent multiple long running transactions while loading an Exam --- .../servicelayer/session/impl/ExamSessionCacheService.java | 2 +- .../servicelayer/session/impl/ExamSessionServiceImpl.java | 2 +- .../webservice/weblayer/oauth/CachableJdbcTokenStore.java | 1 - src/main/resources/config/application-dev-gui.properties | 2 +- src/main/resources/config/application-dev-ws.properties | 4 ++-- src/main/resources/config/application-gui.properties | 3 +-- src/main/resources/config/application-ws.properties | 2 +- 7 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionCacheService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionCacheService.java index c218208f..c93483da 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionCacheService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionCacheService.java @@ -69,7 +69,7 @@ public class ExamSessionCacheService { cacheNames = CACHE_NAME_RUNNING_EXAM, key = "#examId", unless = "#result == null") - public Exam getRunningExam(final Long examId) { + public synchronized Exam getRunningExam(final Long examId) { if (log.isDebugEnabled()) { log.debug("Verify running exam for id: {}", examId); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionServiceImpl.java index 67a62448..ad376ef0 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionServiceImpl.java @@ -193,7 +193,7 @@ public class ExamSessionServiceImpl implements ExamSessionService { } @Override - public Result getRunningExam(final Long examId) { + public synchronized Result getRunningExam(final Long examId) { if (log.isTraceEnabled()) { log.trace("Running exam request for exam {}", examId); } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/oauth/CachableJdbcTokenStore.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/oauth/CachableJdbcTokenStore.java index a3ab3987..8933b1bd 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/oauth/CachableJdbcTokenStore.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/oauth/CachableJdbcTokenStore.java @@ -39,7 +39,6 @@ public class CachableJdbcTokenStore implements TokenStore { } @Override - @Transactional public OAuth2AccessToken getAccessToken(final OAuth2Authentication authentication) { return this.jdbcTokenStore.getAccessToken(authentication); } diff --git a/src/main/resources/config/application-dev-gui.properties b/src/main/resources/config/application-dev-gui.properties index 8069b264..3e830c82 100644 --- a/src/main/resources/config/application-dev-gui.properties +++ b/src/main/resources/config/application-dev-gui.properties @@ -8,7 +8,7 @@ sebserver.gui.webservice.address=localhost sebserver.gui.webservice.port=8080 sebserver.gui.webservice.apipath=/admin-api/v1 # defines the polling interval that is used to poll the webservice for client connection data on a monitored exam page -sebserver.gui.webservice.poll-interval=1000 +#sebserver.gui.webservice.poll-interval=1000 sebserver.gui.theme=css/sebserver.css sebserver.gui.list.page.size=15 diff --git a/src/main/resources/config/application-dev-ws.properties b/src/main/resources/config/application-dev-ws.properties index 374fe1ed..5b2e5488 100644 --- a/src/main/resources/config/application-dev-ws.properties +++ b/src/main/resources/config/application-dev-ws.properties @@ -13,7 +13,7 @@ spring.datasource.hikari.initializationFailTimeout=30000 spring.datasource.hikari.connectionTimeout=30000 spring.datasource.hikari.idleTimeout=600000 spring.datasource.hikari.maxLifetime=1800000 -spring.datasource.hikari.maximumPoolSize=500 +spring.datasource.hikari.maximumPoolSize=5 sebserver.http.client.connect-timeout=15000 sebserver.http.client.connection-request-timeout=10000 @@ -23,7 +23,7 @@ sebserver.webservice.clean-db-on-startup=false # webservice configuration sebserver.init.adminaccount.gen-on-init=false -sebserver.webservice.distributed=false +sebserver.webservice.distributed=true sebserver.webservice.master.delay.threshold=10000 sebserver.webservice.http.external.scheme=http sebserver.webservice.http.external.servername=localhost diff --git a/src/main/resources/config/application-gui.properties b/src/main/resources/config/application-gui.properties index 1b89c317..6b20a69e 100644 --- a/src/main/resources/config/application-gui.properties +++ b/src/main/resources/config/application-gui.properties @@ -25,14 +25,13 @@ sebserver.gui.entrypoint=/gui sebserver.gui.webservice.apipath=${sebserver.webservice.api.admin.endpoint} # defines the polling interval that is used to poll the webservice for client connection data on a monitored exam page -sebserver.gui.webservice.poll-interval=3000 +sebserver.gui.webservice.poll-interval=2000 sebserver.gui.webservice.mock-lms-enabled=true sebserver.gui.webservice.edx-lms-enabled=true sebserver.gui.webservice.moodle-lms-enabled=true sebserver.gui.seb.client.config.download.filename=SEBServerSettings.seb sebserver.gui.seb.exam.config.download.filename=SEBExamSettings.seb sebserver.gui.proctoring.zoom.websdk.version=1.9.8 - sebserver.gui.filter.date.from.years=2 # remote proctoring diff --git a/src/main/resources/config/application-ws.properties b/src/main/resources/config/application-ws.properties index a8b7c3e0..5c3fc468 100644 --- a/src/main/resources/config/application-ws.properties +++ b/src/main/resources/config/application-ws.properties @@ -31,7 +31,7 @@ spring.datasource.hikari.initializationFailTimeout=3000 spring.datasource.hikari.connectionTimeout=30000 spring.datasource.hikari.idleTimeout=600000 spring.datasource.hikari.maxLifetime=1800000 -spring.datasource.hikari.maximumPoolSize=500 +spring.datasource.hikari.maximumPoolSize=100 ### webservice security spring.datasource.password=${sebserver.mariadb.password} From cf8aa0cd009a3c717a484562708345c635c6b215 Mon Sep 17 00:00:00 2001 From: anhefti Date: Thu, 11 Nov 2021 17:07:19 +0100 Subject: [PATCH 4/4] separated exam-record loading (transactional) from quiz data loading before the loading of the persistent exam data and the loading of the quiz data was running all in the same transaction what caused long-time transaction holds while fetching data from LMS. No always exam records are fetched within a DB transaction and after the transaction the exam records get mapped to Exam domain objects that needs also to load LMS data --- .../webservice/servicelayer/dao/ExamDAO.java | 18 +- .../servicelayer/dao/impl/ExamDAOImpl.java | 319 +++-------------- .../servicelayer/dao/impl/ExamRecordDAO.java | 333 ++++++++++++++++++ .../impl/ExamConfigUpdateServiceImpl.java | 4 +- .../session/impl/ExamUpdateHandler.java | 4 +- 5 files changed, 402 insertions(+), 276 deletions(-) create mode 100644 src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamRecordDAO.java diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ExamDAO.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ExamDAO.java index fbcd0840..07aa9998 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ExamDAO.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ExamDAO.java @@ -98,16 +98,26 @@ public interface ExamDAO extends ActivatableEntityDAO, BulkActionSup * * @param examId the exam identifier * @param updateId an update identifier - * @return Result refer to the specified exam or to an error if happened */ - Result placeLock(Long examId, String updateId); + * @return Result refer to the specified exam identifier or to an error if happened */ + Result placeLock(Long examId, String updateId); + + default Result placeLock(final Exam exam, final String updateId) { + return placeLock(exam.id, updateId) + .map(id -> exam); + } /** This is used to release an internal (write)lock for the specified exam. * The exam will be marked as not locked on the persistence level. * * @param examId the exam identifier * @param updateId an update identifier - * @return Result refer to the specified exam or to an error if happened */ - Result releaseLock(Long examId, String updateId); + * @return Result refer to the specified exam identifier or to an error if happened */ + Result releaseLock(Long examId, String updateId); + + default Result releaseLock(final Exam exam, final String updateId) { + return releaseLock(exam.id, updateId) + .map(id -> exam); + } /** This is used to force release an internal (write)lock for the specified exam. * The exam will be marked as not locked on the persistence level even if it is currently locked by another process diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamDAOImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamDAOImpl.java index a258846e..ce0d8420 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamDAOImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamDAOImpl.java @@ -10,7 +10,6 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.dao.impl; import static org.mybatis.dynamic.sql.SqlBuilder.*; -import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; @@ -28,8 +27,6 @@ import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.StringUtils; import org.joda.time.DateTime; import org.mybatis.dynamic.sql.SqlBuilder; -import org.mybatis.dynamic.sql.select.MyBatis3SelectModelAdapter; -import org.mybatis.dynamic.sql.select.QueryExpressionDSL; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; @@ -52,18 +49,13 @@ 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.AdditionalAttributeRecordDynamicSqlSupport; import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.AdditionalAttributeRecordMapper; -import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ClientConnectionRecordMapper; import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ExamRecordDynamicSqlSupport; import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ExamRecordMapper; -import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.InstitutionRecordDynamicSqlSupport; -import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.LmsSetupRecordDynamicSqlSupport; import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.AdditionalAttributeRecord; import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ExamRecord; import ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction.impl.BulkAction; -import ch.ethz.seb.sebserver.webservice.servicelayer.dao.DuplicateResourceException; 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.ResourceNotFoundException; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.TransactionHandler; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate; @@ -77,22 +69,22 @@ public class ExamDAOImpl implements ExamDAO { public static final String FAILED_TO_LOAD_QUIZ_DATA_MARK = "[FAILED TO LOAD DATA FROM LMS]"; private final ExamRecordMapper examRecordMapper; - private final ClientConnectionRecordMapper clientConnectionRecordMapper; - private final AdditionalAttributeRecordMapper additionalAttributeRecordMapper; + private final ExamRecordDAO examRecordDAO; private final ApplicationEventPublisher applicationEventPublisher; + private final AdditionalAttributeRecordMapper additionalAttributeRecordMapper; private final LmsAPIService lmsAPIService; public ExamDAOImpl( final ExamRecordMapper examRecordMapper, - final ClientConnectionRecordMapper clientConnectionRecordMapper, - final AdditionalAttributeRecordMapper additionalAttributeRecordMapper, + final ExamRecordDAO examRecordDAO, final ApplicationEventPublisher applicationEventPublisher, + final AdditionalAttributeRecordMapper additionalAttributeRecordMapper, final LmsAPIService lmsAPIService) { this.examRecordMapper = examRecordMapper; - this.clientConnectionRecordMapper = clientConnectionRecordMapper; - this.additionalAttributeRecordMapper = additionalAttributeRecordMapper; + this.examRecordDAO = examRecordDAO; this.applicationEventPublisher = applicationEventPublisher; + this.additionalAttributeRecordMapper = additionalAttributeRecordMapper; this.lmsAPIService = lmsAPIService; } @@ -102,63 +94,35 @@ public class ExamDAOImpl implements ExamDAO { } @Override - @Transactional(readOnly = true) public Result byPK(final Long id) { - return recordById(id) + return this.examRecordDAO + .recordById(id) .flatMap(this::toDomainModel); } @Override - @Transactional(readOnly = true) public Result examGrantEntityByPK(final Long id) { - return recordById(id) + return this.examRecordDAO.recordById(id) .map(record -> toDomainModel(record, null, null).getOrThrow()); } @Override - @Transactional(readOnly = true) public Result examGrantEntityByClientConnection(final Long connectionId) { - return Result.tryCatch(() -> this.clientConnectionRecordMapper - .selectByPrimaryKey(connectionId)) - .flatMap(ccRecord -> recordById(ccRecord.getExamId())) + return this.examRecordDAO + .recordByClientConnection(connectionId) .map(record -> toDomainModel(record, null, null).getOrThrow()); } @Override - @Transactional(readOnly = true) public Result> all(final Long institutionId, final Boolean active) { - return Result.tryCatch(() -> (active != null) - ? this.examRecordMapper.selectByExample() - .where( - ExamRecordDynamicSqlSupport.institutionId, - isEqualToWhenPresent(institutionId)) - .and( - ExamRecordDynamicSqlSupport.active, - isEqualToWhenPresent(BooleanUtils.toIntegerObject(active))) - .build() - .execute() - : this.examRecordMapper.selectByExample() - .build() - .execute()) + return this.examRecordDAO + .all(institutionId, active) .flatMap(this::toDomainModel); } @Override public Result> allInstitutionIdsByQuizId(final String quizId) { - return Result.tryCatch(() -> { - return this.examRecordMapper.selectByExample() - .where( - ExamRecordDynamicSqlSupport.externalId, - isEqualToWhenPresent(quizId)) - .and( - ExamRecordDynamicSqlSupport.active, - isEqualToWhenPresent(BooleanUtils.toIntegerObject(true))) - .build() - .execute() - .stream() - .map(rec -> rec.getInstitutionId()) - .collect(Collectors.toList()); - }); + return this.examRecordDAO.allInstitutionIdsByQuizId(quizId); } @Override @@ -189,51 +153,9 @@ public class ExamDAOImpl implements ExamDAO { return true; }; - // If we have a sort on institution name, join the institution table - // If we have a sort on lms setup name, join lms setup table - final QueryExpressionDSL>>.QueryExpressionWhereBuilder whereClause = - (filterMap.getBoolean(FilterMap.ATTR_ADD_INSITUTION_JOIN)) - ? this.examRecordMapper - .selectByExample() - .join(InstitutionRecordDynamicSqlSupport.institutionRecord) - .on( - InstitutionRecordDynamicSqlSupport.id, - SqlBuilder.equalTo(ExamRecordDynamicSqlSupport.institutionId)) - .where( - ExamRecordDynamicSqlSupport.active, - isEqualToWhenPresent(filterMap.getActiveAsInt())) - : (filterMap.getBoolean(FilterMap.ATTR_ADD_LMS_SETUP_JOIN)) - ? this.examRecordMapper - .selectByExample() - .join(LmsSetupRecordDynamicSqlSupport.lmsSetupRecord) - .on( - LmsSetupRecordDynamicSqlSupport.id, - SqlBuilder.equalTo(ExamRecordDynamicSqlSupport.lmsSetupId)) - .where( - ExamRecordDynamicSqlSupport.active, - isEqualToWhenPresent(filterMap.getActiveAsInt())) - : this.examRecordMapper.selectByExample() - .where( - ExamRecordDynamicSqlSupport.active, - isEqualToWhenPresent(filterMap.getActiveAsInt())); - - final List records = whereClause - .and( - ExamRecordDynamicSqlSupport.institutionId, - isEqualToWhenPresent(filterMap.getInstitutionId())) - .and( - ExamRecordDynamicSqlSupport.lmsSetupId, - isEqualToWhenPresent(filterMap.getLmsSetupId())) - .and( - ExamRecordDynamicSqlSupport.type, - isEqualToWhenPresent(filterMap.getExamType())) - .and( - ExamRecordDynamicSqlSupport.status, - isEqualToWhenPresent(filterMap.getExamStatus())) - .build() - .execute(); - - return this.toDomainModel(records) + return this.examRecordDAO + .allMatching(filterMap) + .flatMap(this::toDomainModel) .getOrThrow() .stream() .filter(quizDataFilter.and(predicate)) @@ -243,128 +165,32 @@ public class ExamDAOImpl implements ExamDAO { @Override public Result updateState(final Long examId, final ExamStatus status, final String updateId) { - return recordById(examId) - .map(examRecord -> { - if (BooleanUtils.isTrue(BooleanUtils.toBooleanObject(examRecord.getUpdating()))) { - if (!updateId.equals(examRecord.getLastupdate())) { - throw new IllegalStateException("Exam is currently locked: " + examRecord.getExternalId()); - } - } - - final ExamRecord newExamRecord = new ExamRecord( - examRecord.getId(), - null, null, null, null, null, null, null, null, - status.name(), - null, null, null, null); - - this.examRecordMapper.updateByPrimaryKeySelective(newExamRecord); - return this.examRecordMapper.selectByPrimaryKey(examId); - }) - .flatMap(this::toDomainModel) - .onError(TransactionHandler::rollback); + return this.examRecordDAO + .updateState(examId, status, updateId) + .flatMap(this::toDomainModel); } @Override - @Transactional public Result save(final Exam exam) { - return Result.tryCatch(() -> { - - // check internal persistent write-lock - final ExamRecord oldRecord = this.examRecordMapper.selectByPrimaryKey(exam.id); - if (BooleanUtils.isTrue(BooleanUtils.toBooleanObject(oldRecord.getUpdating()))) { - throw new IllegalStateException("Exam is currently locked: " + exam.externalId); - } - - final ExamRecord examRecord = new ExamRecord( - exam.id, - null, null, null, null, - (exam.supporter != null) - ? StringUtils.join(exam.supporter, Constants.LIST_SEPARATOR_CHAR) - : null, - (exam.type != null) - ? exam.type.name() - : null, - null, - exam.browserExamKeys, - (exam.status != null) - ? exam.status.name() - : null, - 1, // seb restriction (deprecated) - null, // updating - null, // lastUpdate - null // active - ); - - this.examRecordMapper.updateByPrimaryKeySelective(examRecord); - return this.examRecordMapper.selectByPrimaryKey(exam.id); - }) - .flatMap(this::toDomainModel) - .onError(TransactionHandler::rollback); + return this.examRecordDAO + .save(exam) + .flatMap(this::toDomainModel); } @Override @Transactional public Result setSEBRestriction(final Long examId, final boolean sebRestriction) { - return Result.tryCatch(() -> { - - final ExamRecord examRecord = new ExamRecord( - examId, - null, null, null, null, null, null, null, null, null, - BooleanUtils.toInteger(sebRestriction), - null, null, null); - - this.examRecordMapper.updateByPrimaryKeySelective(examRecord); - return this.examRecordMapper.selectByPrimaryKey(examId); - }) - .flatMap(this::toDomainModel) - .onError(TransactionHandler::rollback); + return this.examRecordDAO + .setSEBRestriction(examId, sebRestriction) + .flatMap(this::toDomainModel); } @Override @Transactional public Result createNew(final Exam exam) { - return Result.tryCatch(() -> { - - // fist check if it is not already existing - final List records = this.examRecordMapper.selectByExample() - .where(ExamRecordDynamicSqlSupport.lmsSetupId, isEqualTo(exam.lmsSetupId)) - .and(ExamRecordDynamicSqlSupport.externalId, isEqualTo(exam.externalId)) - .build() - .execute(); - - // if there is already an existing imported exam for the quiz, this is - // used to save instead of create a new one - if (records != null && records.size() > 0) { - final ExamRecord examRecord = records.get(0); - // if the same institution tries to import an exam that already exists throw an error - if (exam.institutionId.equals(examRecord.getInstitutionId())) { - throw new DuplicateResourceException(EntityType.EXAM, exam.externalId); - } - } - - final ExamRecord examRecord = new ExamRecord( - null, - exam.institutionId, - exam.lmsSetupId, - exam.externalId, - exam.owner, - (exam.supporter != null) - ? StringUtils.join(exam.supporter, Constants.LIST_SEPARATOR_CHAR) - : null, - (exam.type != null) ? exam.type.name() : ExamType.UNDEFINED.name(), - null, // quitPassword - null, // browser keys - (exam.status != null) ? exam.status.name() : ExamStatus.UP_COMING.name(), - 1, // seb restriction (deprecated) - BooleanUtils.toInteger(false), - null, // lastUpdate - BooleanUtils.toInteger(true)); - - this.examRecordMapper.insert(examRecord); - return examRecord; - }) - .flatMap(this::toDomainModel) - .onError(TransactionHandler::rollback); + return this.examRecordDAO + .createNew(exam) + .flatMap(this::toDomainModel); } @Override @@ -442,57 +268,27 @@ public class ExamDAOImpl implements ExamDAO { } @Override - @Transactional(readOnly = true) public Result> allForRunCheck() { - return Result.tryCatch(() -> { - final List records = this.examRecordMapper.selectByExample() - .where( - ExamRecordDynamicSqlSupport.active, - isEqualTo(BooleanUtils.toInteger(true))) - .and( - ExamRecordDynamicSqlSupport.status, - isNotEqualTo(ExamStatus.RUNNING.name())) - .and( - ExamRecordDynamicSqlSupport.updating, - isEqualTo(BooleanUtils.toInteger(false))) - - .build() - .execute(); - - return new ArrayList<>(this.toDomainModel(records) - .getOrThrow()); - }); + return this.examRecordDAO + .allForRunCheck() + .flatMap(this::toDomainModel); } @Override @Transactional(readOnly = true) public Result> allForEndCheck() { - return Result.tryCatch(() -> { - final List records = this.examRecordMapper.selectByExample() - .where( - ExamRecordDynamicSqlSupport.active, - isEqualTo(BooleanUtils.toInteger(true))) - .and( - ExamRecordDynamicSqlSupport.status, - isEqualTo(ExamStatus.RUNNING.name())) - .and( - ExamRecordDynamicSqlSupport.updating, - isEqualTo(BooleanUtils.toInteger(false))) - - .build() - .execute(); - - return new ArrayList<>(this.toDomainModel(records) - .getOrThrow()); - }); + return this.examRecordDAO + .allForEndCheck() + .flatMap(this::toDomainModel); } @Override @Transactional(propagation = Propagation.REQUIRES_NEW) - public Result placeLock(final Long examId, final String updateId) { + public Result placeLock(final Long examId, final String updateId) { return Result.tryCatch(() -> { - final ExamRecord examRec = this.recordById(examId) + final ExamRecord examRec = this.examRecordDAO + .recordById(examId) .getOrThrow(); // consistency check @@ -509,19 +305,18 @@ public class ExamDAOImpl implements ExamDAO { null); this.examRecordMapper.updateByPrimaryKeySelective(newRecord); - return newRecord; + return examId; }) - .flatMap(rec -> this.recordById(rec.getId())) - .flatMap(this::toDomainModel) .onError(TransactionHandler::rollback); } @Override @Transactional(propagation = Propagation.REQUIRES_NEW) - public Result releaseLock(final Long examId, final String updateId) { + public Result releaseLock(final Long examId, final String updateId) { return Result.tryCatch(() -> { - final ExamRecord examRec = this.recordById(examId) + final ExamRecord examRec = this.examRecordDAO + .recordById(examId) .getOrThrow(); // consistency check @@ -540,10 +335,8 @@ public class ExamDAOImpl implements ExamDAO { null); this.examRecordMapper.updateByPrimaryKeySelective(newRecord); - return newRecord; + return examId; }) - .flatMap(rec -> this.recordById(rec.getId())) - .flatMap(this::toDomainModel) .onError(TransactionHandler::rollback); } @@ -566,7 +359,6 @@ public class ExamDAOImpl implements ExamDAO { return examRecord.getId(); }) .onError(TransactionHandler::rollback); - } @Override @@ -597,7 +389,8 @@ public class ExamDAOImpl implements ExamDAO { @Override @Transactional(readOnly = true) public Result isLocked(final Long examId) { - return this.recordById(examId) + return this.examRecordDAO + .recordById(examId) .map(rec -> BooleanUtils.toBooleanObject(rec.getUpdating())); } @@ -637,7 +430,8 @@ public class ExamDAOImpl implements ExamDAO { @Override @Transactional(readOnly = true) public Result upToDate(final Long examId, final String updateId) { - return this.recordById(examId) + return this.examRecordDAO + .recordById(examId) .map(rec -> { if (updateId == null) { return rec.getLastupdate() == null; @@ -707,10 +501,9 @@ public class ExamDAOImpl implements ExamDAO { @Override @Transactional(readOnly = true) public Result> allOf(final Set pks) { - return Result.tryCatch(() -> this.examRecordMapper.selectByExample() - .where(ExamRecordDynamicSqlSupport.id, isIn(new ArrayList<>(pks))) - .build() - .execute()).flatMap(this::toDomainModel); + return this.examRecordDAO + .allOf(pks) + .flatMap(this::toDomainModel); } @Override @@ -757,18 +550,6 @@ public class ExamDAOImpl implements ExamDAO { userKey)); } - private Result recordById(final Long id) { - return Result.tryCatch(() -> { - final ExamRecord record = this.examRecordMapper.selectByPrimaryKey(id); - if (record == null) { - throw new ResourceNotFoundException( - EntityType.EXAM, - String.valueOf(id)); - } - return record; - }); - } - private Collection toDependencies( final List records, final EntityKey parent) { diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamRecordDAO.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamRecordDAO.java new file mode 100644 index 00000000..8b614b83 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamRecordDAO.java @@ -0,0 +1,333 @@ +/* + * Copyright (c) 2021 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.dao.impl; + +import static org.mybatis.dynamic.sql.SqlBuilder.*; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import org.apache.commons.lang3.BooleanUtils; +import org.apache.commons.lang3.StringUtils; +import org.mybatis.dynamic.sql.SqlBuilder; +import org.mybatis.dynamic.sql.select.MyBatis3SelectModelAdapter; +import org.mybatis.dynamic.sql.select.QueryExpressionDSL; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import ch.ethz.seb.sebserver.gbl.Constants; +import ch.ethz.seb.sebserver.gbl.api.EntityType; +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.Exam.ExamType; +import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; +import ch.ethz.seb.sebserver.gbl.util.Result; +import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ClientConnectionRecordMapper; +import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ExamRecordDynamicSqlSupport; +import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ExamRecordMapper; +import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.InstitutionRecordDynamicSqlSupport; +import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.LmsSetupRecordDynamicSqlSupport; +import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ExamRecord; +import ch.ethz.seb.sebserver.webservice.servicelayer.dao.DuplicateResourceException; +import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap; +import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ResourceNotFoundException; +import ch.ethz.seb.sebserver.webservice.servicelayer.dao.TransactionHandler; + +@Lazy +@Component +@WebServiceProfile +public class ExamRecordDAO { + + private final ExamRecordMapper examRecordMapper; + private final ClientConnectionRecordMapper clientConnectionRecordMapper; + + public ExamRecordDAO( + final ExamRecordMapper examRecordMapper, + final ClientConnectionRecordMapper clientConnectionRecordMapper) { + + this.examRecordMapper = examRecordMapper; + this.clientConnectionRecordMapper = clientConnectionRecordMapper; + } + + @Transactional(readOnly = true) + public Result recordById(final Long id) { + return Result.tryCatch(() -> { + final ExamRecord record = this.examRecordMapper.selectByPrimaryKey(id); + if (record == null) { + throw new ResourceNotFoundException( + EntityType.EXAM, + String.valueOf(id)); + } + return record; + }); + } + + @Transactional(readOnly = true) + public Result recordByClientConnection(final Long connectionId) { + return Result.tryCatch(() -> this.clientConnectionRecordMapper + .selectByPrimaryKey(connectionId)) + .flatMap(ccRecord -> recordById(ccRecord.getExamId())); + } + + @Transactional(readOnly = true) + public Result> all(final Long institutionId, final Boolean active) { + return Result.tryCatch(() -> (active != null) + ? this.examRecordMapper.selectByExample() + .where( + ExamRecordDynamicSqlSupport.institutionId, + isEqualToWhenPresent(institutionId)) + .and( + ExamRecordDynamicSqlSupport.active, + isEqualToWhenPresent(BooleanUtils.toIntegerObject(active))) + .build() + .execute() + : this.examRecordMapper.selectByExample() + .build() + .execute()); + } + + @Transactional(readOnly = true) + public Result> allInstitutionIdsByQuizId(final String quizId) { + return Result.tryCatch(() -> { + return this.examRecordMapper.selectByExample() + .where( + ExamRecordDynamicSqlSupport.externalId, + isEqualToWhenPresent(quizId)) + .and( + ExamRecordDynamicSqlSupport.active, + isEqualToWhenPresent(BooleanUtils.toIntegerObject(true))) + .build() + .execute() + .stream() + .map(rec -> rec.getInstitutionId()) + .collect(Collectors.toList()); + }); + } + + @Transactional(readOnly = true) + public Result> allMatching(final FilterMap filterMap) { + + return Result.tryCatch(() -> { + + // If we have a sort on institution name, join the institution table + // If we have a sort on lms setup name, join lms setup table + final QueryExpressionDSL>>.QueryExpressionWhereBuilder whereClause = + (filterMap.getBoolean(FilterMap.ATTR_ADD_INSITUTION_JOIN)) + ? this.examRecordMapper + .selectByExample() + .join(InstitutionRecordDynamicSqlSupport.institutionRecord) + .on( + InstitutionRecordDynamicSqlSupport.id, + SqlBuilder.equalTo(ExamRecordDynamicSqlSupport.institutionId)) + .where( + ExamRecordDynamicSqlSupport.active, + isEqualToWhenPresent(filterMap.getActiveAsInt())) + : (filterMap.getBoolean(FilterMap.ATTR_ADD_LMS_SETUP_JOIN)) + ? this.examRecordMapper + .selectByExample() + .join(LmsSetupRecordDynamicSqlSupport.lmsSetupRecord) + .on( + LmsSetupRecordDynamicSqlSupport.id, + SqlBuilder.equalTo(ExamRecordDynamicSqlSupport.lmsSetupId)) + .where( + ExamRecordDynamicSqlSupport.active, + isEqualToWhenPresent(filterMap.getActiveAsInt())) + : this.examRecordMapper.selectByExample() + .where( + ExamRecordDynamicSqlSupport.active, + isEqualToWhenPresent(filterMap.getActiveAsInt())); + + final List records = whereClause + .and( + ExamRecordDynamicSqlSupport.institutionId, + isEqualToWhenPresent(filterMap.getInstitutionId())) + .and( + ExamRecordDynamicSqlSupport.lmsSetupId, + isEqualToWhenPresent(filterMap.getLmsSetupId())) + .and( + ExamRecordDynamicSqlSupport.type, + isEqualToWhenPresent(filterMap.getExamType())) + .and( + ExamRecordDynamicSqlSupport.status, + isEqualToWhenPresent(filterMap.getExamStatus())) + .build() + .execute(); + + return records; + }); + } + + @Transactional + public Result updateState(final Long examId, final ExamStatus status, final String updateId) { + return recordById(examId) + .map(examRecord -> { + if (BooleanUtils.isTrue(BooleanUtils.toBooleanObject(examRecord.getUpdating()))) { + if (!updateId.equals(examRecord.getLastupdate())) { + throw new IllegalStateException("Exam is currently locked: " + examRecord.getExternalId()); + } + } + + final ExamRecord newExamRecord = new ExamRecord( + examRecord.getId(), + null, null, null, null, null, null, null, null, + status.name(), + null, null, null, null); + + this.examRecordMapper.updateByPrimaryKeySelective(newExamRecord); + return this.examRecordMapper.selectByPrimaryKey(examId); + }) + .onError(TransactionHandler::rollback); + } + + @Transactional + public Result save(final Exam exam) { + return Result.tryCatch(() -> { + + // check internal persistent write-lock + final ExamRecord oldRecord = this.examRecordMapper.selectByPrimaryKey(exam.id); + if (BooleanUtils.isTrue(BooleanUtils.toBooleanObject(oldRecord.getUpdating()))) { + throw new IllegalStateException("Exam is currently locked: " + exam.externalId); + } + + final ExamRecord examRecord = new ExamRecord( + exam.id, + null, null, null, null, + (exam.supporter != null) + ? StringUtils.join(exam.supporter, Constants.LIST_SEPARATOR_CHAR) + : null, + (exam.type != null) + ? exam.type.name() + : null, + null, + exam.browserExamKeys, + (exam.status != null) + ? exam.status.name() + : null, + 1, // seb restriction (deprecated) + null, // updating + null, // lastUpdate + null // active + ); + + this.examRecordMapper.updateByPrimaryKeySelective(examRecord); + return this.examRecordMapper.selectByPrimaryKey(exam.id); + }) + .onError(TransactionHandler::rollback); + } + + @Transactional + public Result setSEBRestriction(final Long examId, final boolean sebRestriction) { + return Result.tryCatch(() -> { + + final ExamRecord examRecord = new ExamRecord( + examId, + null, null, null, null, null, null, null, null, null, + BooleanUtils.toInteger(sebRestriction), + null, null, null); + + this.examRecordMapper.updateByPrimaryKeySelective(examRecord); + return this.examRecordMapper.selectByPrimaryKey(examId); + }) + .onError(TransactionHandler::rollback); + } + + @Transactional + public Result createNew(final Exam exam) { + return Result.tryCatch(() -> { + + // fist check if it is not already existing + final List records = this.examRecordMapper.selectByExample() + .where(ExamRecordDynamicSqlSupport.lmsSetupId, isEqualTo(exam.lmsSetupId)) + .and(ExamRecordDynamicSqlSupport.externalId, isEqualTo(exam.externalId)) + .build() + .execute(); + + // if there is already an existing imported exam for the quiz, this is + // used to save instead of create a new one + if (records != null && records.size() > 0) { + final ExamRecord examRecord = records.get(0); + // if the same institution tries to import an exam that already exists throw an error + if (exam.institutionId.equals(examRecord.getInstitutionId())) { + throw new DuplicateResourceException(EntityType.EXAM, exam.externalId); + } + } + + final ExamRecord examRecord = new ExamRecord( + null, + exam.institutionId, + exam.lmsSetupId, + exam.externalId, + exam.owner, + (exam.supporter != null) + ? StringUtils.join(exam.supporter, Constants.LIST_SEPARATOR_CHAR) + : null, + (exam.type != null) ? exam.type.name() : ExamType.UNDEFINED.name(), + null, // quitPassword + null, // browser keys + (exam.status != null) ? exam.status.name() : ExamStatus.UP_COMING.name(), + 1, // seb restriction (deprecated) + BooleanUtils.toInteger(false), + null, // lastUpdate + BooleanUtils.toInteger(true)); + + this.examRecordMapper.insert(examRecord); + return examRecord; + }) + .onError(TransactionHandler::rollback); + } + + @Transactional(readOnly = true) + public Result> allForRunCheck() { + return Result.tryCatch(() -> { + return this.examRecordMapper.selectByExample() + .where( + ExamRecordDynamicSqlSupport.active, + isEqualTo(BooleanUtils.toInteger(true))) + .and( + ExamRecordDynamicSqlSupport.status, + isNotEqualTo(ExamStatus.RUNNING.name())) + .and( + ExamRecordDynamicSqlSupport.updating, + isEqualTo(BooleanUtils.toInteger(false))) + .build() + .execute(); + }); + } + + @Transactional(readOnly = true) + public Result> allForEndCheck() { + return Result.tryCatch(() -> { + return this.examRecordMapper.selectByExample() + .where( + ExamRecordDynamicSqlSupport.active, + isEqualTo(BooleanUtils.toInteger(true))) + .and( + ExamRecordDynamicSqlSupport.status, + isEqualTo(ExamStatus.RUNNING.name())) + .and( + ExamRecordDynamicSqlSupport.updating, + isEqualTo(BooleanUtils.toInteger(false))) + .build() + .execute(); + }); + } + + @Transactional(readOnly = true) + public Result> allOf(final Set pks) { + return Result.tryCatch(() -> this.examRecordMapper.selectByExample() + .where(ExamRecordDynamicSqlSupport.id, isIn(new ArrayList<>(pks))) + .build() + .execute()); + } + +} 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 1947a493..dbe1f565 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 @@ -315,7 +315,9 @@ public class ExamConfigUpdateServiceImpl implements ExamConfigUpdateService { private Collection> lockForUpdate(final Collection examIds, final String update) { return examIds.stream() - .map(id -> this.examDAO.placeLock(id, update)) + .map(id -> this.examDAO.byPK(id) + .map(exam -> this.examDAO.placeLock(exam, update)) + .getOrThrow()) .collect(Collectors.toList()); } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamUpdateHandler.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamUpdateHandler.java index 95ac0472..3defad8d 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamUpdateHandler.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamUpdateHandler.java @@ -84,7 +84,7 @@ class ExamUpdateHandler { ExamStatus.RUNNING, updateId)) .flatMap(this.sebRestrictionService::applySEBClientRestriction) - .flatMap(e -> this.examDAO.releaseLock(e.id, updateId)) + .flatMap(e -> this.examDAO.releaseLock(e, updateId)) .onError(error -> this.examDAO.forceUnlock(exam.id) .onError(unlockError -> log.error("Failed to force unlock update look for exam: {}", exam.id))); } @@ -101,7 +101,7 @@ class ExamUpdateHandler { ExamStatus.FINISHED, updateId)) .flatMap(this.sebRestrictionService::releaseSEBClientRestriction) - .flatMap(e -> this.examDAO.releaseLock(e.id, updateId)) + .flatMap(e -> this.examDAO.releaseLock(e, updateId)) .onError(error -> this.examDAO.forceUnlock(exam.id)); }