From cf8aa0cd009a3c717a484562708345c635c6b215 Mon Sep 17 00:00:00 2001 From: anhefti Date: Thu, 11 Nov 2021 17:07:19 +0100 Subject: [PATCH] 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)); }