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 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 30feb53f..1bbd1a26 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 @@ -99,16 +99,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 fae8d9df..3cc651c3 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 @@ -28,8 +28,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 +50,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 +70,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 +95,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 +154,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,129 +166,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, 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 - exam.examTemplateId); - - 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, 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), - exam.examTemplateId); - - this.examRecordMapper.insert(examRecord); - return examRecord; - }) - .flatMap(this::toDomainModel) - .onError(TransactionHandler::rollback); + return this.examRecordDAO + .createNew(exam) + .flatMap(this::toDomainModel); } @Override @@ -443,57 +269,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 @@ -510,19 +306,18 @@ public class ExamDAOImpl implements ExamDAO { null, 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 @@ -541,10 +336,8 @@ public class ExamDAOImpl implements ExamDAO { null, null); this.examRecordMapper.updateByPrimaryKeySelective(newRecord); - return newRecord; + return examId; }) - .flatMap(rec -> this.recordById(rec.getId())) - .flatMap(this::toDomainModel) .onError(TransactionHandler::rollback); } @@ -567,7 +360,6 @@ public class ExamDAOImpl implements ExamDAO { return examRecord.getId(); }) .onError(TransactionHandler::rollback); - } @Override @@ -598,7 +390,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())); } @@ -638,7 +431,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; @@ -708,10 +502,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 @@ -805,18 +598,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..ec273517 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamRecordDAO.java @@ -0,0 +1,334 @@ +/* + * 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, 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 + exam.examTemplateId); + + 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, 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), + exam.examTemplateId); + + 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/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/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)); } 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 ac5a593b..19c7fcf7 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 a7711e30..a285ffeb 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 4774f003..50e8df39 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} diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties index 7b0fb1a2..81d07814 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