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 examId the exam identifier
* @param updateId an update identifier * @param updateId an update identifier
* @return Result refer to the specified exam or to an error if happened */ * @return Result refer to the specified exam identifier or to an error if happened */
Result<Exam> placeLock(Long examId, String updateId); 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. /** 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. * The exam will be marked as not locked on the persistence level.
* *
* @param examId the exam identifier * @param examId the exam identifier
* @param updateId an update identifier * @param updateId an update identifier
* @return Result refer to the specified exam or to an error if happened */ * @return Result refer to the specified exam identifier or to an error if happened */
Result<Exam> releaseLock(Long examId, String updateId); 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. /** 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 * 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.apache.commons.lang3.StringUtils;
import org.joda.time.DateTime; import org.joda.time.DateTime;
import org.mybatis.dynamic.sql.SqlBuilder; 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.ApplicationEventPublisher;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component; 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.gbl.util.Utils;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.AdditionalAttributeRecordDynamicSqlSupport; 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.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.ExamRecordDynamicSqlSupport;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ExamRecordMapper; 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.AdditionalAttributeRecord;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ExamRecord; 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.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.ExamDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap; 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.dao.TransactionHandler;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate; 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]"; public static final String FAILED_TO_LOAD_QUIZ_DATA_MARK = "[FAILED TO LOAD DATA FROM LMS]";
private final ExamRecordMapper examRecordMapper; private final ExamRecordMapper examRecordMapper;
private final ClientConnectionRecordMapper clientConnectionRecordMapper; private final ExamRecordDAO examRecordDAO;
private final AdditionalAttributeRecordMapper additionalAttributeRecordMapper;
private final ApplicationEventPublisher applicationEventPublisher; private final ApplicationEventPublisher applicationEventPublisher;
private final AdditionalAttributeRecordMapper additionalAttributeRecordMapper;
private final LmsAPIService lmsAPIService; private final LmsAPIService lmsAPIService;
public ExamDAOImpl( public ExamDAOImpl(
final ExamRecordMapper examRecordMapper, final ExamRecordMapper examRecordMapper,
final ClientConnectionRecordMapper clientConnectionRecordMapper, final ExamRecordDAO examRecordDAO,
final AdditionalAttributeRecordMapper additionalAttributeRecordMapper,
final ApplicationEventPublisher applicationEventPublisher, final ApplicationEventPublisher applicationEventPublisher,
final AdditionalAttributeRecordMapper additionalAttributeRecordMapper,
final LmsAPIService lmsAPIService) { final LmsAPIService lmsAPIService) {
this.examRecordMapper = examRecordMapper; this.examRecordMapper = examRecordMapper;
this.clientConnectionRecordMapper = clientConnectionRecordMapper; this.examRecordDAO = examRecordDAO;
this.additionalAttributeRecordMapper = additionalAttributeRecordMapper;
this.applicationEventPublisher = applicationEventPublisher; this.applicationEventPublisher = applicationEventPublisher;
this.additionalAttributeRecordMapper = additionalAttributeRecordMapper;
this.lmsAPIService = lmsAPIService; this.lmsAPIService = lmsAPIService;
} }
@ -102,63 +95,35 @@ public class ExamDAOImpl implements ExamDAO {
} }
@Override @Override
@Transactional(readOnly = true)
public Result<Exam> byPK(final Long id) { public Result<Exam> byPK(final Long id) {
return recordById(id) return this.examRecordDAO
.recordById(id)
.flatMap(this::toDomainModel); .flatMap(this::toDomainModel);
} }
@Override @Override
@Transactional(readOnly = true)
public Result<GrantEntity> examGrantEntityByPK(final Long id) { public Result<GrantEntity> examGrantEntityByPK(final Long id) {
return recordById(id) return this.examRecordDAO.recordById(id)
.map(record -> toDomainModel(record, null, null).getOrThrow()); .map(record -> toDomainModel(record, null, null).getOrThrow());
} }
@Override @Override
@Transactional(readOnly = true)
public Result<GrantEntity> examGrantEntityByClientConnection(final Long connectionId) { public Result<GrantEntity> examGrantEntityByClientConnection(final Long connectionId) {
return Result.tryCatch(() -> this.clientConnectionRecordMapper return this.examRecordDAO
.selectByPrimaryKey(connectionId)) .recordByClientConnection(connectionId)
.flatMap(ccRecord -> recordById(ccRecord.getExamId()))
.map(record -> toDomainModel(record, null, null).getOrThrow()); .map(record -> toDomainModel(record, null, null).getOrThrow());
} }
@Override @Override
@Transactional(readOnly = true)
public Result<Collection<Exam>> all(final Long institutionId, final Boolean active) { public Result<Collection<Exam>> all(final Long institutionId, final Boolean active) {
return Result.tryCatch(() -> (active != null) return this.examRecordDAO
? this.examRecordMapper.selectByExample() .all(institutionId, active)
.where(
ExamRecordDynamicSqlSupport.institutionId,
isEqualToWhenPresent(institutionId))
.and(
ExamRecordDynamicSqlSupport.active,
isEqualToWhenPresent(BooleanUtils.toIntegerObject(active)))
.build()
.execute()
: this.examRecordMapper.selectByExample()
.build()
.execute())
.flatMap(this::toDomainModel); .flatMap(this::toDomainModel);
} }
@Override @Override
public Result<Collection<Long>> allInstitutionIdsByQuizId(final String quizId) { public Result<Collection<Long>> allInstitutionIdsByQuizId(final String quizId) {
return Result.tryCatch(() -> { return this.examRecordDAO.allInstitutionIdsByQuizId(quizId);
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());
});
} }
@Override @Override
@ -189,51 +154,9 @@ public class ExamDAOImpl implements ExamDAO {
return true; return true;
}; };
// If we have a sort on institution name, join the institution table return this.examRecordDAO
// If we have a sort on lms setup name, join lms setup table .allMatching(filterMap)
final QueryExpressionDSL<MyBatis3SelectModelAdapter<List<ExamRecord>>>.QueryExpressionWhereBuilder whereClause = .flatMap(this::toDomainModel)
(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)
.getOrThrow() .getOrThrow()
.stream() .stream()
.filter(quizDataFilter.and(predicate)) .filter(quizDataFilter.and(predicate))
@ -243,129 +166,32 @@ public class ExamDAOImpl implements ExamDAO {
@Override @Override
public Result<Exam> updateState(final Long examId, final ExamStatus status, final String updateId) { public Result<Exam> updateState(final Long examId, final ExamStatus status, final String updateId) {
return recordById(examId) return this.examRecordDAO
.map(examRecord -> { .updateState(examId, status, updateId)
if (BooleanUtils.isTrue(BooleanUtils.toBooleanObject(examRecord.getUpdating()))) { .flatMap(this::toDomainModel);
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);
} }
@Override @Override
@Transactional
public Result<Exam> save(final Exam exam) { public Result<Exam> save(final Exam exam) {
return Result.tryCatch(() -> { return this.examRecordDAO
.save(exam)
// check internal persistent write-lock .flatMap(this::toDomainModel);
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);
} }
@Override @Override
@Transactional @Transactional
public Result<Exam> setSEBRestriction(final Long examId, final boolean sebRestriction) { public Result<Exam> setSEBRestriction(final Long examId, final boolean sebRestriction) {
return Result.tryCatch(() -> { return this.examRecordDAO
.setSEBRestriction(examId, sebRestriction)
final ExamRecord examRecord = new ExamRecord( .flatMap(this::toDomainModel);
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);
} }
@Override @Override
@Transactional @Transactional
public Result<Exam> createNew(final Exam exam) { public Result<Exam> createNew(final Exam exam) {
return Result.tryCatch(() -> { return this.examRecordDAO
.createNew(exam)
// fist check if it is not already existing .flatMap(this::toDomainModel);
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);
} }
@Override @Override
@ -443,57 +269,27 @@ public class ExamDAOImpl implements ExamDAO {
} }
@Override @Override
@Transactional(readOnly = true)
public Result<Collection<Exam>> allForRunCheck() { public Result<Collection<Exam>> allForRunCheck() {
return Result.tryCatch(() -> { return this.examRecordDAO
final List<ExamRecord> records = this.examRecordMapper.selectByExample() .allForRunCheck()
.where( .flatMap(this::toDomainModel);
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());
});
} }
@Override @Override
@Transactional(readOnly = true) @Transactional(readOnly = true)
public Result<Collection<Exam>> allForEndCheck() { public Result<Collection<Exam>> allForEndCheck() {
return Result.tryCatch(() -> { return this.examRecordDAO
final List<ExamRecord> records = this.examRecordMapper.selectByExample() .allForEndCheck()
.where( .flatMap(this::toDomainModel);
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());
});
} }
@Override @Override
@Transactional(propagation = Propagation.REQUIRES_NEW) @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(() -> { return Result.tryCatch(() -> {
final ExamRecord examRec = this.recordById(examId) final ExamRecord examRec = this.examRecordDAO
.recordById(examId)
.getOrThrow(); .getOrThrow();
// consistency check // consistency check
@ -510,19 +306,18 @@ public class ExamDAOImpl implements ExamDAO {
null, null); null, null);
this.examRecordMapper.updateByPrimaryKeySelective(newRecord); this.examRecordMapper.updateByPrimaryKeySelective(newRecord);
return newRecord; return examId;
}) })
.flatMap(rec -> this.recordById(rec.getId()))
.flatMap(this::toDomainModel)
.onError(TransactionHandler::rollback); .onError(TransactionHandler::rollback);
} }
@Override @Override
@Transactional(propagation = Propagation.REQUIRES_NEW) @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(() -> { return Result.tryCatch(() -> {
final ExamRecord examRec = this.recordById(examId) final ExamRecord examRec = this.examRecordDAO
.recordById(examId)
.getOrThrow(); .getOrThrow();
// consistency check // consistency check
@ -541,10 +336,8 @@ public class ExamDAOImpl implements ExamDAO {
null, null); null, null);
this.examRecordMapper.updateByPrimaryKeySelective(newRecord); this.examRecordMapper.updateByPrimaryKeySelective(newRecord);
return newRecord; return examId;
}) })
.flatMap(rec -> this.recordById(rec.getId()))
.flatMap(this::toDomainModel)
.onError(TransactionHandler::rollback); .onError(TransactionHandler::rollback);
} }
@ -567,7 +360,6 @@ public class ExamDAOImpl implements ExamDAO {
return examRecord.getId(); return examRecord.getId();
}) })
.onError(TransactionHandler::rollback); .onError(TransactionHandler::rollback);
} }
@Override @Override
@ -598,7 +390,8 @@ public class ExamDAOImpl implements ExamDAO {
@Override @Override
@Transactional(readOnly = true) @Transactional(readOnly = true)
public Result<Boolean> isLocked(final Long examId) { public Result<Boolean> isLocked(final Long examId) {
return this.recordById(examId) return this.examRecordDAO
.recordById(examId)
.map(rec -> BooleanUtils.toBooleanObject(rec.getUpdating())); .map(rec -> BooleanUtils.toBooleanObject(rec.getUpdating()));
} }
@ -638,7 +431,8 @@ public class ExamDAOImpl implements ExamDAO {
@Override @Override
@Transactional(readOnly = true) @Transactional(readOnly = true)
public Result<Boolean> upToDate(final Long examId, final String updateId) { public Result<Boolean> upToDate(final Long examId, final String updateId) {
return this.recordById(examId) return this.examRecordDAO
.recordById(examId)
.map(rec -> { .map(rec -> {
if (updateId == null) { if (updateId == null) {
return rec.getLastupdate() == null; return rec.getLastupdate() == null;
@ -708,10 +502,9 @@ public class ExamDAOImpl implements ExamDAO {
@Override @Override
@Transactional(readOnly = true) @Transactional(readOnly = true)
public Result<Collection<Exam>> allOf(final Set<Long> pks) { public Result<Collection<Exam>> allOf(final Set<Long> pks) {
return Result.tryCatch(() -> this.examRecordMapper.selectByExample() return this.examRecordDAO
.where(ExamRecordDynamicSqlSupport.id, isIn(new ArrayList<>(pks))) .allOf(pks)
.build() .flatMap(this::toDomainModel);
.execute()).flatMap(this::toDomainModel);
} }
@Override @Override
@ -805,18 +598,6 @@ public class ExamDAOImpl implements ExamDAO {
userKey)); 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( private Collection<EntityDependency> toDependencies(
final List<ExamRecord> records, final List<ExamRecord> records,
final EntityKey parent) { 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) { private Collection<Result<Exam>> lockForUpdate(final Collection<Long> examIds, final String update) {
return examIds.stream() 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()); .collect(Collectors.toList());
} }

View file

@ -69,7 +69,7 @@ public class ExamSessionCacheService {
cacheNames = CACHE_NAME_RUNNING_EXAM, cacheNames = CACHE_NAME_RUNNING_EXAM,
key = "#examId", key = "#examId",
unless = "#result == null") unless = "#result == null")
public Exam getRunningExam(final Long examId) { public synchronized Exam getRunningExam(final Long examId) {
if (log.isDebugEnabled()) { if (log.isDebugEnabled()) {
log.debug("Verify running exam for id: {}", examId); log.debug("Verify running exam for id: {}", examId);

View file

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

View file

@ -84,7 +84,7 @@ class ExamUpdateHandler {
ExamStatus.RUNNING, ExamStatus.RUNNING,
updateId)) updateId))
.flatMap(this.sebRestrictionService::applySEBClientRestriction) .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(error -> this.examDAO.forceUnlock(exam.id)
.onError(unlockError -> log.error("Failed to force unlock update look for exam: {}", exam.id))); .onError(unlockError -> log.error("Failed to force unlock update look for exam: {}", exam.id)));
} }
@ -101,7 +101,7 @@ class ExamUpdateHandler {
ExamStatus.FINISHED, ExamStatus.FINISHED,
updateId)) updateId))
.flatMap(this.sebRestrictionService::releaseSEBClientRestriction) .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)); .onError(error -> this.examDAO.forceUnlock(exam.id));
} }

View file

@ -39,7 +39,6 @@ public class CachableJdbcTokenStore implements TokenStore {
} }
@Override @Override
@Transactional
public OAuth2AccessToken getAccessToken(final OAuth2Authentication authentication) { public OAuth2AccessToken getAccessToken(final OAuth2Authentication authentication) {
return this.jdbcTokenStore.getAccessToken(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.port=8080
sebserver.gui.webservice.apipath=/admin-api/v1 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 # 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.theme=css/sebserver.css
sebserver.gui.list.page.size=15 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.connectionTimeout=30000
spring.datasource.hikari.idleTimeout=600000 spring.datasource.hikari.idleTimeout=600000
spring.datasource.hikari.maxLifetime=1800000 spring.datasource.hikari.maxLifetime=1800000
spring.datasource.hikari.maximumPoolSize=500 spring.datasource.hikari.maximumPoolSize=5
sebserver.http.client.connect-timeout=15000 sebserver.http.client.connect-timeout=15000
sebserver.http.client.connection-request-timeout=10000 sebserver.http.client.connection-request-timeout=10000
@ -23,7 +23,7 @@ sebserver.webservice.clean-db-on-startup=false
# webservice configuration # webservice configuration
sebserver.init.adminaccount.gen-on-init=false sebserver.init.adminaccount.gen-on-init=false
sebserver.webservice.distributed=false sebserver.webservice.distributed=true
sebserver.webservice.master.delay.threshold=10000 sebserver.webservice.master.delay.threshold=10000
sebserver.webservice.http.external.scheme=http sebserver.webservice.http.external.scheme=http
sebserver.webservice.http.external.servername=localhost 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} 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 # 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.mock-lms-enabled=true
sebserver.gui.webservice.edx-lms-enabled=true sebserver.gui.webservice.edx-lms-enabled=true
sebserver.gui.webservice.moodle-lms-enabled=true sebserver.gui.webservice.moodle-lms-enabled=true
sebserver.gui.seb.client.config.download.filename=SEBServerSettings.seb sebserver.gui.seb.client.config.download.filename=SEBServerSettings.seb
sebserver.gui.seb.exam.config.download.filename=SEBExamSettings.seb sebserver.gui.seb.exam.config.download.filename=SEBExamSettings.seb
sebserver.gui.proctoring.zoom.websdk.version=1.9.8 sebserver.gui.proctoring.zoom.websdk.version=1.9.8
sebserver.gui.filter.date.from.years=2 sebserver.gui.filter.date.from.years=2
# remote proctoring # remote proctoring

View file

@ -31,7 +31,7 @@ spring.datasource.hikari.initializationFailTimeout=3000
spring.datasource.hikari.connectionTimeout=30000 spring.datasource.hikari.connectionTimeout=30000
spring.datasource.hikari.idleTimeout=600000 spring.datasource.hikari.idleTimeout=600000
spring.datasource.hikari.maxLifetime=1800000 spring.datasource.hikari.maxLifetime=1800000
spring.datasource.hikari.maximumPoolSize=500 spring.datasource.hikari.maximumPoolSize=100
### webservice security ### webservice security
spring.datasource.password=${sebserver.mariadb.password} 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=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.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=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.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 sebserver.overall.upload=Please select a file