Merge remote-tracking branch 'origin/dev-1.2-isolateExamQuizLoad' into

development

Conflicts:
	pom.xml
	src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamDAOImpl.java
This commit is contained in:
anhefti 2021-11-11 17:19:45 +01:00
commit 35905e9dad
14 changed files with 412 additions and 286 deletions

1
docs/requirements.txt Normal file
View file

@ -0,0 +1 @@
docutils<0.18

View file

@ -99,16 +99,26 @@ public interface ExamDAO extends ActivatableEntityDAO<Exam, Exam>, 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<Exam> placeLock(Long examId, String updateId);
* @return Result refer to the specified exam identifier or to an error if happened */
Result<Long> placeLock(Long examId, String updateId);
default Result<Exam> 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<Exam> releaseLock(Long examId, String updateId);
* @return Result refer to the specified exam identifier or to an error if happened */
Result<Long> releaseLock(Long examId, String updateId);
default Result<Exam> 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

View file

@ -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<Exam> byPK(final Long id) {
return recordById(id)
return this.examRecordDAO
.recordById(id)
.flatMap(this::toDomainModel);
}
@Override
@Transactional(readOnly = true)
public Result<GrantEntity> 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<GrantEntity> 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<Collection<Exam>> 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<Collection<Long>> 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<MyBatis3SelectModelAdapter<List<ExamRecord>>>.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<ExamRecord> 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<Exam> 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<Exam> 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<Exam> 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<Exam> createNew(final Exam exam) {
return Result.tryCatch(() -> {
// fist check if it is not already existing
final List<ExamRecord> 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<Collection<Exam>> allForRunCheck() {
return Result.tryCatch(() -> {
final List<ExamRecord> 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<Collection<Exam>> allForEndCheck() {
return Result.tryCatch(() -> {
final List<ExamRecord> 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<Exam> placeLock(final Long examId, final String updateId) {
public Result<Long> 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<Exam> releaseLock(final Long examId, final String updateId) {
public Result<Long> 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<Boolean> 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<Boolean> 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<Collection<Exam>> allOf(final Set<Long> 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<ExamRecord> 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<EntityDependency> toDependencies(
final List<ExamRecord> records,
final EntityKey parent) {

View file

@ -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<ExamRecord> 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<ExamRecord> recordByClientConnection(final Long connectionId) {
return Result.tryCatch(() -> this.clientConnectionRecordMapper
.selectByPrimaryKey(connectionId))
.flatMap(ccRecord -> recordById(ccRecord.getExamId()));
}
@Transactional(readOnly = true)
public Result<Collection<ExamRecord>> 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<Collection<Long>> 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<Collection<ExamRecord>> 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<MyBatis3SelectModelAdapter<List<ExamRecord>>>.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<ExamRecord> 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<ExamRecord> 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<ExamRecord> 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<ExamRecord> 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<ExamRecord> createNew(final Exam exam) {
return Result.tryCatch(() -> {
// fist check if it is not already existing
final List<ExamRecord> 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<Collection<ExamRecord>> 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<Collection<ExamRecord>> 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<Collection<ExamRecord>> allOf(final Set<Long> pks) {
return Result.tryCatch(() -> this.examRecordMapper.selectByExample()
.where(ExamRecordDynamicSqlSupport.id, isIn(new ArrayList<>(pks)))
.build()
.execute());
}
}

View file

@ -315,7 +315,9 @@ public class ExamConfigUpdateServiceImpl implements ExamConfigUpdateService {
private Collection<Result<Exam>> lockForUpdate(final Collection<Long> 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());
}

View file

@ -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);

View file

@ -193,7 +193,7 @@ public class ExamSessionServiceImpl implements ExamSessionService {
}
@Override
public Result<Exam> getRunningExam(final Long examId) {
public synchronized Result<Exam> getRunningExam(final Long examId) {
if (log.isTraceEnabled()) {
log.trace("Running exam request for exam {}", examId);
}

View file

@ -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));
}

View file

@ -39,7 +39,6 @@ public class CachableJdbcTokenStore implements TokenStore {
}
@Override
@Transactional
public OAuth2AccessToken getAccessToken(final OAuth2Authentication authentication) {
return this.jdbcTokenStore.getAccessToken(authentication);
}

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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}

View file

@ -6,7 +6,7 @@ sebserver.overall.version=SEB Server Version : {0}
sebserver.overall.about=About
sebserver.overall.about.markup=<span style='font-family: Arial, Helvetica,sans-serif;font-size: 25px;font-weight: normal;font-style: normal;color: rgb(31, 64, 122);'>SEB Server About</span><br/><br/><span style='font-family: Arial, Helvetica,sans-serif;font-size: 18px;font-weight: bold;font-style: normal;'>1. Installation.</span><br/><br/><span style='font-family: Arial, Helvetica,sans-serif;font-size: 14px;font-weight: normal;font-style: normal;'>This is the SEB Server development setup.</span>
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!<br/>Are you sure you want to leave the page? The changes will be lost.
sebserver.overall.upload=Please select a file